Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (33.3 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
    return d
214

    
215

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

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

    
227

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

    
239

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

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

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

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

    
274

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

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

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

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

    
298
        entries.append(entry)
299

    
300
    return entries
301

    
302

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

    
312

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

    
319

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

    
330

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

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

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

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

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

    
357
    return HttpResponse(data, status=200)
358

    
359

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

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

    
390
    # Verify that personalities are well-formed
391
    util.verify_personality(personality)
392
    # Get image information
393
    image = util.get_image_dict(image_id, user_id)
394
    # Get flavor (ensure it is active)
395
    flavor = util.get_flavor(flavor_id, include_deleted=False)
396
    # Generate password
397
    password = util.random_password()
398

    
399
    vm = servers.create(user_id, name, password, flavor, image,
400
                        metadata=metadata, personality=personality,
401
                        networks=networks)
402

    
403
    server = vm_to_dict(vm, detail=True)
404
    server['status'] = 'BUILD'
405
    server['adminPass'] = password
406

    
407
    response = render_server(request, server, status=202)
408

    
409
    return response
410

    
411

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

    
422
    log.debug('get_server_details %s', server_id)
423
    vm = util.get_vm(server_id, request.user_uniq,
424
                     prefetch_related="nics__ips")
425
    server = vm_to_dict(vm, detail=True)
426
    return render_server(request, server)
427

    
428

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

    
442
    req = utils.get_request_dict(request)
443
    log.info('update_server_name %s %s', server_id, req)
444

    
445
    try:
446
        name = req['server']['name']
447
    except (TypeError, KeyError):
448
        raise faults.BadRequest("Malformed request")
449

    
450
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
451
                     non_suspended=True)
452

    
453
    servers.rename(vm, new_name=name)
454

    
455
    return HttpResponse(status=204)
456

    
457

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

    
469
    log.info('delete_server %s', server_id)
470
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
471
                     non_suspended=True)
472
    vm = servers.destroy(vm)
473
    return HttpResponse(status=204)
474

    
475

    
476
# additional server actions
477
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
478

    
479

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

    
491

    
492
@api.api_method(http_method='POST', user_required=True, logger=log)
493
@transaction.commit_on_success
494
def demux_server_action(request, server_id):
495
    req = utils.get_request_dict(request)
496
    log.debug('server_action %s %s', server_id, req)
497

    
498
    if len(req) != 1:
499
        raise faults.BadRequest("Malformed request")
500

    
501
    # Do not allow any action on deleted or suspended VMs
502
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
503
                     non_deleted=True, non_suspended=True)
504

    
505
    action = req.keys()[0]
506

    
507
    if key_to_action(action) not in [x[0] for x in VirtualMachine.ACTIONS]:
508
        if action not in ARBITRARY_ACTIONS:
509
            raise faults.BadRequest("Action %s not supported" % action)
510
    action_args = req[action]
511

    
512
    if not isinstance(action_args, dict):
513
        raise faults.BadRequest("Invalid argument")
514

    
515
    return server_actions[action](request, vm, action_args)
516

    
517

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

    
527
    log.debug('list_addresses %s', server_id)
528
    vm = util.get_vm(server_id, request.user_uniq, prefetch_related="nic__ips")
529
    attachments = [nic_to_attachments(nic)
530
                   for nic in vm.nics.filter(state="ACTIVE")]
531
    addresses = attachments_to_addresses(attachments)
532

    
533
    if request.serialization == 'xml':
534
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
535
    else:
536
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
537

    
538
    return HttpResponse(data, status=200)
539

    
540

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

    
551
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
552
    machine = util.get_vm(server_id, request.user_uniq)
553
    network = util.get_network(network_id, request.user_uniq)
554
    nics = machine.nics.filter(network=network, state="ACTIVE")
555
    addresses = attachments_to_addresses(map(nic_to_attachments, nics))
556

    
557
    if request.serialization == 'xml':
558
        data = render_to_string('address.xml', {'addresses': addresses})
559
    else:
560
        data = json.dumps({'network': addresses})
561

    
562
    return HttpResponse(data, status=200)
563

    
564

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

    
574
    log.debug('list_server_metadata %s', server_id)
575
    vm = util.get_vm(server_id, request.user_uniq)
576
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
577
    return util.render_metadata(request, metadata, use_values=False,
578
                                status=200)
579

    
580

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

    
593
    req = utils.get_request_dict(request)
594
    log.info('update_server_metadata %s %s', server_id, req)
595
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
596
    try:
597
        metadata = req['metadata']
598
        assert isinstance(metadata, dict)
599
    except (KeyError, AssertionError):
600
        raise faults.BadRequest("Malformed request")
601

    
602
    for key, val in metadata.items():
603
        meta, created = vm.metadata.get_or_create(meta_key=key)
604
        meta.meta_value = val
605
        meta.save()
606

    
607
    vm.save()
608
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
609
    return util.render_metadata(request, vm_meta, status=201)
610

    
611

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

    
622
    log.debug('get_server_metadata_item %s %s', server_id, key)
623
    vm = util.get_vm(server_id, request.user_uniq)
624
    meta = util.get_vm_meta(vm, key)
625
    d = {meta.meta_key: meta.meta_value}
626
    return util.render_meta(request, d, status=200)
627

    
628

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

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

    
653
    meta, created = VirtualMachineMetadata.objects.get_or_create(
654
        meta_key=key,
655
        vm=vm)
656

    
657
    meta.meta_value = metadict[key]
658
    meta.save()
659
    vm.save()
660
    d = {meta.meta_key: meta.meta_value}
661
    return util.render_meta(request, d, status=201)
662

    
663

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

    
677
    log.info('delete_server_metadata_item %s %s', server_id, key)
678
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
679
    meta = util.get_vm_meta(vm, key)
680
    meta.delete()
681
    vm.save()
682
    return HttpResponse(status=204)
683

    
684

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

    
695
    log.debug('server_stats %s', server_id)
696
    vm = util.get_vm(server_id, request.user_uniq)
697
    secret = util.stats_encrypt(vm.backend_vm_id)
698

    
699
    stats = {
700
        'serverRef': vm.id,
701
        'refresh': settings.STATS_REFRESH_PERIOD,
702
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
703
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
704
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
705
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
706

    
707
    if request.serialization == 'xml':
708
        data = render_to_string('server_stats.xml', stats)
709
    else:
710
        data = json.dumps({'stats': stats})
711

    
712
    return HttpResponse(data, status=200)
713

    
714

    
715
# ACTIONS
716

    
717

    
718
server_actions = {}
719
network_actions = {}
720

    
721

    
722
def server_action(name):
723
    '''Decorator for functions implementing server actions.
724
    `name` is the key in the dict passed by the client.
725
    '''
726

    
727
    def decorator(func):
728
        server_actions[name] = func
729
        return func
730
    return decorator
731

    
732

    
733
def network_action(name):
734
    '''Decorator for functions implementing network actions.
735
    `name` is the key in the dict passed by the client.
736
    '''
737

    
738
    def decorator(func):
739
        network_actions[name] = func
740
        return func
741
    return decorator
742

    
743

    
744
@server_action('start')
745
def start(request, vm, args):
746
    # Normal Response Code: 202
747
    # Error Response Codes: serviceUnavailable (503),
748
    #                       itemNotFound (404)
749
    vm = servers.start(vm)
750
    return HttpResponse(status=202)
751

    
752

    
753
@server_action('shutdown')
754
def shutdown(request, vm, args):
755
    # Normal Response Code: 202
756
    # Error Response Codes: serviceUnavailable (503),
757
    #                       itemNotFound (404)
758
    vm = servers.stop(vm)
759
    return HttpResponse(status=202)
760

    
761

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

    
774
    reboot_type = args.get("type", "SOFT")
775
    if reboot_type not in ["SOFT", "HARD"]:
776
        raise faults.BadRequest("Invalid 'type' attribute.")
777
    vm = servers.reboot(vm, reboot_type=reboot_type)
778
    return HttpResponse(status=202)
779

    
780

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

    
802

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

    
823

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

    
836
    log.info("Get console  VM %s: %s", vm, args)
837

    
838
    console_type = args.get("type")
839
    if console_type is None:
840
        raise faults.BadRequest("No console 'type' specified.")
841
    elif console_type != "vnc":
842
        raise faults.BadRequest("Console 'type' can only be 'vnc'.")
843
    console_info = servers.console(vm, console_type)
844

    
845
    if request.serialization == 'xml':
846
        mimetype = 'application/xml'
847
        data = render_to_string('console.xml', {'console': console_info})
848
    else:
849
        mimetype = 'application/json'
850
        data = json.dumps({'console': console_info})
851

    
852
    return HttpResponse(data, mimetype=mimetype, status=200)
853

    
854

    
855
@server_action('changePassword')
856
def change_password(request, vm, args):
857
    raise faults.NotImplemented('Changing password is not supported.')
858

    
859

    
860
@server_action('rebuild')
861
def rebuild(request, vm, args):
862
    raise faults.NotImplemented('Rebuild not supported.')
863

    
864

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

    
869

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

    
874

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

    
891
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
892
    servers.connect(vm, network=net)
893
    return HttpResponse(status=202)
894

    
895

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

    
908
    attachment = args.get("attachment")
909
    if attachment is None:
910
        raise faults.BadRequest("Missing 'attachment' attribute.")
911
    try:
912
        nic_id = int(attachment)
913
    except (ValueError, TypeError):
914
        raise faults.BadRequest("Invalid 'attachment' attribute.")
915

    
916
    nic = util.get_nic(nic_id=nic_id)
917
    server_id = nic.machine_id
918
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
919

    
920
    servers.disconnect(vm, nic)
921

    
922
    return HttpResponse(status=202)
923

    
924

    
925
@server_action("addFloatingIp")
926
def add_floating_ip(request, vm, args):
927
    address = args.get("address")
928
    if address is None:
929
        raise faults.BadRequest("Missing 'address' attribute")
930

    
931
    userid = vm.userid
932
    floating_ip = util.get_floating_ip_by_address(userid, address,
933
                                                  for_update=True)
934
    servers.create_port(userid, floating_ip.network, machine=vm,
935
                        user_ipaddress=floating_ip)
936
    return HttpResponse(status=202)
937

    
938

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