servers.py 33.9 KB
Newer Older
1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
3
4
5
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
6
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
7
8
9
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
10
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
11
12
13
14
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
15
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
16
17
18
19
20
21
22
23
24
25
26
27
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
28
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
29
30
31
32
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# 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
39
from django.http import HttpResponse
from django.template.loader import render_to_string
40
from django.utils import simplejson as json
41

42
43
from snf_django.lib import api
from snf_django.lib.api import faults, utils
44

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
49
from logging import getLogger
50
log = getLogger(__name__)
51

Christos Stavrakakis's avatar
Christos Stavrakakis committed
52
53
urlpatterns = patterns(
    'synnefo.api.servers',
54
55
56
    (r'^(?:/|.json|.xml)?$', 'demux'),
    (r'^/detail(?:.json|.xml)?$', 'list_servers', {'detail': True}),
    (r'^/(\d+)(?:.json|.xml)?$', 'server_demux'),
57
    (r'^/(\d+)/action(?:.json|.xml)?$', 'demux_server_action'),
Giorgos Verigakis's avatar
Giorgos Verigakis committed
58
59
    (r'^/(\d+)/ips(?:.json|.xml)?$', 'list_addresses'),
    (r'^/(\d+)/ips/(.+?)(?:.json|.xml)?$', 'list_addresses_by_network'),
60
61
    (r'^/(\d+)/metadata(?:.json|.xml)?$', 'metadata_demux'),
    (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
68
69
70
71
72
)


def demux(request):
    if request.method == 'GET':
        return list_servers(request)
    elif request.method == 'POST':
        return create_server(request)
    else:
73
74
        return api.api_method_not_allowed(request,
                                          allowed_methods=['GET', 'POST'])
75

76

77
78
79
80
81
82
83
84
def server_demux(request, server_id):
    if request.method == 'GET':
        return get_server_details(request, server_id)
    elif request.method == 'PUT':
        return update_server_name(request, server_id)
    elif request.method == 'DELETE':
        return delete_server(request, server_id)
    else:
85
86
87
88
        return api.api_method_not_allowed(request,
                                          allowed_methods=['GET',
                                                           'PUT',
                                                           'DELETE'])
Giorgos Verigakis's avatar
Giorgos Verigakis committed
89

90

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

100

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

114

115
116
117
118
119
120
121
122
123
124
125
def nic_to_attachments(nic):
    """Convert a NIC object to 'attachments attribute.

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

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

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

Giorgos Verigakis's avatar
Giorgos Verigakis committed
132
133
    if nic.firewall_profile:
        d['firewallProfile'] = nic.firewall_profile
134
135
136
137
138
139
140
141
142
143

    for ip in nic.ips.all():
        if not ip.deleted:
            ip_type = "floating" if ip.floating_ip else "fixed"
            if ip.ipversion == 4:
                d["ipv4"] = ip.address
                d["OS-EXT-IPS:type"] = ip_type
            else:
                d["ipv6"] = ip.address
                d["OS-EXT-IPS:type"] = ip_type
Giorgos Verigakis's avatar
Giorgos Verigakis committed
144
    return d
Giorgos Verigakis's avatar
Giorgos Verigakis committed
145

146

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

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

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


Giorgos Verigakis's avatar
Giorgos Verigakis committed
169
170
def vm_to_dict(vm, detail=False):
    d = dict(id=vm.id, name=vm.name)
171
    d['links'] = util.vm_to_links(vm.id)
172
    if detail:
173
174
        d['user_id'] = vm.userid
        d['tenant_id'] = vm.userid
175
176
177
        d['status'] = logic_utils.get_rsapi_state(vm)
        d['SNF:task_state'] = logic_utils.get_task_state(vm)
        d['progress'] = 100 if d['status'] == 'ACTIVE' else vm.buildpercentage
Giorgos Verigakis's avatar
Giorgos Verigakis committed
178
        d['hostId'] = vm.hostid
179
180
        d['updated'] = utils.isoformat(vm.updated)
        d['created'] = utils.isoformat(vm.created)
181
182
        d['flavor'] = {"id": vm.flavor_id,
                       "links": util.flavor_to_links(vm.flavor_id)}
183
184
        d['image'] = {"id": vm.imageid,
                      "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
191
192
193
        nics = vm.nics.all()
        active_nics = filter(lambda nic: nic.state == "ACTIVE", nics)
        active_nics.sort(key=lambda nic: nic.id)
        attachments = map(nic_to_attachments, active_nics)
194
        d['attachments'] = attachments
195
        d['addresses'] = attachments_to_addresses(attachments)
196
197
198
199
200

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

Giorgos Verigakis's avatar
Giorgos Verigakis committed
216

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

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


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


241
def get_server_port_forwarding(vm, vm_nics, fqdn):
242
243
244
245
246
247
248
249
250
251
252
    """Create API 'port_forwarding' attribute from corresponding setting.

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

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

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


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

    for diagnostic in diagnostics:
        # format source date if set
        formatted_source_date = None
        if diagnostic.source_date:
286
            formatted_source_date = utils.isoformat(diagnostic.source_date)
287
288
289

        entry = {
            'source': diagnostic.source,
290
            'created': utils.isoformat(diagnostic.created),
291
292
293
294
295
296
297
298
299
300
301
302
303
            'message': diagnostic.message,
            'details': diagnostic.details,
            'level': diagnostic.level,
        }

        if formatted_source_date:
            entry['source_date'] = formatted_source_date

        entries.append(entry)

    return entries


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

313

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


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


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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
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", "metadata")
345

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

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

Giorgos Verigakis's avatar
Giorgos Verigakis committed
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
364
365
366
367
368
369
370
371
    # Normal Response Code: 202
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badMediaType(415),
    #                       itemNotFound (404),
    #                       badRequest (400),
    #                       serverCapacityUnavailable (503),
    #                       overLimit (413)
372
373
    req = utils.get_request_dict(request)
    user_id = request.user_uniq
374
    log.info('create_server user: %s request: %s', user_id, req)
375

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

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

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

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

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

414
415
416
    return response


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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
427
    log.debug('get_server_details %s', server_id)
428
    vm = util.get_vm(server_id, request.user_uniq,
429
                     prefetch_related=["nics__ips", "metadata"])
Giorgos Verigakis's avatar
Giorgos Verigakis committed
430
431
    server = vm_to_dict(vm, detail=True)
    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
437
438
439
440
441
442
443
444
445
def update_server_name(request, server_id):
    # Normal Response Code: 204
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       badMediaType(415),
    #                       itemNotFound (404),
    #                       buildInProgress (409),
    #                       overLimit (413)
446

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

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

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

    servers.rename(vm, new_name=name)
458

459
460
    return HttpResponse(status=204)

461

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

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

479

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
480
481
482
# additional server actions
ARBITRARY_ACTIONS = ['console', 'firewallProfile']

Christos Stavrakakis's avatar
Christos Stavrakakis committed
483

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


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

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

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

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

513
514
515
    if key_to_action(action) not in [x[0] for x in VirtualMachine.ACTIONS]:
        if action not in ARBITRARY_ACTIONS:
            raise faults.BadRequest("Action %s not supported" % action)
516
517
    action_args = utils.get_attribute(req, action, required=True,
                                      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)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
523
524
525
526
527
528
529
def list_addresses(request, server_id):
    # Normal Response Codes: 200, 203
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       overLimit (413)
530

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

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

Giorgos Verigakis's avatar
Giorgos Verigakis committed
543
544
    return HttpResponse(data, status=200)

545

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
556
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
557
558
    machine = util.get_vm(server_id, request.user_uniq)
    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

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

Giorgos Verigakis's avatar
Giorgos Verigakis committed
567
    return HttpResponse(data, status=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
568

569

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
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
583
    return util.render_metadata(request, metadata, use_values=False,
                                status=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
584

585

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

598
    req = utils.get_request_dict(request)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
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
602
    metadata = utils.get_attribute(req, "metadata", required=True,
                                   attr_type=dict)
603

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

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

616

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
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)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
630
631
    d = {meta.meta_key: meta.meta_value}
    return util.render_meta(request, d, status=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
632

633

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

647
    req = utils.get_request_dict(request)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
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)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
650
651
652
653
654
655
    try:
        metadict = req['meta']
        assert isinstance(metadict, dict)
        assert len(metadict) == 1
        assert key in metadict
    except (KeyError, AssertionError):
656
        raise faults.BadRequest("Malformed request")
657

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

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

668

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
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)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
685
    meta.delete()
686
    vm.save()
Giorgos Verigakis's avatar
Giorgos Verigakis committed
687
    return HttpResponse(status=204)
688

689

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
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
705
706
    stats = {
        'serverRef': vm.id,
        'refresh': settings.STATS_REFRESH_PERIOD,
707
708
709
710
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
711

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

    return HttpResponse(data, status=200)
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778


# ACTIONS


server_actions = {}
network_actions = {}


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

    def decorator(func):
        server_actions[name] = func
        return func
    return decorator


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

    def decorator(func):
        network_actions[name] = func
        return func
    return decorator


@server_action('start')
def start(request, vm, args):
    # Normal Response Code: 202
    # Error Response Codes: serviceUnavailable (503),
    #                       itemNotFound (404)
    vm = servers.start(vm)
    return HttpResponse(status=202)


@server_action('shutdown')
def shutdown(request, vm, args):
    # Normal Response Code: 202
    # Error Response Codes: serviceUnavailable (503),
    #                       itemNotFound (404)
    vm = servers.stop(vm)
    return HttpResponse(status=202)


@server_action('reboot')
def reboot(request, vm, args):
    # Normal Response Code: 202
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       badMediaType(415),
    #                       itemNotFound (404),
    #                       buildInProgress (409),
    #                       overLimit (413)

779
780
    reboot_type = args.get("type", "SOFT")
    if reboot_type not in ["SOFT", "HARD"]:
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
        raise faults.BadRequest("Invalid 'type' attribute.")
    vm = servers.reboot(vm, reboot_type=reboot_type)
    return HttpResponse(status=202)


@server_action('firewallProfile')
def set_firewall_profile(request, vm, args):
    # Normal Response Code: 200
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       badMediaType(415),
    #                       itemNotFound (404),
    #                       buildInProgress (409),
    #                       overLimit (413)
    profile = args.get("profile")
    if profile is None:
        raise faults.BadRequest("Missing 'profile' attribute")
800
801
802
    nic_id = args.get("nic")
    if nic_id is None:
        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
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
    return HttpResponse(status=202)


@server_action('resize')
def resize(request, vm, args):
    # Normal Response Code: 202
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       badMediaType(415),
    #                       itemNotFound (404),
    #                       buildInProgress (409),
    #                       serverCapacityUnavailable (503),
    #                       overLimit (413),
    #                       resizeNotAllowed (403)
    flavorRef = args.get("flavorRef")
    if flavorRef is None:
        raise faults.BadRequest("Missing 'flavorRef' attribute.")
    flavor = util.get_flavor(flavor_id=flavorRef, include_deleted=False)
    servers.resize(vm, flavor=flavor)
    return HttpResponse(status=202)


@server_action('console')
def get_console(request, vm, args):
    # Normal Response Code: 200
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       badMediaType(415),
    #                       itemNotFound (404),
    #                       buildInProgress (409),
    #                       overLimit (413)

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

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

    if request.serialization == 'xml':
        mimetype = 'application/xml'
        data = render_to_string('console.xml', {'console': console_info})
    else:
        mimetype = 'application/json'
        data = json.dumps({'console': console_info})

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


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


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


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


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


@network_action('add')
@transaction.commit_on_success
def add(request, net, args):
    # Normal Response Code: 202
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       buildInProgress (409),
    #                       badMediaType(415),
    #                       itemNotFound (404),
    #                       overLimit (413)
    server_id = args.get('serverRef', None)
    if not server_id:
        raise faults.BadRequest('Malformed Request.')

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


@network_action('remove')
@transaction.commit_on_success
def remove(request, net, args):
    # Normal Response Code: 202
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       badMediaType(415),
    #                       itemNotFound (404),
    #                       overLimit (413)

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

921
922
    nic = util.get_nic(nic_id=nic_id)
    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
932
933
934
935
def add_floating_ip(request, vm, args):
    address = args.get("address")
    if address is None:
        raise faults.BadRequest("Missing 'address' attribute")

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


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