Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (26.4 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
import datetime
35
from django import dispatch
36
from django.conf import settings
37
from django.conf.urls.defaults import patterns
38
from django.db import transaction
39
from django.http import HttpResponse
40
from django.template.loader import render_to_string
41
from django.utils import simplejson as json
42

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

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

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

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

    
75

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

    
84

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

    
95

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

    
104

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

    
115

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

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

    
127

    
128
def nics_to_addresses(nics):
129
    addresses = {}
130
    for nic in nics:
131
        net_nics = []
132
        net_nics.append({"version": 4,
133
                         "addr": nic.ipv4,
134
                         "OS-EXT-IPS:type": "fixed"})
135
        if nic.ipv6:
136
            net_nics.append({"version": 6,
137
                             "addr": nic.ipv6,
138
                             "OS-EXT-IPS:type": "fixed"})
139
        addresses[nic.network.id] = net_nics
140
    return addresses
141

    
142

    
143
def vm_to_dict(vm, detail=False):
144
    d = dict(id=vm.id, name=vm.name)
145
    d['links'] = util.vm_to_links(vm.id)
146
    if detail:
147
        d['user_id'] = vm.userid
148
        d['tenant_id'] = vm.userid
149
        d['status'] = get_rsapi_state(vm)
150
        d['progress'] = 100 if get_rsapi_state(vm) == 'ACTIVE' \
151
            else vm.buildpercentage
152
        d['hostId'] = vm.hostid
153
        d['updated'] = utils.isoformat(vm.updated)
154
        d['created'] = utils.isoformat(vm.created)
155
        d['flavor'] = {"id": vm.flavor.id,
156
                       "links": util.flavor_to_links(vm.flavor.id)}
157
        d['image'] = {"id": vm.imageid,
158
                      "links": util.image_to_links(vm.imageid)}
159
        d['suspended'] = vm.suspended
160

    
161
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
162
        d['metadata'] = metadata
163

    
164
        vm_nics = vm.nics.filter(state="ACTIVE").order_by("index")
165
        attachments = map(nic_to_dict, vm_nics)
166
        d['attachments'] = attachments
167
        d['addresses'] = nics_to_addresses(vm_nics)
168

    
169
        # include the latest vm diagnostic, if set
170
        diagnostic = vm.get_last_diagnostic()
171
        if diagnostic:
172
            d['diagnostics'] = diagnostics_to_dict([diagnostic])
173
        else:
174
            d['diagnostics'] = []
175
        # Fixed
176
        d["security_groups"] = [{"name": "default"}]
177
        d["key_name"] = None
178
        d["config_drive"] = ""
179
        d["accessIPv4"] = ""
180
        d["accessIPv6"] = ""
181

    
182
    return d
183

    
184

    
185
def diagnostics_to_dict(diagnostics):
186
    """
187
    Extract api data from diagnostics QuerySet.
188
    """
189
    entries = list()
190

    
191
    for diagnostic in diagnostics:
192
        # format source date if set
193
        formatted_source_date = None
194
        if diagnostic.source_date:
195
            formatted_source_date = utils.isoformat(diagnostic.source_date)
196

    
197
        entry = {
198
            'source': diagnostic.source,
199
            'created': utils.isoformat(diagnostic.created),
200
            'message': diagnostic.message,
201
            'details': diagnostic.details,
202
            'level': diagnostic.level,
203
        }
204

    
205
        if formatted_source_date:
206
            entry['source_date'] = formatted_source_date
207

    
208
        entries.append(entry)
209

    
210
    return entries
211

    
212

    
213
def render_server(request, server, status=200):
214
    if request.serialization == 'xml':
215
        data = render_to_string('server.xml', {
216
            'server': server,
217
            'is_root': True})
218
    else:
219
        data = json.dumps({'server': server})
220
    return HttpResponse(data, status=status)
221

    
222

    
223
def render_diagnostics(request, diagnostics_dict, status=200):
224
    """
225
    Render diagnostics dictionary to json response.
226
    """
227
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
228

    
229

    
230
@api.api_method(http_method='GET', user_required=True, logger=log)
231
def get_server_diagnostics(request, server_id):
232
    """
233
    Virtual machine diagnostics api view.
234
    """
235
    log.debug('server_diagnostics %s', server_id)
236
    vm = util.get_vm(server_id, request.user_uniq)
237
    diagnostics = diagnostics_to_dict(vm.diagnostics.all())
238
    return render_diagnostics(request, diagnostics)
239

    
240

    
241
@api.api_method(http_method='GET', user_required=True, logger=log)
242
def list_servers(request, detail=False):
243
    # Normal Response Codes: 200, 203
244
    # Error Response Codes: computeFault (400, 500),
245
    #                       serviceUnavailable (503),
246
    #                       unauthorized (401),
247
    #                       badRequest (400),
248
    #                       overLimit (413)
249

    
250
    log.debug('list_servers detail=%s', detail)
251
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
252

    
253
    since = utils.isoparse(request.GET.get('changes-since'))
254

    
255
    if since:
256
        user_vms = user_vms.filter(updated__gte=since)
257
        if not user_vms:
258
            return HttpResponse(status=304)
259
    else:
260
        user_vms = user_vms.filter(deleted=False)
261

    
262
    servers = [vm_to_dict(server, detail)
263
               for server in user_vms.order_by('id')]
264

    
265
    if request.serialization == 'xml':
266
        data = render_to_string('list_servers.xml', {
267
            'servers': servers,
268
            'detail': detail})
269
    else:
270
        data = json.dumps({'servers': servers})
271

    
272
    return HttpResponse(data, status=200)
273

    
274

    
275
@api.api_method(http_method='POST', user_required=True, logger=log)
276
def create_server(request):
277
    # Normal Response Code: 202
278
    # Error Response Codes: computeFault (400, 500),
279
    #                       serviceUnavailable (503),
280
    #                       unauthorized (401),
281
    #                       badMediaType(415),
282
    #                       itemNotFound (404),
283
    #                       badRequest (400),
284
    #                       serverCapacityUnavailable (503),
285
    #                       overLimit (413)
286
    req = utils.get_request_dict(request)
287
    log.info('create_server %s', req)
288
    user_id = request.user_uniq
289

    
290
    try:
291
        server = req['server']
292
        name = server['name']
293
        metadata = server.get('metadata', {})
294
        assert isinstance(metadata, dict)
295
        image_id = server['imageRef']
296
        flavor_id = server['flavorRef']
297
        personality = server.get('personality', [])
298
        assert isinstance(personality, list)
299
    except (KeyError, AssertionError):
300
        raise faults.BadRequest("Malformed request")
301

    
302
    # Verify that personalities are well-formed
303
    util.verify_personality(personality)
304
    # Get image information
305
    image = util.get_image_dict(image_id, user_id)
306
    # Get flavor (ensure it is active)
307
    flavor = util.get_flavor(flavor_id, include_deleted=False)
308
    # Generate password
309
    password = util.random_password()
310

    
311
    vm = do_create_server(user_id, name, password, flavor, image,
312
                          metadata=metadata, personality=personality)
313

    
314
    server = vm_to_dict(vm, detail=True)
315
    server['status'] = 'BUILD'
316
    server['adminPass'] = password
317

    
318
    response = render_server(request, server, status=202)
319

    
320
    return response
321

    
322

    
323
@transaction.commit_manually
324
def do_create_server(userid, name, password, flavor, image, metadata={},
325
                     personality=[], network=None, backend=None):
326
    # Fix flavor for archipelago
327
    disk_template, provider = util.get_flavor_provider(flavor)
328
    if provider:
329
        flavor.disk_template = disk_template
330
        flavor.disk_provider = provider
331
        flavor.disk_origin = image['checksum']
332
        image['backend_id'] = 'null'
333
    else:
334
        flavor.disk_provider = None
335
        flavor.disk_origin = None
336

    
337
    try:
338
        if backend is None:
339
            # Allocate backend to host the server.
340
            backend_allocator = BackendAllocator()
341
            backend = backend_allocator.allocate(userid, flavor)
342
            if backend is None:
343
                log.error("No available backend for VM with flavor %s", flavor)
344
                raise faults.ServiceUnavailable("No available backends")
345

    
346
        if network is None:
347
            # Allocate IP from public network
348
            (network, address) = util.get_public_ip(backend)
349
            nic = {'ip': address, 'network': network.backend_id}
350
        else:
351
            address = util.get_network_free_address(network)
352

    
353
        # We must save the VM instance now, so that it gets a valid
354
        # vm.backend_vm_id.
355
        vm = VirtualMachine.objects.create(
356
            name=name,
357
            backend=backend,
358
            userid=userid,
359
            imageid=image["id"],
360
            flavor=flavor,
361
            action="CREATE")
362

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

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

    
371
        # Also we must create the VM metadata in the same transaction.
372
        for key, val in metadata.items():
373
            VirtualMachineMetadata.objects.create(
374
                meta_key=key,
375
                meta_value=val,
376
                vm=vm)
377
        # Issue commission to Quotaholder and accept it since at the end of
378
        # this transaction the VirtualMachine object will be created in the DB.
379
        # Note: the following call does a commit!
380
        quotas.issue_and_accept_commission(vm)
381
    except:
382
        transaction.rollback()
383
        raise
384
    else:
385
        transaction.commit()
386

    
387
    try:
388
        vm = VirtualMachine.objects.select_for_update().get(id=vm.id)
389
        # dispatch server created signal needed to trigger the 'vmapi', which
390
        # enriches the vm object with the 'config_url' attribute which must be
391
        # passed to the Ganeti job.
392
        server_created.send(sender=vm, created_vm_params={
393
            'img_id': image['backend_id'],
394
            'img_passwd': password,
395
            'img_format': str(image['format']),
396
            'img_personality': json.dumps(personality),
397
            'img_properties': json.dumps(image['metadata']),
398
        })
399

    
400
        jobID = create_instance(vm, nic, flavor, image)
401
        # At this point the job is enqueued in the Ganeti backend
402
        vm.backendjobid = jobID
403
        vm.save()
404
        transaction.commit()
405
        log.info("User %s created VM %s, NIC %s, Backend %s, JobID %s",
406
                 userid, vm, nic, backend, str(jobID))
407
    except:
408
        # If an exception is raised, then the user will never get the VM id.
409
        # In order to delete it from DB and release it's resources, we
410
        # mock a successful OP_INSTANCE_REMOVE job.
411
        process_op_status(vm=vm,
412
                          etime=datetime.datetime.now(),
413
                          jobid=-0,
414
                          opcode="OP_INSTANCE_REMOVE",
415
                          status="success",
416
                          logmsg="Reconciled eventd: VM creation failed.")
417
        raise
418

    
419
    return vm
420

    
421

    
422
@api.api_method(http_method='GET', user_required=True, logger=log)
423
def get_server_details(request, server_id):
424
    # Normal Response Codes: 200, 203
425
    # Error Response Codes: computeFault (400, 500),
426
    #                       serviceUnavailable (503),
427
    #                       unauthorized (401),
428
    #                       badRequest (400),
429
    #                       itemNotFound (404),
430
    #                       overLimit (413)
431

    
432
    log.debug('get_server_details %s', server_id)
433
    vm = util.get_vm(server_id, request.user_uniq)
434
    server = vm_to_dict(vm, detail=True)
435
    return render_server(request, server)
436

    
437

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

    
450
    req = utils.get_request_dict(request)
451
    log.info('update_server_name %s %s', server_id, req)
452

    
453
    try:
454
        name = req['server']['name']
455
    except (TypeError, KeyError):
456
        raise faults.BadRequest("Malformed request")
457

    
458
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
459
                     non_suspended=True)
460
    vm.name = name
461
    vm.save()
462

    
463
    return HttpResponse(status=204)
464

    
465

    
466
@api.api_method(http_method='DELETE', user_required=True, logger=log)
467
@transaction.commit_on_success
468
def delete_server(request, server_id):
469
    # Normal Response Codes: 204
470
    # Error Response Codes: computeFault (400, 500),
471
    #                       serviceUnavailable (503),
472
    #                       unauthorized (401),
473
    #                       itemNotFound (404),
474
    #                       unauthorized (401),
475
    #                       buildInProgress (409),
476
    #                       overLimit (413)
477

    
478
    log.info('delete_server %s', server_id)
479
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
480
                     non_suspended=True)
481
    start_action(vm, 'DESTROY')
482
    delete_instance(vm)
483
    return HttpResponse(status=204)
484

    
485

    
486
# additional server actions
487
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
488

    
489

    
490
@api.api_method(http_method='POST', user_required=True, logger=log)
491
def server_action(request, server_id):
492
    req = utils.get_request_dict(request)
493
    log.debug('server_action %s %s', server_id, req)
494

    
495
    if len(req) != 1:
496
        raise faults.BadRequest("Malformed request")
497

    
498
    # Do not allow any action on deleted or suspended VMs
499
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
500
                     non_deleted=True, non_suspended=True)
501

    
502
    try:
503
        key = req.keys()[0]
504
        if key not in ARBITRARY_ACTIONS:
505
            start_action(vm, key_to_action(key))
506
        val = req[key]
507
        assert isinstance(val, dict)
508
        return server_actions[key](request, vm, val)
509
    except KeyError:
510
        raise faults.BadRequest("Unknown action")
511
    except AssertionError:
512
        raise faults.BadRequest("Invalid argument")
513

    
514

    
515
def key_to_action(key):
516
    """Map HTTP request key to a VM Action"""
517
    if key == "shutdown":
518
        return "STOP"
519
    if key == "delete":
520
        return "DESTROY"
521
    if key in ARBITRARY_ACTIONS:
522
        return None
523
    else:
524
        return key.upper()
525

    
526

    
527
def start_action(vm, action):
528
    log.debug("Applying action %s to VM %s", action, vm)
529
    if not action:
530
        return
531

    
532
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
533
        raise faults.ServiceUnavailable("Action %s not supported" % action)
534

    
535
    # No actions to deleted VMs
536
    if vm.deleted:
537
        raise faults.BadRequest("VirtualMachine has been deleted.")
538

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

    
543
    vm.action = action
544
    vm.backendjobid = None
545
    vm.backendopcode = None
546
    vm.backendjobstatus = None
547
    vm.backendlogmsg = None
548

    
549
    vm.save()
550

    
551

    
552
@api.api_method(http_method='GET', user_required=True, logger=log)
553
def list_addresses(request, server_id):
554
    # Normal Response Codes: 200, 203
555
    # Error Response Codes: computeFault (400, 500),
556
    #                       serviceUnavailable (503),
557
    #                       unauthorized (401),
558
    #                       badRequest (400),
559
    #                       overLimit (413)
560

    
561
    log.debug('list_addresses %s', server_id)
562
    vm = util.get_vm(server_id, request.user_uniq)
563
    attachments = [nic_to_dict(nic) for nic in vm.nics.all()]
564
    addresses = nics_to_addresses(vm.nics.all())
565

    
566
    if request.serialization == 'xml':
567
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
568
    else:
569
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
570

    
571
    return HttpResponse(data, status=200)
572

    
573

    
574
@api.api_method(http_method='GET', user_required=True, logger=log)
575
def list_addresses_by_network(request, server_id, network_id):
576
    # Normal Response Codes: 200, 203
577
    # Error Response Codes: computeFault (400, 500),
578
    #                       serviceUnavailable (503),
579
    #                       unauthorized (401),
580
    #                       badRequest (400),
581
    #                       itemNotFound (404),
582
    #                       overLimit (413)
583

    
584
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
585
    machine = util.get_vm(server_id, request.user_uniq)
586
    network = util.get_network(network_id, request.user_uniq)
587
    nics = machine.nics.filter(network=network).all()
588
    addresses = nics_to_addresses(nics)
589

    
590
    if request.serialization == 'xml':
591
        data = render_to_string('address.xml', {'addresses': addresses})
592
    else:
593
        data = json.dumps({'network': addresses})
594

    
595
    return HttpResponse(data, status=200)
596

    
597

    
598
@api.api_method(http_method='GET', user_required=True, logger=log)
599
def list_metadata(request, server_id):
600
    # Normal Response Codes: 200, 203
601
    # Error Response Codes: computeFault (400, 500),
602
    #                       serviceUnavailable (503),
603
    #                       unauthorized (401),
604
    #                       badRequest (400),
605
    #                       overLimit (413)
606

    
607
    log.debug('list_server_metadata %s', server_id)
608
    vm = util.get_vm(server_id, request.user_uniq)
609
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
610
    return util.render_metadata(request, metadata, use_values=False,
611
                                status=200)
612

    
613

    
614
@api.api_method(http_method='POST', user_required=True, logger=log)
615
def update_metadata(request, server_id):
616
    # Normal Response Code: 201
617
    # Error Response Codes: computeFault (400, 500),
618
    #                       serviceUnavailable (503),
619
    #                       unauthorized (401),
620
    #                       badRequest (400),
621
    #                       buildInProgress (409),
622
    #                       badMediaType(415),
623
    #                       overLimit (413)
624

    
625
    req = utils.get_request_dict(request)
626
    log.info('update_server_metadata %s %s', server_id, req)
627
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
628
    try:
629
        metadata = req['metadata']
630
        assert isinstance(metadata, dict)
631
    except (KeyError, AssertionError):
632
        raise faults.BadRequest("Malformed request")
633

    
634
    for key, val in metadata.items():
635
        meta, created = vm.metadata.get_or_create(meta_key=key)
636
        meta.meta_value = val
637
        meta.save()
638

    
639
    vm.save()
640
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
641
    return util.render_metadata(request, vm_meta, status=201)
642

    
643

    
644
@api.api_method(http_method='GET', user_required=True, logger=log)
645
def get_metadata_item(request, server_id, key):
646
    # Normal Response Codes: 200, 203
647
    # Error Response Codes: computeFault (400, 500),
648
    #                       serviceUnavailable (503),
649
    #                       unauthorized (401),
650
    #                       itemNotFound (404),
651
    #                       badRequest (400),
652
    #                       overLimit (413)
653

    
654
    log.debug('get_server_metadata_item %s %s', server_id, key)
655
    vm = util.get_vm(server_id, request.user_uniq)
656
    meta = util.get_vm_meta(vm, key)
657
    d = {meta.meta_key: meta.meta_value}
658
    return util.render_meta(request, d, status=200)
659

    
660

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

    
674
    req = utils.get_request_dict(request)
675
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
676
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
677
    try:
678
        metadict = req['meta']
679
        assert isinstance(metadict, dict)
680
        assert len(metadict) == 1
681
        assert key in metadict
682
    except (KeyError, AssertionError):
683
        raise faults.BadRequest("Malformed request")
684

    
685
    meta, created = VirtualMachineMetadata.objects.get_or_create(
686
        meta_key=key,
687
        vm=vm)
688

    
689
    meta.meta_value = metadict[key]
690
    meta.save()
691
    vm.save()
692
    d = {meta.meta_key: meta.meta_value}
693
    return util.render_meta(request, d, status=201)
694

    
695

    
696
@api.api_method(http_method='DELETE', user_required=True, logger=log)
697
@transaction.commit_on_success
698
def delete_metadata_item(request, server_id, key):
699
    # Normal Response Code: 204
700
    # Error Response Codes: computeFault (400, 500),
701
    #                       serviceUnavailable (503),
702
    #                       unauthorized (401),
703
    #                       itemNotFound (404),
704
    #                       badRequest (400),
705
    #                       buildInProgress (409),
706
    #                       badMediaType(415),
707
    #                       overLimit (413),
708

    
709
    log.info('delete_server_metadata_item %s %s', server_id, key)
710
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
711
    meta = util.get_vm_meta(vm, key)
712
    meta.delete()
713
    vm.save()
714
    return HttpResponse(status=204)
715

    
716

    
717
@api.api_method(http_method='GET', user_required=True, logger=log)
718
def server_stats(request, server_id):
719
    # Normal Response Codes: 200
720
    # Error Response Codes: computeFault (400, 500),
721
    #                       serviceUnavailable (503),
722
    #                       unauthorized (401),
723
    #                       badRequest (400),
724
    #                       itemNotFound (404),
725
    #                       overLimit (413)
726

    
727
    log.debug('server_stats %s', server_id)
728
    vm = util.get_vm(server_id, request.user_uniq)
729
    #secret = util.encrypt(vm.backend_vm_id)
730
    secret = vm.backend_vm_id      # XXX disable backend id encryption
731

    
732
    stats = {
733
        'serverRef': vm.id,
734
        'refresh': settings.STATS_REFRESH_PERIOD,
735
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
736
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
737
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
738
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
739

    
740
    if request.serialization == 'xml':
741
        data = render_to_string('server_stats.xml', stats)
742
    else:
743
        data = json.dumps({'stats': stats})
744

    
745
    return HttpResponse(data, status=200)