Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (26.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.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.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.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.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 nics_to_addresses(nics):
128
    addresses = {}
129
    for nic in nics:
130
        net_nics = []
131
        net_nics.append({"version": 4,
132
                         "addr": nic.ipv4,
133
                         "OS-EXT-IPS:type": "fixed"})
134
        if nic.ipv6:
135
            net_nics.append({"version": 6,
136
                             "addr": nic.ipv6,
137
                             "OS-EXT-IPS:type": "fixed"})
138
        addresses[nic.network.id] = net_nics
139
    return addresses
140

    
141

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

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

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

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

    
181
    return d
182

    
183

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

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

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

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

    
207
        entries.append(entry)
208

    
209
    return entries
210

    
211

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

    
221

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

    
228

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

    
239

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

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

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

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

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

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

    
271
    return HttpResponse(data, status=200)
272

    
273

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

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

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

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

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

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

    
319
    return response
320

    
321

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

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

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

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

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

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

    
370
        # dispatch server created signal
371
        server_created.send(sender=vm, created_vm_params={
372
            'img_id': image['backend_id'],
373
            'img_passwd': password,
374
            'img_format': str(image['format']),
375
            'img_personality': json.dumps(personality),
376
            'img_properties': json.dumps(image['metadata']),
377
        })
378

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

    
395
    try:
396
        vm = VirtualMachine.objects.select_for_update().get(id=vm.id)
397
        jobID = create_instance(vm, nic, flavor, image)
398
        # At this point the job is enqueued in the Ganeti backend
399
        vm.backendjobid = jobID
400
        vm.save()
401
        transaction.commit()
402
        log.info("User %s created VM %s, NIC %s, Backend %s, JobID %s",
403
                 userid, vm, nic, backend, str(jobID))
404
    except GanetiApiError as e:
405
        log.exception("Can not communicate to backend %s: %s.",
406
                      backend, e)
407
        # Failed while enqueuing OP_INSTANCE_CREATE to backend. Restore
408
        # already reserved quotas by issuing a negative commission
409
        vm.operstate = "ERROR"
410
        vm.backendlogmsg = "Can not communicate to backend."
411
        already_deleted = vm.deleted
412
        vm.deleted = True
413
        vm.save()
414
        if not already_deleted:
415
            quotas.issue_and_accept_commission(vm, delete=True)
416
        raise
417
    except:
418
        transaction.rollback()
419
        raise
420

    
421
    return vm
422

    
423

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

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

    
439

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

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

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

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

    
465
    return HttpResponse(status=204)
466

    
467

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

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

    
487

    
488
# additional server actions
489
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
490

    
491

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

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

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

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

    
516

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

    
528

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

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

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

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

    
545
    vm.action = action
546
    vm.backendjobid = None
547
    vm.backendopcode = None
548
    vm.backendjobstatus = None
549
    vm.backendlogmsg = None
550

    
551
    vm.save()
552

    
553

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

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

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

    
573
    return HttpResponse(data, status=200)
574

    
575

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

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

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

    
597
    return HttpResponse(data, status=200)
598

    
599

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

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

    
615

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

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

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

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

    
645

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

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

    
662

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

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

    
687
    meta, created = VirtualMachineMetadata.objects.get_or_create(
688
        meta_key=key,
689
        vm=vm)
690

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

    
697

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

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

    
718

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

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

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

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

    
747
    return HttpResponse(data, status=200)