Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (34.2 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.project
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_json_body(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
        project = server.get("project")
389
    except (KeyError, AssertionError):
390
        raise faults.BadRequest("Malformed request")
391

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

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

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

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

    
415
    return response
416

    
417

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

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

    
434

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

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

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

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

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

    
460
    return HttpResponse(status=204)
461

    
462

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

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

    
480

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

    
484

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

    
496

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

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

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

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

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

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

    
522

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

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

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

    
544
    return HttpResponse(data, status=200)
545

    
546

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

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

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

    
568
    return HttpResponse(data, status=200)
569

    
570

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

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

    
586

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

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

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

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

    
617

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

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

    
634

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

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

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

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

    
669

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

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

    
690

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

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

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

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

    
718
    return HttpResponse(data, status=200)
719

    
720

    
721
# ACTIONS
722

    
723

    
724
server_actions = {}
725
network_actions = {}
726

    
727

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

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

    
738

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

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

    
749

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

    
758

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

    
767

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

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

    
786

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

    
808

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

    
829

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

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

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

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

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

    
860

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

    
865

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

    
870

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

    
875

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

    
880

    
881
@server_action('reassign')
882
def reassign(request, vm, args):
883
    project = args.get("project")
884
    if project is None:
885
        raise faults.BadRequest("Missing 'project' attribute.")
886
    servers.reassign(vm, project)
887
    return HttpResponse(status=200)
888

    
889

    
890
@network_action('add')
891
@transaction.commit_on_success
892
def add(request, net, args):
893
    # Normal Response Code: 202
894
    # Error Response Codes: computeFault (400, 500),
895
    #                       serviceUnavailable (503),
896
    #                       unauthorized (401),
897
    #                       badRequest (400),
898
    #                       buildInProgress (409),
899
    #                       badMediaType(415),
900
    #                       itemNotFound (404),
901
    #                       overLimit (413)
902
    server_id = args.get('serverRef', None)
903
    if not server_id:
904
        raise faults.BadRequest('Malformed Request.')
905

    
906
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
907
    servers.connect(vm, network=net)
908
    return HttpResponse(status=202)
909

    
910

    
911
@network_action('remove')
912
@transaction.commit_on_success
913
def remove(request, net, args):
914
    # Normal Response Code: 202
915
    # Error Response Codes: computeFault (400, 500),
916
    #                       serviceUnavailable (503),
917
    #                       unauthorized (401),
918
    #                       badRequest (400),
919
    #                       badMediaType(415),
920
    #                       itemNotFound (404),
921
    #                       overLimit (413)
922

    
923
    attachment = args.get("attachment")
924
    if attachment is None:
925
        raise faults.BadRequest("Missing 'attachment' attribute.")
926
    try:
927
        nic_id = int(attachment)
928
    except (ValueError, TypeError):
929
        raise faults.BadRequest("Invalid 'attachment' attribute.")
930

    
931
    nic = util.get_nic(nic_id=nic_id)
932
    server_id = nic.machine_id
933
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
934

    
935
    servers.disconnect(vm, nic)
936

    
937
    return HttpResponse(status=202)
938

    
939

    
940
@server_action("addFloatingIp")
941
def add_floating_ip(request, vm, args):
942
    address = args.get("address")
943
    if address is None:
944
        raise faults.BadRequest("Missing 'address' attribute")
945

    
946
    userid = vm.userid
947
    floating_ip = util.get_floating_ip_by_address(userid, address,
948
                                                  for_update=True)
949
    servers.create_port(userid, floating_ip.network, machine=vm,
950
                        user_ipaddress=floating_ip)
951
    return HttpResponse(status=202)
952

    
953

    
954
@server_action("removeFloatingIp")
955
def remove_floating_ip(request, vm, args):
956
    address = args.get("address")
957
    if address is None:
958
        raise faults.BadRequest("Missing 'address' attribute")
959
    floating_ip = util.get_floating_ip_by_address(vm.userid, address,
960
                                                  for_update=True)
961
    if floating_ip.nic is None:
962
        raise faults.BadRequest("Floating IP %s not attached to instance"
963
                                % address)
964
    servers.delete_port(floating_ip.nic)
965
    return HttpResponse(status=202)