Statistics
| Branch: | Tag: | Revision:

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

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

    
50

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

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

    
67

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

    
76

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

    
87

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

    
96

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

    
107

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

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

    
119

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

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

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

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

    
146
    return d
147

    
148

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

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

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

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

    
172
        entries.append(entry)
173

    
174
    return entries
175

    
176

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

    
186

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

    
193

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

    
204

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

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

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

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

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

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

    
236
    return HttpResponse(data, status=200)
237

    
238

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

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

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

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

    
280
        count = VirtualMachine.objects.filter(userid=user_id,
281
                                              deleted=False).count()
282

    
283
        # get user limit
284
        vms_limit_for_user = \
285
            settings.VMS_USER_QUOTA.get(user_id,
286
                    settings.MAX_VMS_PER_USER)
287

    
288
        if count >= vms_limit_for_user:
289
            raise faults.OverLimit("Server count limit exceeded for your account.")
290

    
291
        backend_allocator = BackendAllocator()
292
        backend = backend_allocator.allocate(request.user_uniq, flavor)
293

    
294
        if backend is None:
295
            log.error("No available backends for VM with flavor %s", flavor)
296
            raise faults.ServiceUnavailable("No available backends")
297
    except:
298
        transaction.rollback()
299
        raise
300
    else:
301
        transaction.commit()
302

    
303
    try:
304
        (network, address) = util.get_public_ip(backend)
305
        nic = {'ip': address, 'network': network.backend_id}
306
    except:
307
        transaction.rollback()
308
    else:
309
        transaction.commit()
310

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

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

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

    
332
        vm.backendjobid = jobID
333
        vm.save()
334

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

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

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

    
352
    return respsone
353

    
354

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

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

    
370

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

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

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

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

    
396
    return HttpResponse(status=204)
397

    
398

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

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

    
418

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

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

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

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

    
443

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

    
455

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

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

    
464
    # No actions to deleted VMs
465
    if vm.deleted:
466
        raise VirtualMachine.DeletedError
467

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

    
472
    vm.action = action
473
    vm.backendjobid = None
474
    vm.backendopcode = None
475
    vm.backendjobstatus = None
476
    vm.backendlogmsg = None
477

    
478
    vm.save()
479

    
480

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

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

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

    
499
    return HttpResponse(data, status=200)
500

    
501

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

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

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

    
523
    return HttpResponse(data, status=200)
524

    
525

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

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

    
540

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

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

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

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

    
570

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

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

    
587

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

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

    
612
    meta, created = VirtualMachineMetadata.objects.get_or_create(
613
        meta_key=key,
614
        vm=vm)
615

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

    
622

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

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

    
643

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

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

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

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

    
672
    return HttpResponse(data, status=200)