Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (27.1 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
try:
38
    from django.conf.urls import patterns
39
except ImportError:  # Django==1.2
40
    from django.conf.urls.defaults import patterns
41

    
42
from django.db import transaction
43
from django.http import HttpResponse
44
from django.template.loader import render_to_string
45
from django.utils import simplejson as json
46

    
47
from snf_django.lib import api
48
from snf_django.lib.api import faults, utils
49
from synnefo.api import util
50
from synnefo.api.actions import server_actions
51
from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata,
52
                               NetworkInterface)
53
from synnefo.logic.backend import (create_instance, delete_instance,
54
                                   process_op_status, job_is_still_running,
55
                                   vm_exists_in_backend)
56
from synnefo.logic.utils import get_rsapi_state
57
from synnefo.logic.backend_allocator import BackendAllocator
58
from synnefo import quotas
59

    
60
# server creation signal
61
server_created = dispatch.Signal(providing_args=["created_vm_params"])
62

    
63
from logging import getLogger
64
log = getLogger(__name__)
65

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

    
80

    
81
def demux(request):
82
    if request.method == 'GET':
83
        return list_servers(request)
84
    elif request.method == 'POST':
85
        return create_server(request)
86
    else:
87
        return api.api_method_not_allowed(request)
88

    
89

    
90
def server_demux(request, server_id):
91
    if request.method == 'GET':
92
        return get_server_details(request, server_id)
93
    elif request.method == 'PUT':
94
        return update_server_name(request, server_id)
95
    elif request.method == 'DELETE':
96
        return delete_server(request, server_id)
97
    else:
98
        return api.api_method_not_allowed(request)
99

    
100

    
101
def metadata_demux(request, server_id):
102
    if request.method == 'GET':
103
        return list_metadata(request, server_id)
104
    elif request.method == 'POST':
105
        return update_metadata(request, server_id)
106
    else:
107
        return api.api_method_not_allowed(request)
108

    
109

    
110
def metadata_item_demux(request, server_id, key):
111
    if request.method == 'GET':
112
        return get_metadata_item(request, server_id, key)
113
    elif request.method == 'PUT':
114
        return create_metadata_item(request, server_id, key)
115
    elif request.method == 'DELETE':
116
        return delete_metadata_item(request, server_id, key)
117
    else:
118
        return api.api_method_not_allowed(request)
119

    
120

    
121
def nic_to_dict(nic):
122
    d = {'id': util.construct_nic_id(nic),
123
         'network_id': str(nic.network.id),
124
         'mac_address': nic.mac,
125
         'ipv4': nic.ipv4 if nic.ipv4 else None,
126
         'ipv6': nic.ipv6 if nic.ipv6 else None}
127

    
128
    if nic.firewall_profile:
129
        d['firewallProfile'] = nic.firewall_profile
130
    return d
131

    
132

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

    
147

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

    
166
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
167
        d['metadata'] = metadata
168

    
169
        vm_nics = vm.nics.filter(state="ACTIVE").order_by("index")
170
        attachments = map(nic_to_dict, vm_nics)
171
        d['attachments'] = attachments
172
        d['addresses'] = nics_to_addresses(vm_nics)
173

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

    
187
    return d
188

    
189

    
190
def diagnostics_to_dict(diagnostics):
191
    """
192
    Extract api data from diagnostics QuerySet.
193
    """
194
    entries = list()
195

    
196
    for diagnostic in diagnostics:
197
        # format source date if set
198
        formatted_source_date = None
199
        if diagnostic.source_date:
200
            formatted_source_date = utils.isoformat(diagnostic.source_date)
201

    
202
        entry = {
203
            'source': diagnostic.source,
204
            'created': utils.isoformat(diagnostic.created),
205
            'message': diagnostic.message,
206
            'details': diagnostic.details,
207
            'level': diagnostic.level,
208
        }
209

    
210
        if formatted_source_date:
211
            entry['source_date'] = formatted_source_date
212

    
213
        entries.append(entry)
214

    
215
    return entries
216

    
217

    
218
def render_server(request, server, status=200):
219
    if request.serialization == 'xml':
220
        data = render_to_string('server.xml', {
221
            'server': server,
222
            'is_root': True})
223
    else:
224
        data = json.dumps({'server': server})
225
    return HttpResponse(data, status=status)
226

    
227

    
228
def render_diagnostics(request, diagnostics_dict, status=200):
229
    """
230
    Render diagnostics dictionary to json response.
231
    """
232
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
233

    
234

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

    
245

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

    
255
    log.debug('list_servers detail=%s', detail)
256
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
257

    
258
    since = utils.isoparse(request.GET.get('changes-since'))
259

    
260
    if since:
261
        user_vms = user_vms.filter(updated__gte=since)
262
        if not user_vms:
263
            return HttpResponse(status=304)
264
    else:
265
        user_vms = user_vms.filter(deleted=False)
266

    
267
    servers = [vm_to_dict(server, detail)
268
               for server in user_vms.order_by('id')]
269

    
270
    if request.serialization == 'xml':
271
        data = render_to_string('list_servers.xml', {
272
            'servers': servers,
273
            'detail': detail})
274
    else:
275
        data = json.dumps({'servers': servers})
276

    
277
    return HttpResponse(data, status=200)
278

    
279

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

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

    
307
    # Verify that personalities are well-formed
308
    util.verify_personality(personality)
309
    # Get image information
310
    image = util.get_image_dict(image_id, user_id)
311
    # Get flavor (ensure it is active)
312
    flavor = util.get_flavor(flavor_id, include_deleted=False)
313
    # Generate password
314
    password = util.random_password()
315

    
316
    vm = do_create_server(user_id, name, password, flavor, image,
317
                          metadata=metadata, personality=personality)
318

    
319
    server = vm_to_dict(vm, detail=True)
320
    server['status'] = 'BUILD'
321
    server['adminPass'] = password
322

    
323
    response = render_server(request, server, status=202)
324

    
325
    return response
326

    
327

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

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

    
351
        if network is None:
352
            # Allocate IP from public network
353
            (network, address) = util.get_public_ip(backend)
354
        else:
355
            address = util.get_network_free_address(network)
356

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

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

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

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

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

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

    
425
    return vm
426

    
427

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

    
438
    log.debug('get_server_details %s', server_id)
439
    vm = util.get_vm(server_id, request.user_uniq)
440
    server = vm_to_dict(vm, detail=True)
441
    return render_server(request, server)
442

    
443

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

    
456
    req = utils.get_request_dict(request)
457
    log.info('update_server_name %s %s', server_id, req)
458

    
459
    try:
460
        name = req['server']['name']
461
    except (TypeError, KeyError):
462
        raise faults.BadRequest("Malformed request")
463

    
464
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
465
                     non_suspended=True)
466
    vm.name = name
467
    vm.save()
468

    
469
    return HttpResponse(status=204)
470

    
471

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

    
484
    log.info('delete_server %s', server_id)
485
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
486
                     non_suspended=True)
487
    # XXX: Workaround for race where OP_INSTANCE_REMOVE starts executing on
488
    # Ganeti before OP_INSTANCE_CREATE. This will be fixed when
489
    # OP_INSTANCE_REMOVE supports the 'depends' request attribute.
490
    if (vm.backendopcode == "OP_INSTANCE_CREATE" and
491
       vm.backendjobstatus not in ["success", "error", "canceled"]):
492
        if job_is_still_running(vm) and not vm_exists_in_backend(vm):
493
            raise faults.BuildInProgress("Server is being build")
494

    
495
    start_action(vm, 'DESTROY')
496
    delete_instance(vm)
497
    return HttpResponse(status=204)
498

    
499

    
500
# additional server actions
501
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
502

    
503

    
504
@api.api_method(http_method='POST', user_required=True, logger=log)
505
def server_action(request, server_id):
506
    req = utils.get_request_dict(request)
507
    log.debug('server_action %s %s', server_id, req)
508

    
509
    if len(req) != 1:
510
        raise faults.BadRequest("Malformed request")
511

    
512
    # Do not allow any action on deleted or suspended VMs
513
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
514
                     non_deleted=True, non_suspended=True)
515

    
516
    try:
517
        key = req.keys()[0]
518
        if key not in ARBITRARY_ACTIONS:
519
            start_action(vm, key_to_action(key))
520
        val = req[key]
521
        assert isinstance(val, dict)
522
        return server_actions[key](request, vm, val)
523
    except KeyError:
524
        raise faults.BadRequest("Unknown action")
525
    except AssertionError:
526
        raise faults.BadRequest("Invalid argument")
527

    
528

    
529
def key_to_action(key):
530
    """Map HTTP request key to a VM Action"""
531
    if key == "shutdown":
532
        return "STOP"
533
    if key == "delete":
534
        return "DESTROY"
535
    if key in ARBITRARY_ACTIONS:
536
        return None
537
    else:
538
        return key.upper()
539

    
540

    
541
def start_action(vm, action):
542
    log.debug("Applying action %s to VM %s", action, vm)
543
    if not action:
544
        return
545

    
546
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
547
        raise faults.ServiceUnavailable("Action %s not supported" % action)
548

    
549
    # No actions to deleted VMs
550
    if vm.deleted:
551
        raise faults.BadRequest("VirtualMachine has been deleted.")
552

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

    
557
    vm.action = action
558
    vm.backendjobid = None
559
    vm.backendopcode = None
560
    vm.backendjobstatus = None
561
    vm.backendlogmsg = None
562

    
563
    vm.save()
564

    
565

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

    
575
    log.debug('list_addresses %s', server_id)
576
    vm = util.get_vm(server_id, request.user_uniq)
577
    attachments = [nic_to_dict(nic) for nic in vm.nics.all()]
578
    addresses = nics_to_addresses(vm.nics.all())
579

    
580
    if request.serialization == 'xml':
581
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
582
    else:
583
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
584

    
585
    return HttpResponse(data, status=200)
586

    
587

    
588
@api.api_method(http_method='GET', user_required=True, logger=log)
589
def list_addresses_by_network(request, server_id, network_id):
590
    # Normal Response Codes: 200, 203
591
    # Error Response Codes: computeFault (400, 500),
592
    #                       serviceUnavailable (503),
593
    #                       unauthorized (401),
594
    #                       badRequest (400),
595
    #                       itemNotFound (404),
596
    #                       overLimit (413)
597

    
598
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
599
    machine = util.get_vm(server_id, request.user_uniq)
600
    network = util.get_network(network_id, request.user_uniq)
601
    nics = machine.nics.filter(network=network).all()
602
    addresses = nics_to_addresses(nics)
603

    
604
    if request.serialization == 'xml':
605
        data = render_to_string('address.xml', {'addresses': addresses})
606
    else:
607
        data = json.dumps({'network': addresses})
608

    
609
    return HttpResponse(data, status=200)
610

    
611

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

    
621
    log.debug('list_server_metadata %s', server_id)
622
    vm = util.get_vm(server_id, request.user_uniq)
623
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
624
    return util.render_metadata(request, metadata, use_values=False,
625
                                status=200)
626

    
627

    
628
@api.api_method(http_method='POST', user_required=True, logger=log)
629
def update_metadata(request, server_id):
630
    # Normal Response Code: 201
631
    # Error Response Codes: computeFault (400, 500),
632
    #                       serviceUnavailable (503),
633
    #                       unauthorized (401),
634
    #                       badRequest (400),
635
    #                       buildInProgress (409),
636
    #                       badMediaType(415),
637
    #                       overLimit (413)
638

    
639
    req = utils.get_request_dict(request)
640
    log.info('update_server_metadata %s %s', server_id, req)
641
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
642
    try:
643
        metadata = req['metadata']
644
        assert isinstance(metadata, dict)
645
    except (KeyError, AssertionError):
646
        raise faults.BadRequest("Malformed request")
647

    
648
    for key, val in metadata.items():
649
        meta, created = vm.metadata.get_or_create(meta_key=key)
650
        meta.meta_value = val
651
        meta.save()
652

    
653
    vm.save()
654
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
655
    return util.render_metadata(request, vm_meta, status=201)
656

    
657

    
658
@api.api_method(http_method='GET', user_required=True, logger=log)
659
def get_metadata_item(request, server_id, key):
660
    # Normal Response Codes: 200, 203
661
    # Error Response Codes: computeFault (400, 500),
662
    #                       serviceUnavailable (503),
663
    #                       unauthorized (401),
664
    #                       itemNotFound (404),
665
    #                       badRequest (400),
666
    #                       overLimit (413)
667

    
668
    log.debug('get_server_metadata_item %s %s', server_id, key)
669
    vm = util.get_vm(server_id, request.user_uniq)
670
    meta = util.get_vm_meta(vm, key)
671
    d = {meta.meta_key: meta.meta_value}
672
    return util.render_meta(request, d, status=200)
673

    
674

    
675
@api.api_method(http_method='PUT', user_required=True, logger=log)
676
@transaction.commit_on_success
677
def create_metadata_item(request, server_id, key):
678
    # Normal Response Code: 201
679
    # Error Response Codes: computeFault (400, 500),
680
    #                       serviceUnavailable (503),
681
    #                       unauthorized (401),
682
    #                       itemNotFound (404),
683
    #                       badRequest (400),
684
    #                       buildInProgress (409),
685
    #                       badMediaType(415),
686
    #                       overLimit (413)
687

    
688
    req = utils.get_request_dict(request)
689
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
690
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
691
    try:
692
        metadict = req['meta']
693
        assert isinstance(metadict, dict)
694
        assert len(metadict) == 1
695
        assert key in metadict
696
    except (KeyError, AssertionError):
697
        raise faults.BadRequest("Malformed request")
698

    
699
    meta, created = VirtualMachineMetadata.objects.get_or_create(
700
        meta_key=key,
701
        vm=vm)
702

    
703
    meta.meta_value = metadict[key]
704
    meta.save()
705
    vm.save()
706
    d = {meta.meta_key: meta.meta_value}
707
    return util.render_meta(request, d, status=201)
708

    
709

    
710
@api.api_method(http_method='DELETE', user_required=True, logger=log)
711
@transaction.commit_on_success
712
def delete_metadata_item(request, server_id, key):
713
    # Normal Response Code: 204
714
    # Error Response Codes: computeFault (400, 500),
715
    #                       serviceUnavailable (503),
716
    #                       unauthorized (401),
717
    #                       itemNotFound (404),
718
    #                       badRequest (400),
719
    #                       buildInProgress (409),
720
    #                       badMediaType(415),
721
    #                       overLimit (413),
722

    
723
    log.info('delete_server_metadata_item %s %s', server_id, key)
724
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
725
    meta = util.get_vm_meta(vm, key)
726
    meta.delete()
727
    vm.save()
728
    return HttpResponse(status=204)
729

    
730

    
731
@api.api_method(http_method='GET', user_required=True, logger=log)
732
def server_stats(request, server_id):
733
    # Normal Response Codes: 200
734
    # Error Response Codes: computeFault (400, 500),
735
    #                       serviceUnavailable (503),
736
    #                       unauthorized (401),
737
    #                       badRequest (400),
738
    #                       itemNotFound (404),
739
    #                       overLimit (413)
740

    
741
    log.debug('server_stats %s', server_id)
742
    vm = util.get_vm(server_id, request.user_uniq)
743
    #secret = util.encrypt(vm.backend_vm_id)
744
    secret = vm.backend_vm_id      # XXX disable backend id encryption
745

    
746
    stats = {
747
        'serverRef': vm.id,
748
        'refresh': settings.STATS_REFRESH_PERIOD,
749
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
750
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
751
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
752
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
753

    
754
    if request.serialization == 'xml':
755
        data = render_to_string('server_stats.xml', stats)
756
    else:
757
        data = json.dumps({'stats': stats})
758

    
759
    return HttpResponse(data, status=200)