Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (32 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 import patterns
36

    
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

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

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

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

    
66

    
67
def demux(request):
68
    if request.method == 'GET':
69
        return list_servers(request)
70
    elif request.method == 'POST':
71
        return create_server(request)
72
    else:
73
        return api.api_method_not_allowed(request,
74
                                          allowed_methods=['GET', 'POST'])
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 api.api_method_not_allowed(request,
86
                                          allowed_methods=['GET',
87
                                                           'PUT',
88
                                                           'DELETE'])
89

    
90

    
91
def metadata_demux(request, server_id):
92
    if request.method == 'GET':
93
        return list_metadata(request, server_id)
94
    elif request.method == 'POST':
95
        return update_metadata(request, server_id)
96
    else:
97
        return api.api_method_not_allowed(request,
98
                                          allowed_methods=['GET', 'POST'])
99

    
100

    
101
def metadata_item_demux(request, server_id, key):
102
    if request.method == 'GET':
103
        return get_metadata_item(request, server_id, key)
104
    elif request.method == 'PUT':
105
        return create_metadata_item(request, server_id, key)
106
    elif request.method == 'DELETE':
107
        return delete_metadata_item(request, server_id, key)
108
    else:
109
        return api.api_method_not_allowed(request,
110
                                          allowed_methods=['GET',
111
                                                           'PUT',
112
                                                           'DELETE'])
113

    
114

    
115
def nic_to_dict(nic):
116
    ip_type = "floating" if nic.is_floating_ip else "fixed"
117
    d = {'id': util.construct_nic_id(nic),
118
         'network_id': str(nic.network.id),
119
         'mac_address': nic.mac,
120
         'ipv4': nic.ipv4,
121
         'ipv6': nic.ipv6,
122
         'OS-EXT-IPS:type': ip_type}
123

    
124
    if nic.firewall_profile:
125
        d['firewallProfile'] = nic.firewall_profile
126
    return d
127

    
128

    
129
def attachments_to_addresses(attachments):
130
    addresses = {}
131
    for nic in attachments:
132
        net_nics = []
133
        net_nics.append({"version": 4,
134
                         "addr": nic["ipv4"],
135
                         "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
136
        if nic["ipv6"]:
137
            net_nics.append({"version": 6,
138
                             "addr": nic["ipv6"],
139
                             "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
140
        addresses[nic["network_id"]] = net_nics
141
    return addresses
142

    
143

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

    
162
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
163
        d['metadata'] = metadata
164

    
165
        vm_nics = vm.nics.filter(state="ACTIVE").order_by("index")
166
        attachments = map(nic_to_dict, vm_nics)
167
        d['attachments'] = attachments
168
        d['addresses'] = attachments_to_addresses(attachments)
169

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

    
186
    return d
187

    
188

    
189
def get_server_fqdn(vm):
190
    fqdn_setting = settings.CYCLADES_SERVERS_FQDN
191
    if fqdn_setting is None:
192
        public_nics = vm.nics.filter(network__public=True, state="ACTIVE")
193
        # Return the first public IPv4 address if exists
194
        ipv4_nics = public_nics.exclude(ipv4=None)
195
        if ipv4_nics:
196
            return ipv4_nics[0].ipv4
197
        # Else return the first public IPv6 address if exists
198
        ipv6_nics = public_nics.exclude(ipv6=None)
199
        if ipv6_nics:
200
            return ipv6_nics[0].ipv6
201
        return ""
202
    elif isinstance(fqdn_setting, basestring):
203
        return fqdn_setting % {"id": vm.id}
204
    else:
205
        msg = ("Invalid setting: CYCLADES_SERVERS_FQDN."
206
               " Value must be a string.")
207
        raise faults.InternalServerError(msg)
208

    
209

    
210
def get_server_port_forwarding(vm, fqdn):
211
    """Create API 'port_forwarding' attribute from corresponding setting.
212

213
    Create the 'port_forwarding' API vm attribute based on the corresponding
214
    setting (CYCLADES_PORT_FORWARDING), which can be either a tuple
215
    of the form (host, port) or a callable object returning such tuple. In
216
    case of callable object, must be called with the following arguments:
217
    * ip_address
218
    * server_id
219
    * fqdn
220
    * owner UUID
221

222
    """
223
    port_forwarding = {}
224
    for dport, to_dest in settings.CYCLADES_PORT_FORWARDING.items():
225
        if hasattr(to_dest, "__call__"):
226
            public_nics = vm.nics.filter(network__public=True, state="ACTIVE")\
227
                                 .exclude(ipv4=None).order_by('index')
228
            if public_nics:
229
                vm_ipv4 = public_nics[0].ipv4
230
            else:
231
                vm_ipv4 = None
232
            to_dest = to_dest(vm_ipv4, vm.id, fqdn, vm.userid)
233
        msg = ("Invalid setting: CYCLADES_PORT_FOWARDING."
234
               " Value must be a tuple of two elements (host, port).")
235
        if to_dest is None:
236
            continue
237
        if not isinstance(to_dest, tuple) or len(to_dest) != 2:
238
                raise faults.InternalServerError(msg)
239
        else:
240
            try:
241
                host, port = to_dest
242
            except (TypeError, ValueError):
243
                raise faults.InternalServerError(msg)
244

    
245
        port_forwarding[dport] = {"host": host, "port": str(port)}
246
    return port_forwarding
247

    
248

    
249
def diagnostics_to_dict(diagnostics):
250
    """
251
    Extract api data from diagnostics QuerySet.
252
    """
253
    entries = list()
254

    
255
    for diagnostic in diagnostics:
256
        # format source date if set
257
        formatted_source_date = None
258
        if diagnostic.source_date:
259
            formatted_source_date = utils.isoformat(diagnostic.source_date)
260

    
261
        entry = {
262
            'source': diagnostic.source,
263
            'created': utils.isoformat(diagnostic.created),
264
            'message': diagnostic.message,
265
            'details': diagnostic.details,
266
            'level': diagnostic.level,
267
        }
268

    
269
        if formatted_source_date:
270
            entry['source_date'] = formatted_source_date
271

    
272
        entries.append(entry)
273

    
274
    return entries
275

    
276

    
277
def render_server(request, server, status=200):
278
    if request.serialization == 'xml':
279
        data = render_to_string('server.xml', {
280
            'server': server,
281
            'is_root': True})
282
    else:
283
        data = json.dumps({'server': server})
284
    return HttpResponse(data, status=status)
285

    
286

    
287
def render_diagnostics(request, diagnostics_dict, status=200):
288
    """
289
    Render diagnostics dictionary to json response.
290
    """
291
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
292

    
293

    
294
@api.api_method(http_method='GET', user_required=True, logger=log)
295
def get_server_diagnostics(request, server_id):
296
    """
297
    Virtual machine diagnostics api view.
298
    """
299
    log.debug('server_diagnostics %s', server_id)
300
    vm = util.get_vm(server_id, request.user_uniq)
301
    diagnostics = diagnostics_to_dict(vm.diagnostics.all())
302
    return render_diagnostics(request, diagnostics)
303

    
304

    
305
@api.api_method(http_method='GET', user_required=True, logger=log)
306
def list_servers(request, detail=False):
307
    # Normal Response Codes: 200, 203
308
    # Error Response Codes: computeFault (400, 500),
309
    #                       serviceUnavailable (503),
310
    #                       unauthorized (401),
311
    #                       badRequest (400),
312
    #                       overLimit (413)
313

    
314
    log.debug('list_servers detail=%s', detail)
315
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
316

    
317
    user_vms = utils.filter_modified_since(request, objects=user_vms)
318

    
319
    servers_dict = [vm_to_dict(server, detail)
320
                    for server in user_vms.order_by('id')]
321

    
322
    if request.serialization == 'xml':
323
        data = render_to_string('list_servers.xml', {
324
            'servers': servers_dict,
325
            'detail': detail})
326
    else:
327
        data = json.dumps({'servers': servers_dict})
328

    
329
    return HttpResponse(data, status=200)
330

    
331

    
332
@api.api_method(http_method='POST', user_required=True, logger=log)
333
def create_server(request):
334
    # Normal Response Code: 202
335
    # Error Response Codes: computeFault (400, 500),
336
    #                       serviceUnavailable (503),
337
    #                       unauthorized (401),
338
    #                       badMediaType(415),
339
    #                       itemNotFound (404),
340
    #                       badRequest (400),
341
    #                       serverCapacityUnavailable (503),
342
    #                       overLimit (413)
343
    req = utils.get_request_dict(request)
344
    log.info('create_server %s', req)
345
    user_id = request.user_uniq
346

    
347
    try:
348
        server = req['server']
349
        name = server['name']
350
        metadata = server.get('metadata', {})
351
        assert isinstance(metadata, dict)
352
        image_id = server['imageRef']
353
        flavor_id = server['flavorRef']
354
        personality = server.get('personality', [])
355
        assert isinstance(personality, list)
356
        private_networks = server.get("networks", [])
357
        assert isinstance(private_networks, list)
358
        floating_ips = server.get("floating_ips", [])
359
        assert isinstance(floating_ips, list)
360
    except (KeyError, AssertionError):
361
        raise faults.BadRequest("Malformed request")
362

    
363
    # Verify that personalities are well-formed
364
    util.verify_personality(personality)
365
    # Get image information
366
    image = util.get_image_dict(image_id, user_id)
367
    # Get flavor (ensure it is active)
368
    flavor = util.get_flavor(flavor_id, include_deleted=False)
369
    # Generate password
370
    password = util.random_password()
371

    
372
    vm = servers.create(user_id, name, password, flavor, image,
373
                        metadata=metadata, personality=personality,
374
                        private_networks=private_networks,
375
                        floating_ips=floating_ips)
376

    
377
    server = vm_to_dict(vm, detail=True)
378
    server['status'] = 'BUILD'
379
    server['adminPass'] = password
380

    
381
    response = render_server(request, server, status=202)
382

    
383
    return response
384

    
385

    
386
@api.api_method(http_method='GET', user_required=True, logger=log)
387
def get_server_details(request, server_id):
388
    # Normal Response Codes: 200, 203
389
    # Error Response Codes: computeFault (400, 500),
390
    #                       serviceUnavailable (503),
391
    #                       unauthorized (401),
392
    #                       badRequest (400),
393
    #                       itemNotFound (404),
394
    #                       overLimit (413)
395

    
396
    log.debug('get_server_details %s', server_id)
397
    vm = util.get_vm(server_id, request.user_uniq)
398
    server = vm_to_dict(vm, detail=True)
399
    return render_server(request, server)
400

    
401

    
402
@api.api_method(http_method='PUT', user_required=True, logger=log)
403
@transaction.commit_on_success
404
def update_server_name(request, server_id):
405
    # Normal Response Code: 204
406
    # Error Response Codes: computeFault (400, 500),
407
    #                       serviceUnavailable (503),
408
    #                       unauthorized (401),
409
    #                       badRequest (400),
410
    #                       badMediaType(415),
411
    #                       itemNotFound (404),
412
    #                       buildInProgress (409),
413
    #                       overLimit (413)
414

    
415
    req = utils.get_request_dict(request)
416
    log.info('update_server_name %s %s', server_id, req)
417

    
418
    try:
419
        name = req['server']['name']
420
    except (TypeError, KeyError):
421
        raise faults.BadRequest("Malformed request")
422

    
423
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
424
                     non_suspended=True)
425

    
426
    servers.rename(vm, new_name=name)
427

    
428
    return HttpResponse(status=204)
429

    
430

    
431
@api.api_method(http_method='DELETE', user_required=True, logger=log)
432
def delete_server(request, server_id):
433
    # Normal Response Codes: 204
434
    # Error Response Codes: computeFault (400, 500),
435
    #                       serviceUnavailable (503),
436
    #                       unauthorized (401),
437
    #                       itemNotFound (404),
438
    #                       unauthorized (401),
439
    #                       buildInProgress (409),
440
    #                       overLimit (413)
441

    
442
    log.info('delete_server %s', server_id)
443
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
444
                     non_suspended=True)
445
    vm = servers.destroy(vm)
446
    return HttpResponse(status=204)
447

    
448

    
449
# additional server actions
450
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
451

    
452

    
453
def key_to_action(key):
454
    """Map HTTP request key to a VM Action"""
455
    if key == "shutdown":
456
        return "STOP"
457
    if key == "delete":
458
        return "DESTROY"
459
    if key in ARBITRARY_ACTIONS:
460
        return None
461
    else:
462
        return key.upper()
463

    
464

    
465
@api.api_method(http_method='POST', user_required=True, logger=log)
466
@transaction.commit_on_success
467
def demux_server_action(request, server_id):
468
    req = utils.get_request_dict(request)
469
    log.debug('server_action %s %s', server_id, req)
470

    
471
    if len(req) != 1:
472
        raise faults.BadRequest("Malformed request")
473

    
474
    # Do not allow any action on deleted or suspended VMs
475
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
476
                     non_deleted=True, non_suspended=True)
477

    
478
    action = req.keys()[0]
479

    
480
    if key_to_action(action) not in [x[0] for x in VirtualMachine.ACTIONS]:
481
        if action not in ARBITRARY_ACTIONS:
482
            raise faults.BadRequest("Action %s not supported" % action)
483
    action_args = req[action]
484

    
485
    if not isinstance(action_args, dict):
486
        raise faults.BadRequest("Invalid argument")
487

    
488
    return server_actions[action](request, vm, action_args)
489

    
490

    
491
@api.api_method(http_method='GET', user_required=True, logger=log)
492
def list_addresses(request, server_id):
493
    # Normal Response Codes: 200, 203
494
    # Error Response Codes: computeFault (400, 500),
495
    #                       serviceUnavailable (503),
496
    #                       unauthorized (401),
497
    #                       badRequest (400),
498
    #                       overLimit (413)
499

    
500
    log.debug('list_addresses %s', server_id)
501
    vm = util.get_vm(server_id, request.user_uniq)
502
    attachments = [nic_to_dict(nic) for nic in vm.nics.filter(state="ACTIVE")]
503
    addresses = attachments_to_addresses(attachments)
504

    
505
    if request.serialization == 'xml':
506
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
507
    else:
508
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
509

    
510
    return HttpResponse(data, status=200)
511

    
512

    
513
@api.api_method(http_method='GET', user_required=True, logger=log)
514
def list_addresses_by_network(request, server_id, network_id):
515
    # Normal Response Codes: 200, 203
516
    # Error Response Codes: computeFault (400, 500),
517
    #                       serviceUnavailable (503),
518
    #                       unauthorized (401),
519
    #                       badRequest (400),
520
    #                       itemNotFound (404),
521
    #                       overLimit (413)
522

    
523
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
524
    machine = util.get_vm(server_id, request.user_uniq)
525
    network = util.get_network(network_id, request.user_uniq)
526
    nics = machine.nics.filter(network=network, state="ACTIVE").all()
527
    addresses = attachments_to_addresses(map(nic_to_dict, nics))
528

    
529
    if request.serialization == 'xml':
530
        data = render_to_string('address.xml', {'addresses': addresses})
531
    else:
532
        data = json.dumps({'network': addresses})
533

    
534
    return HttpResponse(data, status=200)
535

    
536

    
537
@api.api_method(http_method='GET', user_required=True, logger=log)
538
def list_metadata(request, server_id):
539
    # Normal Response Codes: 200, 203
540
    # Error Response Codes: computeFault (400, 500),
541
    #                       serviceUnavailable (503),
542
    #                       unauthorized (401),
543
    #                       badRequest (400),
544
    #                       overLimit (413)
545

    
546
    log.debug('list_server_metadata %s', server_id)
547
    vm = util.get_vm(server_id, request.user_uniq)
548
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
549
    return util.render_metadata(request, metadata, use_values=False,
550
                                status=200)
551

    
552

    
553
@api.api_method(http_method='POST', user_required=True, logger=log)
554
@transaction.commit_on_success
555
def update_metadata(request, server_id):
556
    # Normal Response Code: 201
557
    # Error Response Codes: computeFault (400, 500),
558
    #                       serviceUnavailable (503),
559
    #                       unauthorized (401),
560
    #                       badRequest (400),
561
    #                       buildInProgress (409),
562
    #                       badMediaType(415),
563
    #                       overLimit (413)
564

    
565
    req = utils.get_request_dict(request)
566
    log.info('update_server_metadata %s %s', server_id, req)
567
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
568
    try:
569
        metadata = req['metadata']
570
        assert isinstance(metadata, dict)
571
    except (KeyError, AssertionError):
572
        raise faults.BadRequest("Malformed request")
573

    
574
    for key, val in metadata.items():
575
        meta, created = vm.metadata.get_or_create(meta_key=key)
576
        meta.meta_value = val
577
        meta.save()
578

    
579
    vm.save()
580
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
581
    return util.render_metadata(request, vm_meta, status=201)
582

    
583

    
584
@api.api_method(http_method='GET', user_required=True, logger=log)
585
def get_metadata_item(request, server_id, key):
586
    # Normal Response Codes: 200, 203
587
    # Error Response Codes: computeFault (400, 500),
588
    #                       serviceUnavailable (503),
589
    #                       unauthorized (401),
590
    #                       itemNotFound (404),
591
    #                       badRequest (400),
592
    #                       overLimit (413)
593

    
594
    log.debug('get_server_metadata_item %s %s', server_id, key)
595
    vm = util.get_vm(server_id, request.user_uniq)
596
    meta = util.get_vm_meta(vm, key)
597
    d = {meta.meta_key: meta.meta_value}
598
    return util.render_meta(request, d, status=200)
599

    
600

    
601
@api.api_method(http_method='PUT', user_required=True, logger=log)
602
@transaction.commit_on_success
603
def create_metadata_item(request, server_id, key):
604
    # Normal Response Code: 201
605
    # Error Response Codes: computeFault (400, 500),
606
    #                       serviceUnavailable (503),
607
    #                       unauthorized (401),
608
    #                       itemNotFound (404),
609
    #                       badRequest (400),
610
    #                       buildInProgress (409),
611
    #                       badMediaType(415),
612
    #                       overLimit (413)
613

    
614
    req = utils.get_request_dict(request)
615
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
616
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
617
    try:
618
        metadict = req['meta']
619
        assert isinstance(metadict, dict)
620
        assert len(metadict) == 1
621
        assert key in metadict
622
    except (KeyError, AssertionError):
623
        raise faults.BadRequest("Malformed request")
624

    
625
    meta, created = VirtualMachineMetadata.objects.get_or_create(
626
        meta_key=key,
627
        vm=vm)
628

    
629
    meta.meta_value = metadict[key]
630
    meta.save()
631
    vm.save()
632
    d = {meta.meta_key: meta.meta_value}
633
    return util.render_meta(request, d, status=201)
634

    
635

    
636
@api.api_method(http_method='DELETE', user_required=True, logger=log)
637
@transaction.commit_on_success
638
def delete_metadata_item(request, server_id, key):
639
    # Normal Response Code: 204
640
    # Error Response Codes: computeFault (400, 500),
641
    #                       serviceUnavailable (503),
642
    #                       unauthorized (401),
643
    #                       itemNotFound (404),
644
    #                       badRequest (400),
645
    #                       buildInProgress (409),
646
    #                       badMediaType(415),
647
    #                       overLimit (413),
648

    
649
    log.info('delete_server_metadata_item %s %s', server_id, key)
650
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
651
    meta = util.get_vm_meta(vm, key)
652
    meta.delete()
653
    vm.save()
654
    return HttpResponse(status=204)
655

    
656

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

    
667
    log.debug('server_stats %s', server_id)
668
    vm = util.get_vm(server_id, request.user_uniq)
669
    #secret = util.encrypt(vm.backend_vm_id)
670
    secret = vm.backend_vm_id      # XXX disable backend id encryption
671

    
672
    stats = {
673
        'serverRef': vm.id,
674
        'refresh': settings.STATS_REFRESH_PERIOD,
675
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
676
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
677
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
678
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
679

    
680
    if request.serialization == 'xml':
681
        data = render_to_string('server_stats.xml', stats)
682
    else:
683
        data = json.dumps({'stats': stats})
684

    
685
    return HttpResponse(data, status=200)
686

    
687

    
688
# ACTIONS
689

    
690

    
691
server_actions = {}
692
network_actions = {}
693

    
694

    
695
def server_action(name):
696
    '''Decorator for functions implementing server actions.
697
    `name` is the key in the dict passed by the client.
698
    '''
699

    
700
    def decorator(func):
701
        server_actions[name] = func
702
        return func
703
    return decorator
704

    
705

    
706
def network_action(name):
707
    '''Decorator for functions implementing network actions.
708
    `name` is the key in the dict passed by the client.
709
    '''
710

    
711
    def decorator(func):
712
        network_actions[name] = func
713
        return func
714
    return decorator
715

    
716

    
717
@server_action('start')
718
def start(request, vm, args):
719
    # Normal Response Code: 202
720
    # Error Response Codes: serviceUnavailable (503),
721
    #                       itemNotFound (404)
722
    vm = servers.start(vm)
723
    return HttpResponse(status=202)
724

    
725

    
726
@server_action('shutdown')
727
def shutdown(request, vm, args):
728
    # Normal Response Code: 202
729
    # Error Response Codes: serviceUnavailable (503),
730
    #                       itemNotFound (404)
731
    vm = servers.stop(vm)
732
    return HttpResponse(status=202)
733

    
734

    
735
@server_action('reboot')
736
def reboot(request, vm, args):
737
    # Normal Response Code: 202
738
    # Error Response Codes: computeFault (400, 500),
739
    #                       serviceUnavailable (503),
740
    #                       unauthorized (401),
741
    #                       badRequest (400),
742
    #                       badMediaType(415),
743
    #                       itemNotFound (404),
744
    #                       buildInProgress (409),
745
    #                       overLimit (413)
746

    
747
    reboot_type = args.get("type", "SOFT")
748
    if reboot_type not in ["SOFT", "HARD"]:
749
        raise faults.BadRequest("Invalid 'type' attribute.")
750
    vm = servers.reboot(vm, reboot_type=reboot_type)
751
    return HttpResponse(status=202)
752

    
753

    
754
@server_action('firewallProfile')
755
def set_firewall_profile(request, vm, args):
756
    # Normal Response Code: 200
757
    # Error Response Codes: computeFault (400, 500),
758
    #                       serviceUnavailable (503),
759
    #                       unauthorized (401),
760
    #                       badRequest (400),
761
    #                       badMediaType(415),
762
    #                       itemNotFound (404),
763
    #                       buildInProgress (409),
764
    #                       overLimit (413)
765
    profile = args.get("profile")
766
    if profile is None:
767
        raise faults.BadRequest("Missing 'profile' attribute")
768
    index = args.get("index", 0)
769
    servers.set_firewall_profile(vm, profile=profile, index=index)
770
    return HttpResponse(status=202)
771

    
772

    
773
@server_action('resize')
774
def resize(request, vm, args):
775
    # Normal Response Code: 202
776
    # Error Response Codes: computeFault (400, 500),
777
    #                       serviceUnavailable (503),
778
    #                       unauthorized (401),
779
    #                       badRequest (400),
780
    #                       badMediaType(415),
781
    #                       itemNotFound (404),
782
    #                       buildInProgress (409),
783
    #                       serverCapacityUnavailable (503),
784
    #                       overLimit (413),
785
    #                       resizeNotAllowed (403)
786
    flavorRef = args.get("flavorRef")
787
    if flavorRef is None:
788
        raise faults.BadRequest("Missing 'flavorRef' attribute.")
789
    flavor = util.get_flavor(flavor_id=flavorRef, include_deleted=False)
790
    servers.resize(vm, flavor=flavor)
791
    return HttpResponse(status=202)
792

    
793

    
794
@server_action('console')
795
def get_console(request, vm, args):
796
    # Normal Response Code: 200
797
    # Error Response Codes: computeFault (400, 500),
798
    #                       serviceUnavailable (503),
799
    #                       unauthorized (401),
800
    #                       badRequest (400),
801
    #                       badMediaType(415),
802
    #                       itemNotFound (404),
803
    #                       buildInProgress (409),
804
    #                       overLimit (413)
805

    
806
    log.info("Get console  VM %s: %s", vm, args)
807

    
808
    console_type = args.get("type")
809
    if console_type is None:
810
        raise faults.BadRequest("No console 'type' specified.")
811
    elif console_type != "vnc":
812
        raise faults.BadRequest("Console 'type' can only be 'vnc'.")
813
    console_info = servers.console(vm, console_type)
814

    
815
    if request.serialization == 'xml':
816
        mimetype = 'application/xml'
817
        data = render_to_string('console.xml', {'console': console_info})
818
    else:
819
        mimetype = 'application/json'
820
        data = json.dumps({'console': console_info})
821

    
822
    return HttpResponse(data, mimetype=mimetype, status=200)
823

    
824

    
825
@server_action('changePassword')
826
def change_password(request, vm, args):
827
    raise faults.NotImplemented('Changing password is not supported.')
828

    
829

    
830
@server_action('rebuild')
831
def rebuild(request, vm, args):
832
    raise faults.NotImplemented('Rebuild not supported.')
833

    
834

    
835
@server_action('confirmResize')
836
def confirm_resize(request, vm, args):
837
    raise faults.NotImplemented('Resize not supported.')
838

    
839

    
840
@server_action('revertResize')
841
def revert_resize(request, vm, args):
842
    raise faults.NotImplemented('Resize not supported.')
843

    
844

    
845
@network_action('add')
846
@transaction.commit_on_success
847
def add(request, net, args):
848
    # Normal Response Code: 202
849
    # Error Response Codes: computeFault (400, 500),
850
    #                       serviceUnavailable (503),
851
    #                       unauthorized (401),
852
    #                       badRequest (400),
853
    #                       buildInProgress (409),
854
    #                       badMediaType(415),
855
    #                       itemNotFound (404),
856
    #                       overLimit (413)
857
    server_id = args.get('serverRef', None)
858
    if not server_id:
859
        raise faults.BadRequest('Malformed Request.')
860

    
861
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
862
    servers.connect(vm, network=net)
863
    return HttpResponse(status=202)
864

    
865

    
866
@network_action('remove')
867
@transaction.commit_on_success
868
def remove(request, net, args):
869
    # Normal Response Code: 202
870
    # Error Response Codes: computeFault (400, 500),
871
    #                       serviceUnavailable (503),
872
    #                       unauthorized (401),
873
    #                       badRequest (400),
874
    #                       badMediaType(415),
875
    #                       itemNotFound (404),
876
    #                       overLimit (413)
877

    
878
    attachment = args.get("attachment")
879
    if attachment is None:
880
        raise faults.BadRequest("Missing 'attachment' attribute.")
881
    try:
882
        # attachment string: nic-<vm-id>-<nic-index>
883
        _, server_id, nic_index = attachment.split("-", 2)
884
        server_id = int(server_id)
885
        nic_index = int(nic_index)
886
    except (ValueError, TypeError):
887
        raise faults.BadRequest("Invalid 'attachment' attribute.")
888

    
889
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
890
    servers.disconnect(vm, nic_index=nic_index)
891

    
892
    return HttpResponse(status=202)
893

    
894

    
895
@server_action("addFloatingIp")
896
def add_floating_ip(request, vm, args):
897
    address = args.get("address")
898
    if address is None:
899
        raise faults.BadRequest("Missing 'address' attribute")
900

    
901
    servers.add_floating_ip(vm, address)
902
    return HttpResponse(status=202)
903

    
904

    
905
@server_action("removeFloatingIp")
906
def remove_floating_ip(request, vm, args):
907
    address = args.get("address")
908
    if address is None:
909
        raise faults.BadRequest("Missing 'address' attribute")
910

    
911
    servers.remove_floating_ip(vm, address)
912
    return HttpResponse(status=202)