Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (29.9 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.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 snf_django.lib import api
42
from snf_django.lib.api import faults, utils
43

    
44
from synnefo.api import util
45
from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata)
46
from synnefo.logic import servers, utils as logic_utils
47

    
48
from logging import getLogger
49
log = getLogger(__name__)
50

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

    
65

    
66
def demux(request):
67
    if request.method == 'GET':
68
        return list_servers(request)
69
    elif request.method == 'POST':
70
        return create_server(request)
71
    else:
72
        return api.api_method_not_allowed(request)
73

    
74

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

    
85

    
86
def metadata_demux(request, server_id):
87
    if request.method == 'GET':
88
        return list_metadata(request, server_id)
89
    elif request.method == 'POST':
90
        return update_metadata(request, server_id)
91
    else:
92
        return api.api_method_not_allowed(request)
93

    
94

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

    
105

    
106
def nic_to_dict(nic):
107
    ip_type = "floating" if nic.is_floating_ip else "fixed"
108
    d = {'id': util.construct_nic_id(nic),
109
         'network_id': str(nic.network.id),
110
         'mac_address': nic.mac,
111
         'ipv4': nic.ipv4,
112
         'ipv6': nic.ipv6,
113
         'OS-EXT-IPS:type': ip_type}
114

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

    
119

    
120
def attachments_to_addresses(attachments):
121
    addresses = {}
122
    for nic in attachments:
123
        net_nics = []
124
        net_nics.append({"version": 4,
125
                         "addr": nic["ipv4"],
126
                         "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
127
        if nic["ipv6"]:
128
            net_nics.append({"version": 6,
129
                             "addr": nic["ipv6"],
130
                             "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
131
        addresses[nic["network_id"]] = net_nics
132
    return addresses
133

    
134

    
135
def vm_to_dict(vm, detail=False):
136
    d = dict(id=vm.id, name=vm.name)
137
    d['links'] = util.vm_to_links(vm.id)
138
    if detail:
139
        d['user_id'] = vm.userid
140
        d['tenant_id'] = vm.userid
141
        d['status'] = logic_utils.get_rsapi_state(vm)
142
        d['SNF:task_state'] = logic_utils.get_task_state(vm)
143
        d['progress'] = 100 if d['status'] == 'ACTIVE' else vm.buildpercentage
144
        d['hostId'] = vm.hostid
145
        d['updated'] = utils.isoformat(vm.updated)
146
        d['created'] = utils.isoformat(vm.created)
147
        d['flavor'] = {"id": vm.flavor.id,
148
                       "links": util.flavor_to_links(vm.flavor.id)}
149
        d['image'] = {"id": vm.imageid,
150
                      "links": util.image_to_links(vm.imageid)}
151
        d['suspended'] = vm.suspended
152

    
153
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
154
        d['metadata'] = metadata
155

    
156
        vm_nics = vm.nics.filter(state="ACTIVE").order_by("index")
157
        attachments = map(nic_to_dict, vm_nics)
158
        d['attachments'] = attachments
159
        d['addresses'] = attachments_to_addresses(attachments)
160

    
161
        # include the latest vm diagnostic, if set
162
        diagnostic = vm.get_last_diagnostic()
163
        if diagnostic:
164
            d['diagnostics'] = diagnostics_to_dict([diagnostic])
165
        else:
166
            d['diagnostics'] = []
167
        # Fixed
168
        d["security_groups"] = [{"name": "default"}]
169
        d["key_name"] = None
170
        d["config_drive"] = ""
171
        d["accessIPv4"] = ""
172
        d["accessIPv6"] = ""
173
        d["SNF:fqdn"] = get_server_fqdn(vm)
174

    
175
    return d
176

    
177

    
178
def get_server_fqdn(vm):
179
    fqdn_setting = settings.CYCLADES_SERVERS_FQDN
180
    if fqdn_setting is None:
181
        public_nics = vm.nics.filter(network__public=True, state="ACTIVE")
182
        # Return the first public IPv4 address if exists
183
        ipv4_nics = public_nics.exclude(ipv4=None)
184
        if ipv4_nics:
185
            return ipv4_nics[0].ipv4
186
        # Else return the first public IPv6 address if exists
187
        ipv6_nics = public_nics.exclude(ipv6=None)
188
        if ipv6_nics:
189
            return ipv6_nics[0].ipv6
190
        return ""
191
    elif isinstance(fqdn_setting, basestring):
192
        return fqdn_setting % {"id": vm.id}
193
    else:
194
        msg = ("Invalid setting: CYCLADES_SERVERS_FQDN."
195
               " Value must be a string.")
196
        raise faults.InternalServerError(msg)
197

    
198

    
199
def diagnostics_to_dict(diagnostics):
200
    """
201
    Extract api data from diagnostics QuerySet.
202
    """
203
    entries = list()
204

    
205
    for diagnostic in diagnostics:
206
        # format source date if set
207
        formatted_source_date = None
208
        if diagnostic.source_date:
209
            formatted_source_date = utils.isoformat(diagnostic.source_date)
210

    
211
        entry = {
212
            'source': diagnostic.source,
213
            'created': utils.isoformat(diagnostic.created),
214
            'message': diagnostic.message,
215
            'details': diagnostic.details,
216
            'level': diagnostic.level,
217
        }
218

    
219
        if formatted_source_date:
220
            entry['source_date'] = formatted_source_date
221

    
222
        entries.append(entry)
223

    
224
    return entries
225

    
226

    
227
def render_server(request, server, status=200):
228
    if request.serialization == 'xml':
229
        data = render_to_string('server.xml', {
230
            'server': server,
231
            'is_root': True})
232
    else:
233
        data = json.dumps({'server': server})
234
    return HttpResponse(data, status=status)
235

    
236

    
237
def render_diagnostics(request, diagnostics_dict, status=200):
238
    """
239
    Render diagnostics dictionary to json response.
240
    """
241
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
242

    
243

    
244
@api.api_method(http_method='GET', user_required=True, logger=log)
245
def get_server_diagnostics(request, server_id):
246
    """
247
    Virtual machine diagnostics api view.
248
    """
249
    log.debug('server_diagnostics %s', server_id)
250
    vm = util.get_vm(server_id, request.user_uniq)
251
    diagnostics = diagnostics_to_dict(vm.diagnostics.all())
252
    return render_diagnostics(request, diagnostics)
253

    
254

    
255
@api.api_method(http_method='GET', user_required=True, logger=log)
256
def list_servers(request, detail=False):
257
    # Normal Response Codes: 200, 203
258
    # Error Response Codes: computeFault (400, 500),
259
    #                       serviceUnavailable (503),
260
    #                       unauthorized (401),
261
    #                       badRequest (400),
262
    #                       overLimit (413)
263

    
264
    log.debug('list_servers detail=%s', detail)
265
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
266

    
267
    user_vms = utils.filter_modified_since(request, objects=user_vms)
268

    
269
    servers_dict = [vm_to_dict(server, detail)
270
                    for server in user_vms.order_by('id')]
271

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

    
279
    return HttpResponse(data, status=200)
280

    
281

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

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

    
313
    # Verify that personalities are well-formed
314
    util.verify_personality(personality)
315
    # Get image information
316
    image = util.get_image_dict(image_id, user_id)
317
    # Get flavor (ensure it is active)
318
    flavor = util.get_flavor(flavor_id, include_deleted=False)
319
    # Generate password
320
    password = util.random_password()
321

    
322
    vm = servers.create(user_id, name, password, flavor, image,
323
                        metadata=metadata, personality=personality,
324
                        private_networks=private_networks,
325
                        floating_ips=floating_ips)
326

    
327
    server = vm_to_dict(vm, detail=True)
328
    server['status'] = 'BUILD'
329
    server['adminPass'] = password
330

    
331
    response = render_server(request, server, status=202)
332

    
333
    return response
334

    
335

    
336
@api.api_method(http_method='GET', user_required=True, logger=log)
337
def get_server_details(request, server_id):
338
    # Normal Response Codes: 200, 203
339
    # Error Response Codes: computeFault (400, 500),
340
    #                       serviceUnavailable (503),
341
    #                       unauthorized (401),
342
    #                       badRequest (400),
343
    #                       itemNotFound (404),
344
    #                       overLimit (413)
345

    
346
    log.debug('get_server_details %s', server_id)
347
    vm = util.get_vm(server_id, request.user_uniq)
348
    server = vm_to_dict(vm, detail=True)
349
    return render_server(request, server)
350

    
351

    
352
@api.api_method(http_method='PUT', user_required=True, logger=log)
353
def update_server_name(request, server_id):
354
    # Normal Response Code: 204
355
    # Error Response Codes: computeFault (400, 500),
356
    #                       serviceUnavailable (503),
357
    #                       unauthorized (401),
358
    #                       badRequest (400),
359
    #                       badMediaType(415),
360
    #                       itemNotFound (404),
361
    #                       buildInProgress (409),
362
    #                       overLimit (413)
363

    
364
    req = utils.get_request_dict(request)
365
    log.info('update_server_name %s %s', server_id, req)
366

    
367
    try:
368
        name = req['server']['name']
369
    except (TypeError, KeyError):
370
        raise faults.BadRequest("Malformed request")
371

    
372
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
373
                     non_suspended=True)
374
    vm.name = name
375
    vm.save()
376

    
377
    return HttpResponse(status=204)
378

    
379

    
380
@api.api_method(http_method='DELETE', user_required=True, logger=log)
381
def delete_server(request, server_id):
382
    # Normal Response Codes: 204
383
    # Error Response Codes: computeFault (400, 500),
384
    #                       serviceUnavailable (503),
385
    #                       unauthorized (401),
386
    #                       itemNotFound (404),
387
    #                       unauthorized (401),
388
    #                       buildInProgress (409),
389
    #                       overLimit (413)
390

    
391
    log.info('delete_server %s', server_id)
392
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
393
                     non_suspended=True)
394
    vm = servers.destroy(vm)
395
    return HttpResponse(status=204)
396

    
397

    
398
# additional server actions
399
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
400

    
401

    
402
def key_to_action(key):
403
    """Map HTTP request key to a VM Action"""
404
    if key == "shutdown":
405
        return "STOP"
406
    if key == "delete":
407
        return "DESTROY"
408
    if key in ARBITRARY_ACTIONS:
409
        return None
410
    else:
411
        return key.upper()
412

    
413

    
414
@api.api_method(http_method='POST', user_required=True, logger=log)
415
@transaction.commit_on_success
416
def demux_server_action(request, server_id):
417
    req = utils.get_request_dict(request)
418
    log.debug('server_action %s %s', server_id, req)
419

    
420
    if len(req) != 1:
421
        raise faults.BadRequest("Malformed request")
422

    
423
    # Do not allow any action on deleted or suspended VMs
424
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
425
                     non_deleted=True, non_suspended=True)
426

    
427
    action = req.keys()[0]
428

    
429
    if key_to_action(action) not in [x[0] for x in VirtualMachine.ACTIONS]:
430
        if action not in ARBITRARY_ACTIONS:
431
            raise faults.BadRequest("Action %s not supported" % action)
432
    action_args = req[action]
433

    
434
    if not isinstance(action_args, dict):
435
        raise faults.BadRequest("Invalid argument")
436

    
437
    return server_actions[action](request, vm, action_args)
438

    
439

    
440
@api.api_method(http_method='GET', user_required=True, logger=log)
441
def list_addresses(request, server_id):
442
    # Normal Response Codes: 200, 203
443
    # Error Response Codes: computeFault (400, 500),
444
    #                       serviceUnavailable (503),
445
    #                       unauthorized (401),
446
    #                       badRequest (400),
447
    #                       overLimit (413)
448

    
449
    log.debug('list_addresses %s', server_id)
450
    vm = util.get_vm(server_id, request.user_uniq)
451
    attachments = [nic_to_dict(nic) for nic in vm.nics.all()]
452
    addresses = attachments_to_addresses(attachments)
453

    
454
    if request.serialization == 'xml':
455
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
456
    else:
457
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
458

    
459
    return HttpResponse(data, status=200)
460

    
461

    
462
@api.api_method(http_method='GET', user_required=True, logger=log)
463
def list_addresses_by_network(request, server_id, network_id):
464
    # Normal Response Codes: 200, 203
465
    # Error Response Codes: computeFault (400, 500),
466
    #                       serviceUnavailable (503),
467
    #                       unauthorized (401),
468
    #                       badRequest (400),
469
    #                       itemNotFound (404),
470
    #                       overLimit (413)
471

    
472
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
473
    machine = util.get_vm(server_id, request.user_uniq)
474
    network = util.get_network(network_id, request.user_uniq)
475
    nics = machine.nics.filter(network=network).all()
476
    addresses = attachments_to_addresses(map(nic_to_dict, nics))
477

    
478
    if request.serialization == 'xml':
479
        data = render_to_string('address.xml', {'addresses': addresses})
480
    else:
481
        data = json.dumps({'network': addresses})
482

    
483
    return HttpResponse(data, status=200)
484

    
485

    
486
@api.api_method(http_method='GET', user_required=True, logger=log)
487
def list_metadata(request, server_id):
488
    # Normal Response Codes: 200, 203
489
    # Error Response Codes: computeFault (400, 500),
490
    #                       serviceUnavailable (503),
491
    #                       unauthorized (401),
492
    #                       badRequest (400),
493
    #                       overLimit (413)
494

    
495
    log.debug('list_server_metadata %s', server_id)
496
    vm = util.get_vm(server_id, request.user_uniq)
497
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
498
    return util.render_metadata(request, metadata, use_values=False,
499
                                status=200)
500

    
501

    
502
@api.api_method(http_method='POST', user_required=True, logger=log)
503
def update_metadata(request, server_id):
504
    # Normal Response Code: 201
505
    # Error Response Codes: computeFault (400, 500),
506
    #                       serviceUnavailable (503),
507
    #                       unauthorized (401),
508
    #                       badRequest (400),
509
    #                       buildInProgress (409),
510
    #                       badMediaType(415),
511
    #                       overLimit (413)
512

    
513
    req = utils.get_request_dict(request)
514
    log.info('update_server_metadata %s %s', server_id, req)
515
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
516
    try:
517
        metadata = req['metadata']
518
        assert isinstance(metadata, dict)
519
    except (KeyError, AssertionError):
520
        raise faults.BadRequest("Malformed request")
521

    
522
    for key, val in metadata.items():
523
        meta, created = vm.metadata.get_or_create(meta_key=key)
524
        meta.meta_value = val
525
        meta.save()
526

    
527
    vm.save()
528
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
529
    return util.render_metadata(request, vm_meta, status=201)
530

    
531

    
532
@api.api_method(http_method='GET', user_required=True, logger=log)
533
def get_metadata_item(request, server_id, key):
534
    # Normal Response Codes: 200, 203
535
    # Error Response Codes: computeFault (400, 500),
536
    #                       serviceUnavailable (503),
537
    #                       unauthorized (401),
538
    #                       itemNotFound (404),
539
    #                       badRequest (400),
540
    #                       overLimit (413)
541

    
542
    log.debug('get_server_metadata_item %s %s', server_id, key)
543
    vm = util.get_vm(server_id, request.user_uniq)
544
    meta = util.get_vm_meta(vm, key)
545
    d = {meta.meta_key: meta.meta_value}
546
    return util.render_meta(request, d, status=200)
547

    
548

    
549
@api.api_method(http_method='PUT', user_required=True, logger=log)
550
@transaction.commit_on_success
551
def create_metadata_item(request, server_id, key):
552
    # Normal Response Code: 201
553
    # Error Response Codes: computeFault (400, 500),
554
    #                       serviceUnavailable (503),
555
    #                       unauthorized (401),
556
    #                       itemNotFound (404),
557
    #                       badRequest (400),
558
    #                       buildInProgress (409),
559
    #                       badMediaType(415),
560
    #                       overLimit (413)
561

    
562
    req = utils.get_request_dict(request)
563
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
564
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
565
    try:
566
        metadict = req['meta']
567
        assert isinstance(metadict, dict)
568
        assert len(metadict) == 1
569
        assert key in metadict
570
    except (KeyError, AssertionError):
571
        raise faults.BadRequest("Malformed request")
572

    
573
    meta, created = VirtualMachineMetadata.objects.get_or_create(
574
        meta_key=key,
575
        vm=vm)
576

    
577
    meta.meta_value = metadict[key]
578
    meta.save()
579
    vm.save()
580
    d = {meta.meta_key: meta.meta_value}
581
    return util.render_meta(request, d, status=201)
582

    
583

    
584
@api.api_method(http_method='DELETE', user_required=True, logger=log)
585
@transaction.commit_on_success
586
def delete_metadata_item(request, server_id, key):
587
    # Normal Response Code: 204
588
    # Error Response Codes: computeFault (400, 500),
589
    #                       serviceUnavailable (503),
590
    #                       unauthorized (401),
591
    #                       itemNotFound (404),
592
    #                       badRequest (400),
593
    #                       buildInProgress (409),
594
    #                       badMediaType(415),
595
    #                       overLimit (413),
596

    
597
    log.info('delete_server_metadata_item %s %s', server_id, key)
598
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
599
    meta = util.get_vm_meta(vm, key)
600
    meta.delete()
601
    vm.save()
602
    return HttpResponse(status=204)
603

    
604

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

    
615
    log.debug('server_stats %s', server_id)
616
    vm = util.get_vm(server_id, request.user_uniq)
617
    #secret = util.encrypt(vm.backend_vm_id)
618
    secret = vm.backend_vm_id      # XXX disable backend id encryption
619

    
620
    stats = {
621
        'serverRef': vm.id,
622
        'refresh': settings.STATS_REFRESH_PERIOD,
623
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
624
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
625
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
626
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
627

    
628
    if request.serialization == 'xml':
629
        data = render_to_string('server_stats.xml', stats)
630
    else:
631
        data = json.dumps({'stats': stats})
632

    
633
    return HttpResponse(data, status=200)
634

    
635

    
636
# ACTIONS
637

    
638

    
639
server_actions = {}
640
network_actions = {}
641

    
642

    
643
def server_action(name):
644
    '''Decorator for functions implementing server actions.
645
    `name` is the key in the dict passed by the client.
646
    '''
647

    
648
    def decorator(func):
649
        server_actions[name] = func
650
        return func
651
    return decorator
652

    
653

    
654
def network_action(name):
655
    '''Decorator for functions implementing network actions.
656
    `name` is the key in the dict passed by the client.
657
    '''
658

    
659
    def decorator(func):
660
        network_actions[name] = func
661
        return func
662
    return decorator
663

    
664

    
665
@server_action('start')
666
def start(request, vm, args):
667
    # Normal Response Code: 202
668
    # Error Response Codes: serviceUnavailable (503),
669
    #                       itemNotFound (404)
670
    vm = servers.start(vm)
671
    return HttpResponse(status=202)
672

    
673

    
674
@server_action('shutdown')
675
def shutdown(request, vm, args):
676
    # Normal Response Code: 202
677
    # Error Response Codes: serviceUnavailable (503),
678
    #                       itemNotFound (404)
679
    vm = servers.stop(vm)
680
    return HttpResponse(status=202)
681

    
682

    
683
@server_action('reboot')
684
def reboot(request, vm, args):
685
    # Normal Response Code: 202
686
    # Error Response Codes: computeFault (400, 500),
687
    #                       serviceUnavailable (503),
688
    #                       unauthorized (401),
689
    #                       badRequest (400),
690
    #                       badMediaType(415),
691
    #                       itemNotFound (404),
692
    #                       buildInProgress (409),
693
    #                       overLimit (413)
694

    
695
    reboot_type = args.get("type")
696
    if reboot_type is None:
697
        raise faults.BadRequest("Missing 'type' attribute.")
698
    elif reboot_type not in ["SOFT", "HARD"]:
699
        raise faults.BadRequest("Invalid 'type' attribute.")
700
    vm = servers.reboot(vm, reboot_type=reboot_type)
701
    return HttpResponse(status=202)
702

    
703

    
704
@server_action('firewallProfile')
705
def set_firewall_profile(request, vm, args):
706
    # Normal Response Code: 200
707
    # Error Response Codes: computeFault (400, 500),
708
    #                       serviceUnavailable (503),
709
    #                       unauthorized (401),
710
    #                       badRequest (400),
711
    #                       badMediaType(415),
712
    #                       itemNotFound (404),
713
    #                       buildInProgress (409),
714
    #                       overLimit (413)
715
    profile = args.get("profile")
716
    if profile is None:
717
        raise faults.BadRequest("Missing 'profile' attribute")
718
    index = args.get("index", 0)
719
    servers.set_firewall_profile(vm, profile=profile, index=index)
720
    return HttpResponse(status=202)
721

    
722

    
723
@server_action('resize')
724
def resize(request, vm, args):
725
    # Normal Response Code: 202
726
    # Error Response Codes: computeFault (400, 500),
727
    #                       serviceUnavailable (503),
728
    #                       unauthorized (401),
729
    #                       badRequest (400),
730
    #                       badMediaType(415),
731
    #                       itemNotFound (404),
732
    #                       buildInProgress (409),
733
    #                       serverCapacityUnavailable (503),
734
    #                       overLimit (413),
735
    #                       resizeNotAllowed (403)
736
    flavorRef = args.get("flavorRef")
737
    if flavorRef is None:
738
        raise faults.BadRequest("Missing 'flavorRef' attribute.")
739
    flavor = util.get_flavor(flavor_id=flavorRef, include_deleted=False)
740
    servers.resize(vm, flavor=flavor)
741
    return HttpResponse(status=202)
742

    
743

    
744
@server_action('console')
745
def get_console(request, vm, args):
746
    # Normal Response Code: 200
747
    # Error Response Codes: computeFault (400, 500),
748
    #                       serviceUnavailable (503),
749
    #                       unauthorized (401),
750
    #                       badRequest (400),
751
    #                       badMediaType(415),
752
    #                       itemNotFound (404),
753
    #                       buildInProgress (409),
754
    #                       overLimit (413)
755

    
756
    log.info("Get console  VM %s: %s", vm, args)
757

    
758
    console_type = args.get("type")
759
    if console_type is None:
760
        raise faults.BadRequest("No console 'type' specified.")
761
    elif console_type != "vnc":
762
        raise faults.BadRequest("Console 'type' can only be 'vnc'.")
763
    console_info = servers.console(vm, console_type)
764

    
765
    if request.serialization == 'xml':
766
        mimetype = 'application/xml'
767
        data = render_to_string('console.xml', {'console': console_info})
768
    else:
769
        mimetype = 'application/json'
770
        data = json.dumps({'console': console_info})
771

    
772
    return HttpResponse(data, mimetype=mimetype, status=200)
773

    
774

    
775
@server_action('changePassword')
776
def change_password(request, vm, args):
777
    raise faults.NotImplemented('Changing password is not supported.')
778

    
779

    
780
@server_action('rebuild')
781
def rebuild(request, vm, args):
782
    raise faults.NotImplemented('Rebuild not supported.')
783

    
784

    
785
@server_action('confirmResize')
786
def confirm_resize(request, vm, args):
787
    raise faults.NotImplemented('Resize not supported.')
788

    
789

    
790
@server_action('revertResize')
791
def revert_resize(request, vm, args):
792
    raise faults.NotImplemented('Resize not supported.')
793

    
794

    
795
@network_action('add')
796
@transaction.commit_on_success
797
def add(request, net, args):
798
    # Normal Response Code: 202
799
    # Error Response Codes: computeFault (400, 500),
800
    #                       serviceUnavailable (503),
801
    #                       unauthorized (401),
802
    #                       badRequest (400),
803
    #                       buildInProgress (409),
804
    #                       badMediaType(415),
805
    #                       itemNotFound (404),
806
    #                       overLimit (413)
807
    server_id = args.get('serverRef', None)
808
    if not server_id:
809
        raise faults.BadRequest('Malformed Request.')
810

    
811
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
812
    servers.connect(vm, network=net)
813
    return HttpResponse(status=202)
814

    
815

    
816
@network_action('remove')
817
@transaction.commit_on_success
818
def remove(request, net, args):
819
    # Normal Response Code: 202
820
    # Error Response Codes: computeFault (400, 500),
821
    #                       serviceUnavailable (503),
822
    #                       unauthorized (401),
823
    #                       badRequest (400),
824
    #                       badMediaType(415),
825
    #                       itemNotFound (404),
826
    #                       overLimit (413)
827

    
828
    attachment = args.get("attachment")
829
    if attachment is None:
830
        raise faults.BadRequest("Missing 'attachment' attribute.")
831
    try:
832
        # attachment string: nic-<vm-id>-<nic-index>
833
        _, server_id, nic_index = attachment.split("-", 2)
834
        server_id = int(server_id)
835
        nic_index = int(nic_index)
836
    except (ValueError, TypeError):
837
        raise faults.BadRequest("Invalid 'attachment' attribute.")
838

    
839
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
840
    servers.disconnect(vm, nic_index=nic_index)
841

    
842
    return HttpResponse(status=202)
843

    
844

    
845
@server_action("addFloatingIp")
846
def add_floating_ip(request, vm, args):
847
    address = args.get("address")
848
    if address is None:
849
        raise faults.BadRequest("Missing 'address' attribute")
850

    
851
    servers.add_floating_ip(vm, address)
852
    return HttpResponse(status=202)
853

    
854

    
855
@server_action("removeFloatingIp")
856
def remove_floating_ip(request, vm, args):
857
    address = args.get("address")
858
    if address is None:
859
        raise faults.BadRequest("Missing 'address' attribute")
860

    
861
    servers.remove_floating_ip(vm, address)
862
    return HttpResponse(status=202)