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 183 184
        d['flavor'] = {"id": vm.flavor.id,
                       "links": util.flavor_to_links(vm.flavor.id)}
        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 344
    if detail:
        user_vms = user_vms.prefetch_related("nics__ips")
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 374 375
    req = utils.get_request_dict(request)
    log.info('create_server %s', req)
    user_id = request.user_uniq

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
        project = server.get("project")
389 390 391 392 393 394 395 396 397
    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)
398 399 400 401
    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)
402 403 404
    # Generate password
    password = util.random_password()

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

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

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

415 416 417
    return response


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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
428
    log.debug('get_server_details %s', server_id)
429 430
    vm = util.get_vm(server_id, request.user_uniq,
                     prefetch_related="nics__ips")
Giorgos Verigakis's avatar
Giorgos Verigakis committed
431 432
    server = vm_to_dict(vm, detail=True)
    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 438 439 440 441 442 443 444 445 446
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)
447

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

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

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

    servers.rename(vm, new_name=name)
459

460 461
    return HttpResponse(status=204)

462

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

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

480

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
484

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


497 498 499 500 501
@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)
502

503
    if not isinstance(req, dict) and len(req) != 1:
504 505 506 507 508
        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)
509

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

514 515 516
    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)
517 518
    action_args = utils.get_attribute(req, action, required=True,
                                      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)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
524 525 526 527 528 529 530
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)
531

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

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

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

546

547
@api.api_method(http_method='GET', user_required=True, logger=log)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
548 549 550 551 552 553 554 555
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)
556

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

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

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

570

571
@api.api_method(http_method='GET', user_required=True, logger=log)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
572 573 574 575 576 577 578
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)
579

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

586

587
@api.api_method(http_method='POST', user_required=True, logger=log)
588
@transaction.commit_on_success
Giorgos Verigakis's avatar
Giorgos Verigakis committed
589 590 591 592 593 594 595 596 597
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)
598

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

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

613 614 615
    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
616

617

618
@api.api_method(http_method='GET', user_required=True, logger=log)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
619 620 621 622 623 624 625 626
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)
627

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

634

635
@api.api_method(http_method='PUT', user_required=True, logger=log)
636
@transaction.commit_on_success
Giorgos Verigakis's avatar
Giorgos Verigakis committed
637 638 639 640 641 642 643 644 645 646
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)
647

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

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

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

669

670
@api.api_method(http_method='DELETE', user_required=True, logger=log)
671
@transaction.commit_on_success
Giorgos Verigakis's avatar
Giorgos Verigakis committed
672 673 674 675 676 677 678 679 680 681
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),
682

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

690

691
@api.api_method(http_method='GET', user_required=True, logger=log)
692 693 694 695 696 697 698 699
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)
700

Christos Stavrakakis's avatar
Christos Stavrakakis committed
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 706 707
    stats = {
        'serverRef': vm.id,
        'refresh': settings.STATS_REFRESH_PERIOD,
708 709 710 711
        '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}
712

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

    return HttpResponse(data, status=200)
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 779


# 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)

780 781
    reboot_type = args.get("type", "SOFT")
    if reboot_type not in ["SOFT", "HARD"]:
782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800
        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")
801 802 803
    nic_id = args.get("nic")
    if nic_id is None:
        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 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 917
    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:
918
        nic_id = int(attachment)
919 920 921
    except (ValueError, TypeError):
        raise faults.BadRequest("Invalid 'attachment' attribute.")

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

926
    servers.disconnect(vm, nic)
927 928

    return HttpResponse(status=202)
929 930


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

937 938 939 940 941
    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)
942 943 944
    return HttpResponse(status=202)


945
@server_action("removeFloatingIp")
946 947 948 949
def remove_floating_ip(request, vm, args):
    address = args.get("address")
    if address is None:
        raise faults.BadRequest("Missing 'address' attribute")
950 951 952 953 954 955
    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)
956
    return HttpResponse(status=202)