Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (22.6 kB)

1
# Copyright 2011-2012 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.conf import settings
35
from django.conf.urls.defaults import patterns
36
from django.db import transaction
37
from django.http import HttpResponse
38
from django.template.loader import render_to_string
39
from django.utils import simplejson as json
40

    
41
from synnefo.api import faults, util
42
from synnefo.api.actions import server_actions
43
from synnefo.api.common import method_not_allowed
44
from synnefo.db.models import VirtualMachine, VirtualMachineMetadata
45
from synnefo.logic.backend import create_instance, delete_instance
46
from synnefo.logic.utils import get_rsapi_state
47
from synnefo.logic.rapi import GanetiApiError
48
from synnefo.logic.backend_allocator import BackendAllocator
49
from synnefo import quotas
50

    
51

    
52
from logging import getLogger
53
log = getLogger('synnefo.api')
54

    
55
urlpatterns = patterns('synnefo.api.servers',
56
    (r'^(?:/|.json|.xml)?$', 'demux'),
57
    (r'^/detail(?:.json|.xml)?$', 'list_servers', {'detail': True}),
58
    (r'^/(\d+)(?:.json|.xml)?$', 'server_demux'),
59
    (r'^/(\d+)/action(?:.json|.xml)?$', 'server_action'),
60
    (r'^/(\d+)/ips(?:.json|.xml)?$', 'list_addresses'),
61
    (r'^/(\d+)/ips/(.+?)(?:.json|.xml)?$', 'list_addresses_by_network'),
62
    (r'^/(\d+)/meta(?:.json|.xml)?$', 'metadata_demux'),
63
    (r'^/(\d+)/meta/(.+?)(?:.json|.xml)?$', 'metadata_item_demux'),
64
    (r'^/(\d+)/stats(?:.json|.xml)?$', 'server_stats'),
65
    (r'^/(\d+)/diagnostics(?:.json)?$', 'get_server_diagnostics'),
66
)
67

    
68

    
69
def demux(request):
70
    if request.method == 'GET':
71
        return list_servers(request)
72
    elif request.method == 'POST':
73
        return create_server(request)
74
    else:
75
        return method_not_allowed(request)
76

    
77

    
78
def server_demux(request, server_id):
79
    if request.method == 'GET':
80
        return get_server_details(request, server_id)
81
    elif request.method == 'PUT':
82
        return update_server_name(request, server_id)
83
    elif request.method == 'DELETE':
84
        return delete_server(request, server_id)
85
    else:
86
        return method_not_allowed(request)
87

    
88

    
89
def metadata_demux(request, server_id):
90
    if request.method == 'GET':
91
        return list_metadata(request, server_id)
92
    elif request.method == 'POST':
93
        return update_metadata(request, server_id)
94
    else:
95
        return method_not_allowed(request)
96

    
97

    
98
def metadata_item_demux(request, server_id, key):
99
    if request.method == 'GET':
100
        return get_metadata_item(request, server_id, key)
101
    elif request.method == 'PUT':
102
        return create_metadata_item(request, server_id, key)
103
    elif request.method == 'DELETE':
104
        return delete_metadata_item(request, server_id, key)
105
    else:
106
        return method_not_allowed(request)
107

    
108

    
109
def nic_to_dict(nic):
110
    d = {'id': util.construct_nic_id(nic),
111
         'network_id': str(nic.network.id),
112
         'mac_address': nic.mac,
113
         'ipv4': nic.ipv4 if nic.ipv4 else None,
114
         'ipv6': nic.ipv6 if nic.ipv6 else None}
115

    
116
    if nic.firewall_profile:
117
        d['firewallProfile'] = nic.firewall_profile
118
    return d
119

    
120

    
121
def vm_to_dict(vm, detail=False):
122
    d = dict(id=vm.id, name=vm.name)
123
    if detail:
124
        d['status'] = get_rsapi_state(vm)
125
        d['progress'] = 100 if get_rsapi_state(vm) == 'ACTIVE' \
126
                        else vm.buildpercentage
127
        d['hostId'] = vm.hostid
128
        d['updated'] = util.isoformat(vm.updated)
129
        d['created'] = util.isoformat(vm.created)
130
        d['flavorRef'] = vm.flavor.id
131
        d['imageRef'] = vm.imageid
132
        d['suspended'] = vm.suspended
133

    
134
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
135
        if metadata:
136
            d['metadata'] = {'values': metadata}
137

    
138
        attachments = [nic_to_dict(nic) for nic in vm.nics.order_by('index')]
139
        if attachments:
140
            d['attachments'] = {'values': attachments}
141

    
142
        # include the latest vm diagnostic, if set
143
        diagnostic = vm.get_last_diagnostic()
144
        if diagnostic:
145
            d['diagnostics'] = diagnostics_to_dict([diagnostic])
146

    
147
    return d
148

    
149

    
150
def diagnostics_to_dict(diagnostics):
151
    """
152
    Extract api data from diagnostics QuerySet.
153
    """
154
    entries = list()
155

    
156
    for diagnostic in diagnostics:
157
        # format source date if set
158
        formatted_source_date = None
159
        if diagnostic.source_date:
160
            formatted_source_date = util.isoformat(diagnostic.source_date)
161

    
162
        entry = {
163
            'source': diagnostic.source,
164
            'created': util.isoformat(diagnostic.created),
165
            'message': diagnostic.message,
166
            'details': diagnostic.details,
167
            'level': diagnostic.level,
168
        }
169

    
170
        if formatted_source_date:
171
            entry['source_date'] = formatted_source_date
172

    
173
        entries.append(entry)
174

    
175
    return entries
176

    
177

    
178
def render_server(request, server, status=200):
179
    if request.serialization == 'xml':
180
        data = render_to_string('server.xml', {
181
            'server': server,
182
            'is_root': True})
183
    else:
184
        data = json.dumps({'server': server})
185
    return HttpResponse(data, status=status)
186

    
187

    
188
def render_diagnostics(request, diagnostics_dict, status=200):
189
    """
190
    Render diagnostics dictionary to json response.
191
    """
192
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
193

    
194

    
195
@util.api_method('GET')
196
def get_server_diagnostics(request, server_id):
197
    """
198
    Virtual machine diagnostics api view.
199
    """
200
    log.debug('server_diagnostics %s', server_id)
201
    vm = util.get_vm(server_id, request.user_uniq)
202
    diagnostics = diagnostics_to_dict(vm.diagnostics.all())
203
    return render_diagnostics(request, diagnostics)
204

    
205

    
206
@util.api_method('GET')
207
def list_servers(request, detail=False):
208
    # Normal Response Codes: 200, 203
209
    # Error Response Codes: computeFault (400, 500),
210
    #                       serviceUnavailable (503),
211
    #                       unauthorized (401),
212
    #                       badRequest (400),
213
    #                       overLimit (413)
214

    
215
    log.debug('list_servers detail=%s', detail)
216
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
217

    
218
    since = util.isoparse(request.GET.get('changes-since'))
219

    
220
    if since:
221
        user_vms = user_vms.filter(updated__gte=since)
222
        if not user_vms:
223
            return HttpResponse(status=304)
224
    else:
225
        user_vms = user_vms.filter(deleted=False)
226

    
227
    servers = [vm_to_dict(server, detail)\
228
               for server in user_vms.order_by('id')]
229

    
230
    if request.serialization == 'xml':
231
        data = render_to_string('list_servers.xml', {
232
            'servers': servers,
233
            'detail': detail})
234
    else:
235
        data = json.dumps({'servers': {'values': servers}})
236

    
237
    return HttpResponse(data, status=200)
238

    
239

    
240
@util.api_method('POST')
241
# Use manual transactions. Backend and IP pool allocations need exclusive
242
# access (SELECT..FOR UPDATE). Running create_server with commit_on_success
243
# would result in backends and public networks to be locked until the job is
244
# sent to the Ganeti backend.
245
@quotas.uses_commission
246
@transaction.commit_manually
247
def create_server(serials, request):
248
    # Normal Response Code: 202
249
    # Error Response Codes: computeFault (400, 500),
250
    #                       serviceUnavailable (503),
251
    #                       unauthorized (401),
252
    #                       badMediaType(415),
253
    #                       itemNotFound (404),
254
    #                       badRequest (400),
255
    #                       serverCapacityUnavailable (503),
256
    #                       overLimit (413)
257
    try:
258
        req = util.get_request_dict(request)
259
        log.info('create_server %s', req)
260
        user_id = request.user_uniq
261

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

    
274
        if len(personality) > settings.MAX_PERSONALITY:
275
            raise faults.OverLimit("Maximum number of personalities exceeded")
276

    
277
        util.verify_personality(personality)
278
        image = util.get_image_dict(image_id, user_id)
279
        flavor = util.get_flavor(flavor_id)
280
        password = util.random_password()
281

    
282
        backend_allocator = BackendAllocator()
283
        backend = backend_allocator.allocate(request.user_uniq, flavor)
284

    
285
        if backend is None:
286
            log.error("No available backends for VM with flavor %s", flavor)
287
            raise faults.ServiceUnavailable("No available backends")
288
    except:
289
        transaction.rollback()
290
        raise
291
    else:
292
        transaction.commit()
293

    
294
    try:
295
        (network, address) = util.get_public_ip(backend)
296
        nic = {'ip': address, 'network': network.backend_id}
297
    except:
298
        transaction.rollback()
299
        raise
300
    else:
301
        transaction.commit()
302

    
303

    
304
    try:
305
        # Issue commission
306
        serial = quotas.issue_vm_commission(user_id, flavor)
307
        serials.append(serial)
308
        # Make the commission accepted, since in the end of this
309
        # transaction the VM will have been created in the DB.
310
        serial.accepted = True
311
        serial.save()
312

    
313
        # We must save the VM instance now, so that it gets a valid
314
        # vm.backend_vm_id.
315
        vm = VirtualMachine.objects.create(
316
            name=name,
317
            backend=backend,
318
            userid=user_id,
319
            imageid=image_id,
320
            flavor=flavor,
321
            action="CREATE",
322
            serial=serial)
323

    
324
        try:
325
            jobID = create_instance(vm, nic, flavor, image, password,
326
                                    personality)
327
        except GanetiApiError:
328
            vm.delete()
329
            raise
330

    
331
        log.info("User %s created VM %s, NIC %s, Backend %s, JobID %s",
332
                user_id, vm, nic, backend, str(jobID))
333

    
334
        vm.backendjobid = jobID
335
        vm.save()
336

    
337
        for key, val in metadata.items():
338
            VirtualMachineMetadata.objects.create(
339
                meta_key=key,
340
                meta_value=val,
341
                vm=vm)
342

    
343
        server = vm_to_dict(vm, detail=True)
344
        server['status'] = 'BUILD'
345
        server['adminPass'] = password
346

    
347
        respsone = render_server(request, server, status=202)
348
    except:
349
        transaction.rollback()
350
        raise
351
    else:
352
        transaction.commit()
353

    
354
    return respsone
355

    
356

    
357
@util.api_method('GET')
358
def get_server_details(request, server_id):
359
    # Normal Response Codes: 200, 203
360
    # Error Response Codes: computeFault (400, 500),
361
    #                       serviceUnavailable (503),
362
    #                       unauthorized (401),
363
    #                       badRequest (400),
364
    #                       itemNotFound (404),
365
    #                       overLimit (413)
366

    
367
    log.debug('get_server_details %s', server_id)
368
    vm = util.get_vm(server_id, request.user_uniq)
369
    server = vm_to_dict(vm, detail=True)
370
    return render_server(request, server)
371

    
372

    
373
@util.api_method('PUT')
374
def update_server_name(request, server_id):
375
    # Normal Response Code: 204
376
    # Error Response Codes: computeFault (400, 500),
377
    #                       serviceUnavailable (503),
378
    #                       unauthorized (401),
379
    #                       badRequest (400),
380
    #                       badMediaType(415),
381
    #                       itemNotFound (404),
382
    #                       buildInProgress (409),
383
    #                       overLimit (413)
384

    
385
    req = util.get_request_dict(request)
386
    log.info('update_server_name %s %s', server_id, req)
387

    
388
    try:
389
        name = req['server']['name']
390
    except (TypeError, KeyError):
391
        raise faults.BadRequest("Malformed request")
392

    
393
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
394
                     non_suspended=True)
395
    vm.name = name
396
    vm.save()
397

    
398
    return HttpResponse(status=204)
399

    
400

    
401
@util.api_method('DELETE')
402
@transaction.commit_on_success
403
def delete_server(request, server_id):
404
    # Normal Response Codes: 204
405
    # Error Response Codes: computeFault (400, 500),
406
    #                       serviceUnavailable (503),
407
    #                       unauthorized (401),
408
    #                       itemNotFound (404),
409
    #                       unauthorized (401),
410
    #                       buildInProgress (409),
411
    #                       overLimit (413)
412

    
413
    log.info('delete_server %s', server_id)
414
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
415
                     non_suspended=True)
416
    start_action(vm, 'DESTROY')
417
    delete_instance(vm)
418
    return HttpResponse(status=204)
419

    
420

    
421
@util.api_method('POST')
422
def server_action(request, server_id):
423
    req = util.get_request_dict(request)
424
    log.debug('server_action %s %s', server_id, req)
425

    
426
    if len(req) != 1:
427
        raise faults.BadRequest("Malformed request")
428

    
429
    # Do not allow any action on deleted or suspended VMs
430
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
431
                     non_deleted=True, non_suspended=True)
432

    
433
    try:
434
        key = req.keys()[0]
435
        if key != 'console':
436
            start_action(vm, key_to_action(key))
437
        val = req[key]
438
        assert isinstance(val, dict)
439
        return server_actions[key](request, vm, val)
440
    except KeyError:
441
        raise faults.BadRequest("Unknown action")
442
    except AssertionError:
443
        raise faults.BadRequest("Invalid argument")
444

    
445

    
446
def key_to_action(key):
447
    """Map HTTP request key to a VM Action"""
448
    if key == "shutdown":
449
        return "STOP"
450
    if key == "delete":
451
        return "DESTROY"
452
    if key == "console":
453
        return None
454
    else:
455
        return key.upper()
456

    
457

    
458
def start_action(vm, action):
459
    log.debug("Applying action %s to VM %s", action, vm)
460
    if not action:
461
        return
462

    
463
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
464
        raise faults.ServiceUnavailable("Action %s not supported" % action)
465

    
466
    # No actions to deleted VMs
467
    if vm.deleted:
468
        raise VirtualMachine.DeletedError
469

    
470
    # No actions to machines being built. They may be destroyed, however.
471
    if vm.operstate == 'BUILD' and action != 'DESTROY':
472
        raise VirtualMachine.BuildingError
473

    
474
    vm.action = action
475
    vm.backendjobid = None
476
    vm.backendopcode = None
477
    vm.backendjobstatus = None
478
    vm.backendlogmsg = None
479

    
480
    vm.save()
481

    
482

    
483
@util.api_method('GET')
484
def list_addresses(request, server_id):
485
    # Normal Response Codes: 200, 203
486
    # Error Response Codes: computeFault (400, 500),
487
    #                       serviceUnavailable (503),
488
    #                       unauthorized (401),
489
    #                       badRequest (400),
490
    #                       overLimit (413)
491

    
492
    log.debug('list_addresses %s', server_id)
493
    vm = util.get_vm(server_id, request.user_uniq)
494
    addresses = [nic_to_dict(nic) for nic in vm.nics.all()]
495

    
496
    if request.serialization == 'xml':
497
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
498
    else:
499
        data = json.dumps({'addresses': {'values': addresses}})
500

    
501
    return HttpResponse(data, status=200)
502

    
503

    
504
@util.api_method('GET')
505
def list_addresses_by_network(request, server_id, network_id):
506
    # Normal Response Codes: 200, 203
507
    # Error Response Codes: computeFault (400, 500),
508
    #                       serviceUnavailable (503),
509
    #                       unauthorized (401),
510
    #                       badRequest (400),
511
    #                       itemNotFound (404),
512
    #                       overLimit (413)
513

    
514
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
515
    machine = util.get_vm(server_id, request.user_uniq)
516
    network = util.get_network(network_id, request.user_uniq)
517
    nic = util.get_nic(machine, network)
518
    address = nic_to_dict(nic)
519

    
520
    if request.serialization == 'xml':
521
        data = render_to_string('address.xml', {'address': address})
522
    else:
523
        data = json.dumps({'network': address})
524

    
525
    return HttpResponse(data, status=200)
526

    
527

    
528
@util.api_method('GET')
529
def list_metadata(request, server_id):
530
    # Normal Response Codes: 200, 203
531
    # Error Response Codes: computeFault (400, 500),
532
    #                       serviceUnavailable (503),
533
    #                       unauthorized (401),
534
    #                       badRequest (400),
535
    #                       overLimit (413)
536

    
537
    log.debug('list_server_metadata %s', server_id)
538
    vm = util.get_vm(server_id, request.user_uniq)
539
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
540
    return util.render_metadata(request, metadata, use_values=True, status=200)
541

    
542

    
543
@util.api_method('POST')
544
def update_metadata(request, server_id):
545
    # Normal Response Code: 201
546
    # Error Response Codes: computeFault (400, 500),
547
    #                       serviceUnavailable (503),
548
    #                       unauthorized (401),
549
    #                       badRequest (400),
550
    #                       buildInProgress (409),
551
    #                       badMediaType(415),
552
    #                       overLimit (413)
553

    
554
    req = util.get_request_dict(request)
555
    log.info('update_server_metadata %s %s', server_id, req)
556
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
557
    try:
558
        metadata = req['metadata']
559
        assert isinstance(metadata, dict)
560
    except (KeyError, AssertionError):
561
        raise faults.BadRequest("Malformed request")
562

    
563
    for key, val in metadata.items():
564
        meta, created = vm.metadata.get_or_create(meta_key=key)
565
        meta.meta_value = val
566
        meta.save()
567

    
568
    vm.save()
569
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
570
    return util.render_metadata(request, vm_meta, status=201)
571

    
572

    
573
@util.api_method('GET')
574
def get_metadata_item(request, server_id, key):
575
    # Normal Response Codes: 200, 203
576
    # Error Response Codes: computeFault (400, 500),
577
    #                       serviceUnavailable (503),
578
    #                       unauthorized (401),
579
    #                       itemNotFound (404),
580
    #                       badRequest (400),
581
    #                       overLimit (413)
582

    
583
    log.debug('get_server_metadata_item %s %s', server_id, key)
584
    vm = util.get_vm(server_id, request.user_uniq)
585
    meta = util.get_vm_meta(vm, key)
586
    d = {meta.meta_key: meta.meta_value}
587
    return util.render_meta(request, d, status=200)
588

    
589

    
590
@util.api_method('PUT')
591
@transaction.commit_on_success
592
def create_metadata_item(request, server_id, key):
593
    # Normal Response Code: 201
594
    # Error Response Codes: computeFault (400, 500),
595
    #                       serviceUnavailable (503),
596
    #                       unauthorized (401),
597
    #                       itemNotFound (404),
598
    #                       badRequest (400),
599
    #                       buildInProgress (409),
600
    #                       badMediaType(415),
601
    #                       overLimit (413)
602

    
603
    req = util.get_request_dict(request)
604
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
605
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
606
    try:
607
        metadict = req['meta']
608
        assert isinstance(metadict, dict)
609
        assert len(metadict) == 1
610
        assert key in metadict
611
    except (KeyError, AssertionError):
612
        raise faults.BadRequest("Malformed request")
613

    
614
    meta, created = VirtualMachineMetadata.objects.get_or_create(
615
        meta_key=key,
616
        vm=vm)
617

    
618
    meta.meta_value = metadict[key]
619
    meta.save()
620
    vm.save()
621
    d = {meta.meta_key: meta.meta_value}
622
    return util.render_meta(request, d, status=201)
623

    
624

    
625
@util.api_method('DELETE')
626
@transaction.commit_on_success
627
def delete_metadata_item(request, server_id, key):
628
    # Normal Response Code: 204
629
    # Error Response Codes: computeFault (400, 500),
630
    #                       serviceUnavailable (503),
631
    #                       unauthorized (401),
632
    #                       itemNotFound (404),
633
    #                       badRequest (400),
634
    #                       buildInProgress (409),
635
    #                       badMediaType(415),
636
    #                       overLimit (413),
637

    
638
    log.info('delete_server_metadata_item %s %s', server_id, key)
639
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
640
    meta = util.get_vm_meta(vm, key)
641
    meta.delete()
642
    vm.save()
643
    return HttpResponse(status=204)
644

    
645

    
646
@util.api_method('GET')
647
def server_stats(request, server_id):
648
    # Normal Response Codes: 200
649
    # Error Response Codes: computeFault (400, 500),
650
    #                       serviceUnavailable (503),
651
    #                       unauthorized (401),
652
    #                       badRequest (400),
653
    #                       itemNotFound (404),
654
    #                       overLimit (413)
655

    
656
    log.debug('server_stats %s', server_id)
657
    vm = util.get_vm(server_id, request.user_uniq)
658
    #secret = util.encrypt(vm.backend_vm_id)
659
    secret = vm.backend_vm_id      # XXX disable backend id encryption
660

    
661
    stats = {
662
        'serverRef': vm.id,
663
        'refresh': settings.STATS_REFRESH_PERIOD,
664
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
665
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
666
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
667
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
668

    
669
    if request.serialization == 'xml':
670
        data = render_to_string('server_stats.xml', stats)
671
    else:
672
        data = json.dumps({'stats': stats})
673

    
674
    return HttpResponse(data, status=200)