Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (33.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 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_attachments(nic):
116
    """Convert a NIC object to 'attachments attribute.
117

118
    Convert a NIC object to match the format of 'attachments' attribute of the
119
    response to the /servers API call.
120

121
    NOTE: The 'ips' of the NIC object have been prefetched in order to avoid DB
122
    queries. No subsequent queries for 'ips' (like filtering) should be
123
    performed because this will return in a new DB query.
124

125
    """
126
    d = {'id': nic.id,
127
         'network_id': str(nic.network_id),
128
         'mac_address': nic.mac,
129
         'ipv4': '',
130
         'ipv6': ''}
131

    
132
    if nic.firewall_profile:
133
        d['firewallProfile'] = nic.firewall_profile
134

    
135
    for ip in nic.ips.all():
136
        if not ip.deleted:
137
            ip_type = "floating" if ip.floating_ip else "fixed"
138
            if ip.ipversion == 4:
139
                d["ipv4"] = ip.address
140
                d["OS-EXT-IPS:type"] = ip_type
141
            else:
142
                d["ipv6"] = ip.address
143
                d["OS-EXT-IPS:type"] = ip_type
144
    return d
145

    
146

    
147
def attachments_to_addresses(attachments):
148
    """Convert 'attachments' attribute to 'addresses'.
149

150
    Convert a a list of 'attachments' attribute to a list of 'addresses'
151
    attribute, as expected in the response to /servers API call.
152

153
    """
154
    addresses = {}
155
    for nic in attachments:
156
        net_addrs = []
157
        if nic["ipv4"]:
158
            net_addrs.append({"version": 4,
159
                              "addr": nic["ipv4"],
160
                              "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
161
        if nic["ipv6"]:
162
            net_addrs.append({"version": 6,
163
                              "addr": nic["ipv6"],
164
                              "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
165
        addresses[nic["network_id"]] = net_addrs
166
    return addresses
167

    
168

    
169
def vm_to_dict(vm, detail=False):
170
    d = dict(id=vm.id, name=vm.name)
171
    d['links'] = util.vm_to_links(vm.id)
172
    if detail:
173
        d['user_id'] = vm.userid
174
        d['tenant_id'] = vm.userid
175
        d['status'] = logic_utils.get_rsapi_state(vm)
176
        d['SNF:task_state'] = logic_utils.get_task_state(vm)
177
        d['progress'] = 100 if d['status'] == 'ACTIVE' else vm.buildpercentage
178
        d['hostId'] = vm.hostid
179
        d['updated'] = utils.isoformat(vm.updated)
180
        d['created'] = utils.isoformat(vm.created)
181
        d['flavor'] = {"id": vm.flavor.id,
182
                       "links": util.flavor_to_links(vm.flavor.id)}
183
        d['image'] = {"id": vm.imageid,
184
                      "links": util.image_to_links(vm.imageid)}
185
        d['suspended'] = vm.suspended
186

    
187
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
188
        d['metadata'] = metadata
189

    
190
        nics = vm.nics.all()
191
        active_nics = filter(lambda nic: nic.state == "ACTIVE", nics)
192
        active_nics.sort(key=lambda nic: nic.id)
193
        attachments = map(nic_to_attachments, active_nics)
194
        d['attachments'] = attachments
195
        d['addresses'] = attachments_to_addresses(attachments)
196

    
197
        # include the latest vm diagnostic, if set
198
        diagnostic = vm.get_last_diagnostic()
199
        if diagnostic:
200
            d['diagnostics'] = diagnostics_to_dict([diagnostic])
201
        else:
202
            d['diagnostics'] = []
203
        # Fixed
204
        d["security_groups"] = [{"name": "default"}]
205
        d["key_name"] = None
206
        d["config_drive"] = ""
207
        d["accessIPv4"] = ""
208
        d["accessIPv6"] = ""
209
        fqdn = get_server_fqdn(vm, active_nics)
210
        d["SNF:fqdn"] = fqdn
211
        d["SNF:port_forwarding"] = get_server_port_forwarding(vm, active_nics,
212
                                                              fqdn)
213
        d['deleted'] = vm.deleted
214
    return d
215

    
216

    
217
def get_server_public_ip(vm_nics, version=4):
218
    """Get the first public IP address of a server.
219

220
    NOTE: 'vm_nics' objects have prefetched the ips
221
    """
222
    for nic in vm_nics:
223
        for ip in nic.ips.all():
224
            if ip.ipversion == version and ip.public:
225
                return ip
226
    return None
227

    
228

    
229
def get_server_fqdn(vm, vm_nics):
230
    fqdn_setting = settings.CYCLADES_SERVERS_FQDN
231
    if fqdn_setting is None:
232
        return None
233
    elif isinstance(fqdn_setting, basestring):
234
        return fqdn_setting % {"id": vm.id}
235
    else:
236
        msg = ("Invalid setting: CYCLADES_SERVERS_FQDN."
237
               " Value must be a string.")
238
        raise faults.InternalServerError(msg)
239

    
240

    
241
def get_server_port_forwarding(vm, vm_nics, fqdn):
242
    """Create API 'port_forwarding' attribute from corresponding setting.
243

244
    Create the 'port_forwarding' API vm attribute based on the corresponding
245
    setting (CYCLADES_PORT_FORWARDING), which can be either a tuple
246
    of the form (host, port) or a callable object returning such tuple. In
247
    case of callable object, must be called with the following arguments:
248
    * ip_address
249
    * server_id
250
    * fqdn
251
    * owner UUID
252

253
    NOTE: 'vm_nics' objects have prefetched the ips
254
    """
255
    port_forwarding = {}
256
    public_ip = get_server_public_ip(vm_nics)
257
    if public_ip is None:
258
        return port_forwarding
259
    for dport, to_dest in settings.CYCLADES_PORT_FORWARDING.items():
260
        if hasattr(to_dest, "__call__"):
261
            to_dest = to_dest(public_ip.address, vm.id, fqdn, vm.userid)
262
        msg = ("Invalid setting: CYCLADES_PORT_FOWARDING."
263
               " Value must be a tuple of two elements (host, port).")
264
        if not isinstance(to_dest, tuple) or len(to_dest) != 2:
265
                raise faults.InternalServerError(msg)
266
        else:
267
            try:
268
                host, port = to_dest
269
            except (TypeError, ValueError):
270
                raise faults.InternalServerError(msg)
271

    
272
        port_forwarding[dport] = {"host": host, "port": str(port)}
273
    return port_forwarding
274

    
275

    
276
def diagnostics_to_dict(diagnostics):
277
    """
278
    Extract api data from diagnostics QuerySet.
279
    """
280
    entries = list()
281

    
282
    for diagnostic in diagnostics:
283
        # format source date if set
284
        formatted_source_date = None
285
        if diagnostic.source_date:
286
            formatted_source_date = utils.isoformat(diagnostic.source_date)
287

    
288
        entry = {
289
            'source': diagnostic.source,
290
            'created': utils.isoformat(diagnostic.created),
291
            'message': diagnostic.message,
292
            'details': diagnostic.details,
293
            'level': diagnostic.level,
294
        }
295

    
296
        if formatted_source_date:
297
            entry['source_date'] = formatted_source_date
298

    
299
        entries.append(entry)
300

    
301
    return entries
302

    
303

    
304
def render_server(request, server, status=200):
305
    if request.serialization == 'xml':
306
        data = render_to_string('server.xml', {
307
            'server': server,
308
            'is_root': True})
309
    else:
310
        data = json.dumps({'server': server})
311
    return HttpResponse(data, status=status)
312

    
313

    
314
def render_diagnostics(request, diagnostics_dict, status=200):
315
    """
316
    Render diagnostics dictionary to json response.
317
    """
318
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
319

    
320

    
321
@api.api_method(http_method='GET', user_required=True, logger=log)
322
def get_server_diagnostics(request, server_id):
323
    """
324
    Virtual machine diagnostics api view.
325
    """
326
    log.debug('server_diagnostics %s', server_id)
327
    vm = util.get_vm(server_id, request.user_uniq)
328
    diagnostics = diagnostics_to_dict(vm.diagnostics.all())
329
    return render_diagnostics(request, diagnostics)
330

    
331

    
332
@api.api_method(http_method='GET', user_required=True, logger=log)
333
def list_servers(request, detail=False):
334
    # Normal Response Codes: 200, 203
335
    # Error Response Codes: computeFault (400, 500),
336
    #                       serviceUnavailable (503),
337
    #                       unauthorized (401),
338
    #                       badRequest (400),
339
    #                       overLimit (413)
340

    
341
    log.debug('list_servers detail=%s', detail)
342
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
343
    if detail:
344
        user_vms = user_vms.prefetch_related("nics__ips")
345

    
346
    user_vms = utils.filter_modified_since(request, objects=user_vms)
347

    
348
    servers_dict = [vm_to_dict(server, detail)
349
                    for server in user_vms.order_by('id')]
350

    
351
    if request.serialization == 'xml':
352
        data = render_to_string('list_servers.xml', {
353
            'servers': servers_dict,
354
            'detail': detail})
355
    else:
356
        data = json.dumps({'servers': servers_dict})
357

    
358
    return HttpResponse(data, status=200)
359

    
360

    
361
@api.api_method(http_method='POST', user_required=True, logger=log)
362
def create_server(request):
363
    # Normal Response Code: 202
364
    # Error Response Codes: computeFault (400, 500),
365
    #                       serviceUnavailable (503),
366
    #                       unauthorized (401),
367
    #                       badMediaType(415),
368
    #                       itemNotFound (404),
369
    #                       badRequest (400),
370
    #                       serverCapacityUnavailable (503),
371
    #                       overLimit (413)
372
    req = utils.get_request_dict(request)
373
    log.info('create_server %s', req)
374
    user_id = request.user_uniq
375

    
376
    try:
377
        server = req['server']
378
        name = server['name']
379
        metadata = server.get('metadata', {})
380
        assert isinstance(metadata, dict)
381
        image_id = server['imageRef']
382
        flavor_id = server['flavorRef']
383
        personality = server.get('personality', [])
384
        assert isinstance(personality, list)
385
        networks = server.get("networks")
386
        if networks is not None:
387
            assert isinstance(networks, list)
388
    except (KeyError, AssertionError):
389
        raise faults.BadRequest("Malformed request")
390

    
391
    # Verify that personalities are well-formed
392
    util.verify_personality(personality)
393
    # Get image information
394
    image = util.get_image_dict(image_id, user_id)
395
    # Get flavor (ensure it is active)
396
    flavor = util.get_flavor(flavor_id, include_deleted=False)
397
    if not flavor.allow_create:
398
        msg = ("It is not allowed to create a server from flavor with id '%d',"
399
               " see 'allow_create' flavor attribute")
400
        raise faults.Forbidden(msg % flavor.id)
401
    # Generate password
402
    password = util.random_password()
403

    
404
    vm = servers.create(user_id, name, password, flavor, image,
405
                        metadata=metadata, personality=personality,
406
                        networks=networks)
407

    
408
    server = vm_to_dict(vm, detail=True)
409
    server['status'] = 'BUILD'
410
    server['adminPass'] = password
411

    
412
    response = render_server(request, server, status=202)
413

    
414
    return response
415

    
416

    
417
@api.api_method(http_method='GET', user_required=True, logger=log)
418
def get_server_details(request, server_id):
419
    # Normal Response Codes: 200, 203
420
    # Error Response Codes: computeFault (400, 500),
421
    #                       serviceUnavailable (503),
422
    #                       unauthorized (401),
423
    #                       badRequest (400),
424
    #                       itemNotFound (404),
425
    #                       overLimit (413)
426

    
427
    log.debug('get_server_details %s', server_id)
428
    vm = util.get_vm(server_id, request.user_uniq,
429
                     prefetch_related="nics__ips")
430
    server = vm_to_dict(vm, detail=True)
431
    return render_server(request, server)
432

    
433

    
434
@api.api_method(http_method='PUT', user_required=True, logger=log)
435
@transaction.commit_on_success
436
def update_server_name(request, server_id):
437
    # Normal Response Code: 204
438
    # Error Response Codes: computeFault (400, 500),
439
    #                       serviceUnavailable (503),
440
    #                       unauthorized (401),
441
    #                       badRequest (400),
442
    #                       badMediaType(415),
443
    #                       itemNotFound (404),
444
    #                       buildInProgress (409),
445
    #                       overLimit (413)
446

    
447
    req = utils.get_request_dict(request)
448
    log.info('update_server_name %s %s', server_id, req)
449

    
450
    req = utils.get_attribute(req, "server", attr_type=dict, required=True)
451
    name = utils.get_attribute(req, "name", attr_type=basestring,
452
                               required=True)
453

    
454
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
455
                     non_suspended=True)
456

    
457
    servers.rename(vm, new_name=name)
458

    
459
    return HttpResponse(status=204)
460

    
461

    
462
@api.api_method(http_method='DELETE', user_required=True, logger=log)
463
def delete_server(request, server_id):
464
    # Normal Response Codes: 204
465
    # Error Response Codes: computeFault (400, 500),
466
    #                       serviceUnavailable (503),
467
    #                       unauthorized (401),
468
    #                       itemNotFound (404),
469
    #                       unauthorized (401),
470
    #                       buildInProgress (409),
471
    #                       overLimit (413)
472

    
473
    log.info('delete_server %s', server_id)
474
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
475
                     non_suspended=True)
476
    vm = servers.destroy(vm)
477
    return HttpResponse(status=204)
478

    
479

    
480
# additional server actions
481
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
482

    
483

    
484
def key_to_action(key):
485
    """Map HTTP request key to a VM Action"""
486
    if key == "shutdown":
487
        return "STOP"
488
    if key == "delete":
489
        return "DESTROY"
490
    if key in ARBITRARY_ACTIONS:
491
        return None
492
    else:
493
        return key.upper()
494

    
495

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

    
502
    if not isinstance(req, dict) and len(req) != 1:
503
        raise faults.BadRequest("Malformed request")
504

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

    
509
    action = req.keys()[0]
510
    if not isinstance(action, basestring):
511
        raise faults.BadRequest("Malformed Request. Invalid action.")
512

    
513
    if key_to_action(action) not in [x[0] for x in VirtualMachine.ACTIONS]:
514
        if action not in ARBITRARY_ACTIONS:
515
            raise faults.BadRequest("Action %s not supported" % action)
516
    action_args = utils.get_attribute(req, action, required=True,
517
                                      attr_type=dict)
518

    
519
    return server_actions[action](request, vm, action_args)
520

    
521

    
522
@api.api_method(http_method='GET', user_required=True, logger=log)
523
def list_addresses(request, server_id):
524
    # Normal Response Codes: 200, 203
525
    # Error Response Codes: computeFault (400, 500),
526
    #                       serviceUnavailable (503),
527
    #                       unauthorized (401),
528
    #                       badRequest (400),
529
    #                       overLimit (413)
530

    
531
    log.debug('list_addresses %s', server_id)
532
    vm = util.get_vm(server_id, request.user_uniq,
533
                     prefetch_related="nics__ips")
534
    attachments = [nic_to_attachments(nic)
535
                   for nic in vm.nics.filter(state="ACTIVE")]
536
    addresses = attachments_to_addresses(attachments)
537

    
538
    if request.serialization == 'xml':
539
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
540
    else:
541
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
542

    
543
    return HttpResponse(data, status=200)
544

    
545

    
546
@api.api_method(http_method='GET', user_required=True, logger=log)
547
def list_addresses_by_network(request, server_id, network_id):
548
    # Normal Response Codes: 200, 203
549
    # Error Response Codes: computeFault (400, 500),
550
    #                       serviceUnavailable (503),
551
    #                       unauthorized (401),
552
    #                       badRequest (400),
553
    #                       itemNotFound (404),
554
    #                       overLimit (413)
555

    
556
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
557
    machine = util.get_vm(server_id, request.user_uniq)
558
    network = util.get_network(network_id, request.user_uniq)
559
    nics = machine.nics.filter(network=network, state="ACTIVE")
560
    addresses = attachments_to_addresses(map(nic_to_attachments, nics))
561

    
562
    if request.serialization == 'xml':
563
        data = render_to_string('address.xml', {'addresses': addresses})
564
    else:
565
        data = json.dumps({'network': addresses})
566

    
567
    return HttpResponse(data, status=200)
568

    
569

    
570
@api.api_method(http_method='GET', user_required=True, logger=log)
571
def list_metadata(request, server_id):
572
    # Normal Response Codes: 200, 203
573
    # Error Response Codes: computeFault (400, 500),
574
    #                       serviceUnavailable (503),
575
    #                       unauthorized (401),
576
    #                       badRequest (400),
577
    #                       overLimit (413)
578

    
579
    log.debug('list_server_metadata %s', server_id)
580
    vm = util.get_vm(server_id, request.user_uniq)
581
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
582
    return util.render_metadata(request, metadata, use_values=False,
583
                                status=200)
584

    
585

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

    
598
    req = utils.get_request_dict(request)
599
    log.info('update_server_metadata %s %s', server_id, req)
600
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
601
    metadata = utils.get_attribute(req, "metadata", required=True,
602
                                   attr_type=dict)
603

    
604
    for key, val in metadata.items():
605
        if not isinstance(key, (basestring, int)) or\
606
           not isinstance(val, (basestring, int)):
607
            raise faults.BadRequest("Malformed Request. Invalid metadata.")
608
        meta, created = vm.metadata.get_or_create(meta_key=key)
609
        meta.meta_value = val
610
        meta.save()
611

    
612
    vm.save()
613
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
614
    return util.render_metadata(request, vm_meta, status=201)
615

    
616

    
617
@api.api_method(http_method='GET', user_required=True, logger=log)
618
def get_metadata_item(request, server_id, key):
619
    # Normal Response Codes: 200, 203
620
    # Error Response Codes: computeFault (400, 500),
621
    #                       serviceUnavailable (503),
622
    #                       unauthorized (401),
623
    #                       itemNotFound (404),
624
    #                       badRequest (400),
625
    #                       overLimit (413)
626

    
627
    log.debug('get_server_metadata_item %s %s', server_id, key)
628
    vm = util.get_vm(server_id, request.user_uniq)
629
    meta = util.get_vm_meta(vm, key)
630
    d = {meta.meta_key: meta.meta_value}
631
    return util.render_meta(request, d, status=200)
632

    
633

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

    
647
    req = utils.get_request_dict(request)
648
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
649
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
650
    try:
651
        metadict = req['meta']
652
        assert isinstance(metadict, dict)
653
        assert len(metadict) == 1
654
        assert key in metadict
655
    except (KeyError, AssertionError):
656
        raise faults.BadRequest("Malformed request")
657

    
658
    meta, created = VirtualMachineMetadata.objects.get_or_create(
659
        meta_key=key,
660
        vm=vm)
661

    
662
    meta.meta_value = metadict[key]
663
    meta.save()
664
    vm.save()
665
    d = {meta.meta_key: meta.meta_value}
666
    return util.render_meta(request, d, status=201)
667

    
668

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

    
682
    log.info('delete_server_metadata_item %s %s', server_id, key)
683
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
684
    meta = util.get_vm_meta(vm, key)
685
    meta.delete()
686
    vm.save()
687
    return HttpResponse(status=204)
688

    
689

    
690
@api.api_method(http_method='GET', user_required=True, logger=log)
691
def server_stats(request, server_id):
692
    # Normal Response Codes: 200
693
    # Error Response Codes: computeFault (400, 500),
694
    #                       serviceUnavailable (503),
695
    #                       unauthorized (401),
696
    #                       badRequest (400),
697
    #                       itemNotFound (404),
698
    #                       overLimit (413)
699

    
700
    log.debug('server_stats %s', server_id)
701
    vm = util.get_vm(server_id, request.user_uniq)
702
    secret = util.stats_encrypt(vm.backend_vm_id)
703

    
704
    stats = {
705
        'serverRef': vm.id,
706
        'refresh': settings.STATS_REFRESH_PERIOD,
707
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
708
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
709
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
710
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
711

    
712
    if request.serialization == 'xml':
713
        data = render_to_string('server_stats.xml', stats)
714
    else:
715
        data = json.dumps({'stats': stats})
716

    
717
    return HttpResponse(data, status=200)
718

    
719

    
720
# ACTIONS
721

    
722

    
723
server_actions = {}
724
network_actions = {}
725

    
726

    
727
def server_action(name):
728
    '''Decorator for functions implementing server actions.
729
    `name` is the key in the dict passed by the client.
730
    '''
731

    
732
    def decorator(func):
733
        server_actions[name] = func
734
        return func
735
    return decorator
736

    
737

    
738
def network_action(name):
739
    '''Decorator for functions implementing network actions.
740
    `name` is the key in the dict passed by the client.
741
    '''
742

    
743
    def decorator(func):
744
        network_actions[name] = func
745
        return func
746
    return decorator
747

    
748

    
749
@server_action('start')
750
def start(request, vm, args):
751
    # Normal Response Code: 202
752
    # Error Response Codes: serviceUnavailable (503),
753
    #                       itemNotFound (404)
754
    vm = servers.start(vm)
755
    return HttpResponse(status=202)
756

    
757

    
758
@server_action('shutdown')
759
def shutdown(request, vm, args):
760
    # Normal Response Code: 202
761
    # Error Response Codes: serviceUnavailable (503),
762
    #                       itemNotFound (404)
763
    vm = servers.stop(vm)
764
    return HttpResponse(status=202)
765

    
766

    
767
@server_action('reboot')
768
def reboot(request, vm, args):
769
    # Normal Response Code: 202
770
    # Error Response Codes: computeFault (400, 500),
771
    #                       serviceUnavailable (503),
772
    #                       unauthorized (401),
773
    #                       badRequest (400),
774
    #                       badMediaType(415),
775
    #                       itemNotFound (404),
776
    #                       buildInProgress (409),
777
    #                       overLimit (413)
778

    
779
    reboot_type = args.get("type", "SOFT")
780
    if reboot_type not in ["SOFT", "HARD"]:
781
        raise faults.BadRequest("Invalid 'type' attribute.")
782
    vm = servers.reboot(vm, reboot_type=reboot_type)
783
    return HttpResponse(status=202)
784

    
785

    
786
@server_action('firewallProfile')
787
def set_firewall_profile(request, vm, args):
788
    # Normal Response Code: 200
789
    # Error Response Codes: computeFault (400, 500),
790
    #                       serviceUnavailable (503),
791
    #                       unauthorized (401),
792
    #                       badRequest (400),
793
    #                       badMediaType(415),
794
    #                       itemNotFound (404),
795
    #                       buildInProgress (409),
796
    #                       overLimit (413)
797
    profile = args.get("profile")
798
    if profile is None:
799
        raise faults.BadRequest("Missing 'profile' attribute")
800
    nic_id = args.get("nic")
801
    if nic_id is None:
802
        raise faults.BadRequest("Missing 'nic' attribute")
803
    nic = util.get_vm_nic(vm, nic_id)
804
    servers.set_firewall_profile(vm, profile=profile, nic=nic)
805
    return HttpResponse(status=202)
806

    
807

    
808
@server_action('resize')
809
def resize(request, vm, args):
810
    # Normal Response Code: 202
811
    # Error Response Codes: computeFault (400, 500),
812
    #                       serviceUnavailable (503),
813
    #                       unauthorized (401),
814
    #                       badRequest (400),
815
    #                       badMediaType(415),
816
    #                       itemNotFound (404),
817
    #                       buildInProgress (409),
818
    #                       serverCapacityUnavailable (503),
819
    #                       overLimit (413),
820
    #                       resizeNotAllowed (403)
821
    flavorRef = args.get("flavorRef")
822
    if flavorRef is None:
823
        raise faults.BadRequest("Missing 'flavorRef' attribute.")
824
    flavor = util.get_flavor(flavor_id=flavorRef, include_deleted=False)
825
    servers.resize(vm, flavor=flavor)
826
    return HttpResponse(status=202)
827

    
828

    
829
@server_action('console')
830
def get_console(request, vm, args):
831
    # Normal Response Code: 200
832
    # Error Response Codes: computeFault (400, 500),
833
    #                       serviceUnavailable (503),
834
    #                       unauthorized (401),
835
    #                       badRequest (400),
836
    #                       badMediaType(415),
837
    #                       itemNotFound (404),
838
    #                       buildInProgress (409),
839
    #                       overLimit (413)
840

    
841
    log.info("Get console  VM %s: %s", vm, args)
842

    
843
    console_type = args.get("type")
844
    if console_type is None:
845
        raise faults.BadRequest("No console 'type' specified.")
846
    elif console_type != "vnc":
847
        raise faults.BadRequest("Console 'type' can only be 'vnc'.")
848
    console_info = servers.console(vm, console_type)
849

    
850
    if request.serialization == 'xml':
851
        mimetype = 'application/xml'
852
        data = render_to_string('console.xml', {'console': console_info})
853
    else:
854
        mimetype = 'application/json'
855
        data = json.dumps({'console': console_info})
856

    
857
    return HttpResponse(data, mimetype=mimetype, status=200)
858

    
859

    
860
@server_action('changePassword')
861
def change_password(request, vm, args):
862
    raise faults.NotImplemented('Changing password is not supported.')
863

    
864

    
865
@server_action('rebuild')
866
def rebuild(request, vm, args):
867
    raise faults.NotImplemented('Rebuild not supported.')
868

    
869

    
870
@server_action('confirmResize')
871
def confirm_resize(request, vm, args):
872
    raise faults.NotImplemented('Resize not supported.')
873

    
874

    
875
@server_action('revertResize')
876
def revert_resize(request, vm, args):
877
    raise faults.NotImplemented('Resize not supported.')
878

    
879

    
880
@network_action('add')
881
@transaction.commit_on_success
882
def add(request, net, args):
883
    # Normal Response Code: 202
884
    # Error Response Codes: computeFault (400, 500),
885
    #                       serviceUnavailable (503),
886
    #                       unauthorized (401),
887
    #                       badRequest (400),
888
    #                       buildInProgress (409),
889
    #                       badMediaType(415),
890
    #                       itemNotFound (404),
891
    #                       overLimit (413)
892
    server_id = args.get('serverRef', None)
893
    if not server_id:
894
        raise faults.BadRequest('Malformed Request.')
895

    
896
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
897
    servers.connect(vm, network=net)
898
    return HttpResponse(status=202)
899

    
900

    
901
@network_action('remove')
902
@transaction.commit_on_success
903
def remove(request, net, args):
904
    # Normal Response Code: 202
905
    # Error Response Codes: computeFault (400, 500),
906
    #                       serviceUnavailable (503),
907
    #                       unauthorized (401),
908
    #                       badRequest (400),
909
    #                       badMediaType(415),
910
    #                       itemNotFound (404),
911
    #                       overLimit (413)
912

    
913
    attachment = args.get("attachment")
914
    if attachment is None:
915
        raise faults.BadRequest("Missing 'attachment' attribute.")
916
    try:
917
        nic_id = int(attachment)
918
    except (ValueError, TypeError):
919
        raise faults.BadRequest("Invalid 'attachment' attribute.")
920

    
921
    nic = util.get_nic(nic_id=nic_id)
922
    server_id = nic.machine_id
923
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
924

    
925
    servers.disconnect(vm, nic)
926

    
927
    return HttpResponse(status=202)
928

    
929

    
930
@server_action("addFloatingIp")
931
def add_floating_ip(request, vm, args):
932
    address = args.get("address")
933
    if address is None:
934
        raise faults.BadRequest("Missing 'address' attribute")
935

    
936
    userid = vm.userid
937
    floating_ip = util.get_floating_ip_by_address(userid, address,
938
                                                  for_update=True)
939
    servers.create_port(userid, floating_ip.network, machine=vm,
940
                        user_ipaddress=floating_ip)
941
    return HttpResponse(status=202)
942

    
943

    
944
@server_action("removeFloatingIp")
945
def remove_floating_ip(request, vm, args):
946
    address = args.get("address")
947
    if address is None:
948
        raise faults.BadRequest("Missing 'address' attribute")
949
    floating_ip = util.get_floating_ip_by_address(vm.userid, address,
950
                                                  for_update=True)
951
    if floating_ip.nic is None:
952
        raise faults.BadRequest("Floating IP %s not attached to instance"
953
                                % address)
954
    servers.delete_port(floating_ip.nic)
955
    return HttpResponse(status=202)