Statistics
| Branch: | Tag: | Revision:

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

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
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
    if backend is None:
326
        # Allocate backend to host the server. Commit after allocation to
327
        # release the locks hold by the backend allocator.
328
        try:
329
            backend_allocator = BackendAllocator()
330
            backend = backend_allocator.allocate(userid, flavor)
331
            if backend is None:
332
                log.error("No available backend for VM with flavor %s", flavor)
333
                raise faults.ServiceUnavailable("No available backends")
334
        except:
335
            transaction.rollback()
336
            raise
337
        else:
338
            transaction.commit()
339

    
340
    # Fix flavor for archipelago
341
    disk_template, provider = util.get_flavor_provider(flavor)
342
    if provider:
343
        flavor.disk_template = disk_template
344
        flavor.disk_provider = provider
345
        flavor.disk_origin = image['checksum']
346
        image['backend_id'] = 'null'
347
    else:
348
        flavor.disk_provider = None
349
        flavor.disk_origin = None
350

    
351
    try:
352
        if network is None:
353
            # Allocate IP from public network
354
            (network, address) = util.get_public_ip(backend)
355
            nic = {'ip': address, 'network': network.backend_id}
356
        else:
357
            address = util.get_network_free_address(network)
358

    
359
        # We must save the VM instance now, so that it gets a valid
360
        # vm.backend_vm_id.
361
        vm = VirtualMachine.objects.create(
362
            name=name,
363
            backend=backend,
364
            userid=userid,
365
            imageid=image["id"],
366
            flavor=flavor,
367
            action="CREATE")
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
        NetworkInterface.objects.create(machine=vm, network=network, index=0,
373
                                        ipv4=address, state="BUILDING")
374

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

    
377
        # dispatch server created signal
378
        server_created.send(sender=vm, created_vm_params={
379
            'img_id': image['backend_id'],
380
            'img_passwd': password,
381
            'img_format': str(image['format']),
382
            'img_personality': json.dumps(personality),
383
            'img_properties': json.dumps(image['metadata']),
384
        })
385

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

    
402
    try:
403
        jobID = create_instance(vm, nic, flavor, image)
404
        # At this point the job is enqueued in the Ganeti backend
405
        vm.backendjobid = jobID
406
        vm.save()
407
        transaction.commit()
408
        log.info("User %s created VM %s, NIC %s, Backend %s, JobID %s",
409
                 userid, vm, nic, backend, str(jobID))
410
    except GanetiApiError as e:
411
        log.exception("Can not communicate to backend %s: %s.",
412
                      backend, e)
413
        # Failed while enqueuing OP_INSTANCE_CREATE to backend. Restore
414
        # already reserved quotas by issuing a negative commission
415
        vm.operstate = "ERROR"
416
        vm.backendlogmsg = "Can not communicate to backend."
417
        vm.deleted = True
418
        vm.save()
419
        quotas.issue_and_accept_commission(vm, delete=True)
420
        raise
421
    except:
422
        transaction.rollback()
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
    start_action(vm, 'DESTROY')
488
    delete_instance(vm)
489
    return HttpResponse(status=204)
490

    
491

    
492
# additional server actions
493
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
494

    
495

    
496
@api.api_method(http_method='POST', user_required=True, logger=log)
497
def server_action(request, server_id):
498
    req = utils.get_request_dict(request)
499
    log.debug('server_action %s %s', server_id, req)
500

    
501
    if len(req) != 1:
502
        raise faults.BadRequest("Malformed request")
503

    
504
    # Do not allow any action on deleted or suspended VMs
505
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
506
                     non_deleted=True, non_suspended=True)
507

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

    
520

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

    
532

    
533
def start_action(vm, action):
534
    log.debug("Applying action %s to VM %s", action, vm)
535
    if not action:
536
        return
537

    
538
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
539
        raise faults.ServiceUnavailable("Action %s not supported" % action)
540

    
541
    # No actions to deleted VMs
542
    if vm.deleted:
543
        raise faults.BadRequest("VirtualMachine has been deleted.")
544

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

    
549
    vm.action = action
550
    vm.backendjobid = None
551
    vm.backendopcode = None
552
    vm.backendjobstatus = None
553
    vm.backendlogmsg = None
554

    
555
    vm.save()
556

    
557

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

    
567
    log.debug('list_addresses %s', server_id)
568
    vm = util.get_vm(server_id, request.user_uniq)
569
    attachments = [nic_to_dict(nic) for nic in vm.nics.all()]
570
    addresses = nics_to_addresses(vm.nics.all())
571

    
572
    if request.serialization == 'xml':
573
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
574
    else:
575
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
576

    
577
    return HttpResponse(data, status=200)
578

    
579

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

    
590
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
591
    machine = util.get_vm(server_id, request.user_uniq)
592
    network = util.get_network(network_id, request.user_uniq)
593
    nics = machine.nics.filter(network=network).all()
594
    addresses = nics_to_addresses(nics)
595

    
596
    if request.serialization == 'xml':
597
        data = render_to_string('address.xml', {'addresses': addresses})
598
    else:
599
        data = json.dumps({'network': addresses})
600

    
601
    return HttpResponse(data, status=200)
602

    
603

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

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

    
619

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

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

    
640
    for key, val in metadata.items():
641
        meta, created = vm.metadata.get_or_create(meta_key=key)
642
        meta.meta_value = val
643
        meta.save()
644

    
645
    vm.save()
646
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
647
    return util.render_metadata(request, vm_meta, status=201)
648

    
649

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

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

    
666

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

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

    
691
    meta, created = VirtualMachineMetadata.objects.get_or_create(
692
        meta_key=key,
693
        vm=vm)
694

    
695
    meta.meta_value = metadict[key]
696
    meta.save()
697
    vm.save()
698
    d = {meta.meta_key: meta.meta_value}
699
    return util.render_meta(request, d, status=201)
700

    
701

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

    
715
    log.info('delete_server_metadata_item %s %s', server_id, key)
716
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
717
    meta = util.get_vm_meta(vm, key)
718
    meta.delete()
719
    vm.save()
720
    return HttpResponse(status=204)
721

    
722

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

    
733
    log.debug('server_stats %s', server_id)
734
    vm = util.get_vm(server_id, request.user_uniq)
735
    #secret = util.encrypt(vm.backend_vm_id)
736
    secret = vm.backend_vm_id      # XXX disable backend id encryption
737

    
738
    stats = {
739
        'serverRef': vm.id,
740
        'refresh': settings.STATS_REFRESH_PERIOD,
741
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
742
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
743
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
744
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
745

    
746
    if request.serialization == 'xml':
747
        data = render_to_string('server_stats.xml', stats)
748
    else:
749
        data = json.dumps({'stats': stats})
750

    
751
    return HttpResponse(data, status=200)