servers.py 44.8 KB
Newer Older
Vangelis Koukis's avatar
Vangelis Koukis committed
1
# Copyright (C) 2010-2014 GRNET S.A.
2
#
Vangelis Koukis's avatar
Vangelis Koukis committed
3 4 5 6
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
7
#
Vangelis Koukis's avatar
Vangelis Koukis committed
8 9 10 11
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
12
#
Vangelis Koukis's avatar
Vangelis Koukis committed
13 14
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

16
from django.conf import settings
17
from django.conf.urls import patterns
18

19
from synnefo.db import transaction
20 21
from django.http import HttpResponse
from django.template.loader import render_to_string
22
from django.utils import simplejson as json
23
from django.core.urlresolvers import reverse
24

25 26
from snf_django.lib import api
from snf_django.lib.api import faults, utils
27

28
from synnefo.api import util
29
from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata)
30 31
from synnefo.logic import servers, utils as logic_utils, server_attachments
from synnefo.volume.util import get_volume
32

Christos Stavrakakis's avatar
Christos Stavrakakis committed
33
from logging import getLogger
34
log = getLogger(__name__)
35

Christos Stavrakakis's avatar
Christos Stavrakakis committed
36 37
urlpatterns = patterns(
    'synnefo.api.servers',
38 39 40
    (r'^(?:/|.json|.xml)?$', 'demux'),
    (r'^/detail(?:.json|.xml)?$', 'list_servers', {'detail': True}),
    (r'^/(\d+)(?:.json|.xml)?$', 'server_demux'),
41
    (r'^/(\d+)/action(?:.json|.xml)?$', 'demux_server_action'),
Giorgos Verigakis's avatar
Giorgos Verigakis committed
42 43
    (r'^/(\d+)/ips(?:.json|.xml)?$', 'list_addresses'),
    (r'^/(\d+)/ips/(.+?)(?:.json|.xml)?$', 'list_addresses_by_network'),
44 45
    (r'^/(\d+)/metadata(?:.json|.xml)?$', 'metadata_demux'),
    (r'^/(\d+)/metadata/(.+?)(?:.json|.xml)?$', 'metadata_item_demux'),
46
    (r'^/(\d+)/stats(?:.json|.xml)?$', 'server_stats'),
47
    (r'^/(\d+)/diagnostics(?:.json)?$', 'get_server_diagnostics'),
48 49
    (r'^/(\d+)/os-volume_attachments(?:.json)?$', 'demux_volumes'),
    (r'^/(\d+)/os-volume_attachments/(\d+)(?:.json)?$', 'demux_volumes_item'),
50 51
)

52 53 54 55 56 57
VOLUME_SOURCE_TYPES = [
    "image",
    "volume",
    "blank"
]

58 59 60 61 62
if settings.CYCLADES_SNAPSHOTS_ENABLED:
    # If snapshots are enabled, add 'snapshot' to the list of allowed sources
    # for a new block device.
    VOLUME_SOURCE_TYPES.append("snapshot")

63 64 65 66 67 68 69

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

73

74 75 76 77 78 79 80 81
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:
82 83 84 85
        return api.api_method_not_allowed(request,
                                          allowed_methods=['GET',
                                                           'PUT',
                                                           'DELETE'])
Giorgos Verigakis's avatar
Giorgos Verigakis committed
86

87

Giorgos Verigakis's avatar
Giorgos Verigakis committed
88 89 90 91 92 93
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:
94 95
        return api.api_method_not_allowed(request,
                                          allowed_methods=['GET', 'POST'])
Giorgos Verigakis's avatar
Giorgos Verigakis committed
96

97

Giorgos Verigakis's avatar
Giorgos Verigakis committed
98 99 100 101 102 103 104 105
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:
106 107 108 109
        return api.api_method_not_allowed(request,
                                          allowed_methods=['GET',
                                                           'PUT',
                                                           'DELETE'])
110

111

112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
def demux_volumes(request, server_id):
    if request.method == 'GET':
        return get_volumes(request, server_id)
    elif request.method == 'POST':
        return attach_volume(request, server_id)
    else:
        return api.api_method_not_allowed(request,
                                          allowed_methods=['GET', 'POST'])


def demux_volumes_item(request, server_id, volume_id):
    if request.method == 'GET':
        return get_volume_info(request, server_id, volume_id)
    elif request.method == 'DELETE':
        return detach_volume(request, server_id, volume_id)
    else:
        return api.api_method_not_allowed(request,
                                          allowed_methods=['GET', 'DELETE'])


132 133 134 135 136 137 138 139 140 141 142
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.

    """
143
    d = {'id': nic.id,
144
         'network_id': str(nic.network_id),
145
         'mac_address': nic.mac,
146 147
         'ipv4': '',
         'ipv6': ''}
148

Giorgos Verigakis's avatar
Giorgos Verigakis committed
149 150
    if nic.firewall_profile:
        d['firewallProfile'] = nic.firewall_profile
151 152 153 154 155 156 157 158 159 160

    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
161
    return d
Giorgos Verigakis's avatar
Giorgos Verigakis committed
162

163

164
def attachments_to_addresses(attachments):
165 166 167 168 169 170
    """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.

    """
171
    addresses = {}
172
    for nic in attachments:
173
        net_addrs = []
174
        if nic["ipv4"]:
175 176 177
            net_addrs.append({"version": 4,
                              "addr": nic["ipv4"],
                              "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
178
        if nic["ipv6"]:
179 180 181 182
            net_addrs.append({"version": 6,
                              "addr": nic["ipv6"],
                              "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
        addresses[nic["network_id"]] = net_addrs
183 184 185
    return addresses


Giorgos Verigakis's avatar
Giorgos Verigakis committed
186 187
def vm_to_dict(vm, detail=False):
    d = dict(id=vm.id, name=vm.name)
188
    d['links'] = util.vm_to_links(vm.id)
189
    if detail:
190
        d['user_id'] = vm.userid
191
        d['tenant_id'] = vm.project
192 193 194
        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
195
        d['hostId'] = vm.hostid
196 197
        d['updated'] = utils.isoformat(vm.updated)
        d['created'] = utils.isoformat(vm.created)
198 199
        d['flavor'] = {"id": vm.flavor_id,
                       "links": util.flavor_to_links(vm.flavor_id)}
200 201
        d['image'] = {"id": vm.imageid,
                      "links": util.image_to_links(vm.imageid)}
202
        d['suspended'] = vm.suspended
203

204
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
205
        d['metadata'] = metadata
206

207 208 209 210
        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)
211
        d['attachments'] = attachments
212
        d['addresses'] = attachments_to_addresses(attachments)
213

214
        d['volumes'] = [v.id for v in vm.volumes.filter(deleted=False).order_by('id')]
215

216 217 218 219
        # include the latest vm diagnostic, if set
        diagnostic = vm.get_last_diagnostic()
        if diagnostic:
            d['diagnostics'] = diagnostics_to_dict([diagnostic])
220 221
        else:
            d['diagnostics'] = []
222 223 224 225 226 227
        # Fixed
        d["security_groups"] = [{"name": "default"}]
        d["key_name"] = None
        d["config_drive"] = ""
        d["accessIPv4"] = ""
        d["accessIPv6"] = ""
228
        fqdn = get_server_fqdn(vm, active_nics)
229
        d["SNF:fqdn"] = fqdn
230 231
        d["SNF:port_forwarding"] = get_server_port_forwarding(vm, active_nics,
                                                              fqdn)
232
        d['deleted'] = vm.deleted
233 234
    return d

Giorgos Verigakis's avatar
Giorgos Verigakis committed
235

236 237 238 239 240
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
    """
241 242
    for nic in vm_nics:
        for ip in nic.ips.all():
243
            if nic.public and ip.ipversion == version:
244
                return ip
245 246 247 248
    return None


def get_server_fqdn(vm, vm_nics):
249 250
    fqdn_setting = settings.CYCLADES_SERVERS_FQDN
    if fqdn_setting is None:
251
        return None
252 253 254 255 256 257 258 259
    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)


260
def get_server_port_forwarding(vm, vm_nics, fqdn):
261 262 263 264 265 266 267 268 269 270 271
    """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

272
    NOTE: 'vm_nics' objects have prefetched the ips
273 274
    """
    port_forwarding = {}
275 276 277
    public_ip = get_server_public_ip(vm_nics)
    if public_ip is None:
        return port_forwarding
278 279
    for dport, to_dest in settings.CYCLADES_PORT_FORWARDING.items():
        if hasattr(to_dest, "__call__"):
280
            to_dest = to_dest(public_ip.address, vm.id, fqdn, vm.userid)
281 282 283 284 285 286 287 288 289 290 291 292 293 294
        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


295 296 297 298 299 300 301 302 303 304
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:
305
            formatted_source_date = utils.isoformat(diagnostic.source_date)
306 307 308

        entry = {
            'source': diagnostic.source,
309
            'created': utils.isoformat(diagnostic.created),
310 311 312 313 314 315 316 317 318 319 320 321 322
            '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
323 324
def render_server(request, server, status=200):
    if request.serialization == 'xml':
325 326 327
        data = render_to_string('server.xml', {
            'server': server,
            'is_root': True})
328
    else:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
329
        data = json.dumps({'server': server})
330
    return HttpResponse(data, status=status)
331

332

333 334 335 336 337 338 339
def render_diagnostics(request, diagnostics_dict, status=200):
    """
    Render diagnostics dictionary to json response.
    """
    return HttpResponse(json.dumps(diagnostics_dict), status=status)


340
@api.api_method(http_method='GET', user_required=True, logger=log)
341 342 343 344 345 346 347 348 349 350
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)


351
@api.api_method(http_method='GET', user_required=True, logger=log)
352 353 354 355 356 357 358
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)
359

Christos Stavrakakis's avatar
Christos Stavrakakis committed
360
    log.debug('list_servers detail=%s', detail)
361
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
362
    if detail:
363
        user_vms = user_vms.prefetch_related("nics__ips", "metadata")
364

365
    user_vms = utils.filter_modified_since(request, objects=user_vms)
366

367 368
    servers_dict = [vm_to_dict(server, detail)
                    for server in user_vms.order_by('id')]
369

Giorgos Verigakis's avatar
Giorgos Verigakis committed
370
    if request.serialization == 'xml':
371
        data = render_to_string('list_servers.xml', {
372
            'servers': servers_dict,
373
            'detail': detail})
374
    else:
375
        data = json.dumps({'servers': servers_dict})
376

377
    return HttpResponse(data, status=200)
378

379

380
@api.api_method(http_method='POST', user_required=True, logger=log)
381
def create_server(request):
382 383 384 385 386 387 388 389 390
    # Normal Response Code: 202
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badMediaType(415),
    #                       itemNotFound (404),
    #                       badRequest (400),
    #                       serverCapacityUnavailable (503),
    #                       overLimit (413)
391
    req = utils.get_json_body(request)
392
    user_id = request.user_uniq
393
    log.info('create_server user: %s request: %s', user_id, req)
394

395
    try:
396 397 398 399 400 401 402 403
        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)
404 405 406
        networks = server.get("networks")
        if networks is not None:
            assert isinstance(networks, list)
407
        project = server.get("project")
408 409 410
    except (KeyError, AssertionError):
        raise faults.BadRequest("Malformed request")

411 412 413 414 415
    volumes = None
    dev_map = server.get("block_device_mapping_v2")
    if dev_map is not None:
        volumes = parse_block_device_mapping(dev_map)

416 417 418 419
    # Verify that personalities are well-formed
    util.verify_personality(personality)
    # Get flavor (ensure it is active)
    flavor = util.get_flavor(flavor_id, include_deleted=False)
420 421 422 423
    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)
424 425 426
    # Generate password
    password = util.random_password()

427
    vm = servers.create(user_id, name, password, flavor, image_id,
428
                        metadata=metadata, personality=personality,
429
                        project=project, networks=networks, volumes=volumes)
430 431 432 433 434 435

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

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

437 438 439
    return response


440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498
def parse_block_device_mapping(dev_map):
    """Parse 'block_device_mapping_v2' attribute"""
    if not isinstance(dev_map, list):
        raise faults.BadRequest("Block Device Mapping is Invalid")
    return [_parse_block_device(device) for device in dev_map]


def _parse_block_device(device):
    """Parse and validate a block device mapping"""
    if not isinstance(device, dict):
        raise faults.BadRequest("Block Device Mapping is Invalid")

    # Validate source type
    source_type = device.get("source_type")
    if source_type is None:
        raise faults.BadRequest("Block Device Mapping is Invalid: Invalid"
                                " source_type field")
    elif source_type not in VOLUME_SOURCE_TYPES:
        raise faults.BadRequest("Block Device Mapping is Invalid: source_type"
                                " must be on of %s"
                                % ", ".join(VOLUME_SOURCE_TYPES))

    # Validate source UUID
    uuid = device.get("uuid")
    if uuid is None and source_type != "blank":
        raise faults.BadRequest("Block Device Mapping is Invalid: uuid of"
                                " %s is missing" % source_type)

    # Validate volume size
    size = device.get("volume_size")
    if size is not None:
        try:
            size = int(size)
        except (TypeError, ValueError):
            raise faults.BadRequest("Block Device Mapping is Invalid: Invalid"
                                    " size field")

    # Validate 'delete_on_termination'
    delete_on_termination = device.get("delete_on_termination")
    if delete_on_termination is not None:
        if not isinstance(delete_on_termination, bool):
            raise faults.BadRequest("Block Device Mapping is Invalid: Invalid"
                                    " delete_on_termination field")
    else:
        if source_type == "volume":
            delete_on_termination = False
        else:
            delete_on_termination = True

    # Unused API Attributes
    # boot_index = device.get("boot_index")
    # destination_type = device.get("destination_type")

    return {"source_type": source_type,
            "source_uuid": uuid,
            "size": size,
            "delete_on_termination": delete_on_termination}


499
@api.api_method(http_method='GET', user_required=True, logger=log)
500
def get_server_details(request, server_id):
501 502 503 504 505 506 507
    # Normal Response Codes: 200, 203
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       itemNotFound (404),
    #                       overLimit (413)
508

Christos Stavrakakis's avatar
Christos Stavrakakis committed
509
    log.debug('get_server_details %s', server_id)
510
    vm = util.get_vm(server_id, request.user_uniq,
511
                     prefetch_related=["nics__ips", "metadata"])
Giorgos Verigakis's avatar
Giorgos Verigakis committed
512 513
    server = vm_to_dict(vm, detail=True)
    return render_server(request, server)
514

515

516
@api.api_method(http_method='PUT', user_required=True, logger=log)
517
@transaction.commit_on_success
518 519 520 521 522 523 524 525 526 527
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)
528

529
    req = utils.get_json_body(request)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
530
    log.info('update_server_name %s %s', server_id, req)
531

532 533 534
    req = utils.get_attribute(req, "server", attr_type=dict, required=True)
    name = utils.get_attribute(req, "name", attr_type=basestring,
                               required=True)
535

536
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
537
                     non_suspended=True, non_deleted=True)
538 539

    servers.rename(vm, new_name=name)
540

541 542
    return HttpResponse(status=204)

543

544
@api.api_method(http_method='DELETE', user_required=True, logger=log)
545
def delete_server(request, server_id):
546 547 548 549 550 551 552 553
    # Normal Response Codes: 204
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       itemNotFound (404),
    #                       unauthorized (401),
    #                       buildInProgress (409),
    #                       overLimit (413)
554

Christos Stavrakakis's avatar
Christos Stavrakakis committed
555
    log.info('delete_server %s', server_id)
556
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
557
                     non_suspended=True, non_deleted=True)
558
    vm = servers.destroy(vm)
559
    return HttpResponse(status=204)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
560

561

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
562
# additional server actions
563 564 565
ARBITRARY_ACTIONS = ['console', 'firewallProfile', 'reassign',
                     'os-getVNCConsole', 'os-getRDPConsole',
                     'os-getSPICEConsole']
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
566

Christos Stavrakakis's avatar
Christos Stavrakakis committed
567

568 569 570 571 572 573
def key_to_action(key):
    """Map HTTP request key to a VM Action"""
    if key == "shutdown":
        return "STOP"
    if key == "delete":
        return "DESTROY"
574
    if key in ARBITRARY_ACTIONS:
575 576 577 578 579
        return None
    else:
        return key.upper()


580 581 582
@api.api_method(http_method='POST', user_required=True, logger=log)
@transaction.commit_on_success
def demux_server_action(request, server_id):
583
    req = utils.get_json_body(request)
584
    log.debug('server_action %s %s', server_id, req)
585

586
    if not isinstance(req, dict) and len(req) != 1:
587 588 589 590 591
        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)
592

593 594 595 596 597
    try:
        action = req.keys()[0]
    except IndexError:
        raise faults.BadRequest("Malformed Request.")

598 599
    if not isinstance(action, basestring):
        raise faults.BadRequest("Malformed Request. Invalid action.")
600

601 602 603
    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)
604 605
    action_args = utils.get_attribute(req, action, required=True,
                                      attr_type=dict)
606

607
    return server_actions[action](request, vm, action_args)
608 609


610
@api.api_method(http_method='GET', user_required=True, logger=log)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
611 612 613 614 615 616 617
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)
618

Christos Stavrakakis's avatar
Christos Stavrakakis committed
619
    log.debug('list_addresses %s', server_id)
620 621
    vm = util.get_vm(server_id, request.user_uniq,
                     prefetch_related="nics__ips")
622 623
    attachments = [nic_to_attachments(nic)
                   for nic in vm.nics.filter(state="ACTIVE")]
624
    addresses = attachments_to_addresses(attachments)
625

Giorgos Verigakis's avatar
Giorgos Verigakis committed
626
    if request.serialization == 'xml':
Giorgos Verigakis's avatar
Giorgos Verigakis committed
627 628
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
    else:
629
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
630

Giorgos Verigakis's avatar
Giorgos Verigakis committed
631 632
    return HttpResponse(data, status=200)

633

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
644
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
645 646
    machine = util.get_vm(server_id, request.user_uniq)
    network = util.get_network(network_id, request.user_uniq)
647
    nics = machine.nics.filter(network=network, state="ACTIVE")
648
    addresses = attachments_to_addresses(map(nic_to_attachments, nics))
649

Giorgos Verigakis's avatar
Giorgos Verigakis committed
650
    if request.serialization == 'xml':
651
        data = render_to_string('address.xml', {'addresses': addresses})
Giorgos Verigakis's avatar
Giorgos Verigakis committed
652
    else:
653
        data = json.dumps({'network': addresses})
654

Giorgos Verigakis's avatar
Giorgos Verigakis committed
655
    return HttpResponse(data, status=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
656

657

658
@api.api_method(http_method='GET', user_required=True, logger=log)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
659 660 661 662 663 664 665
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)
666

Christos Stavrakakis's avatar
Christos Stavrakakis committed
667
    log.debug('list_server_metadata %s', server_id)
668
    vm = util.get_vm(server_id, request.user_uniq)
669
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
670 671
    return util.render_metadata(request, metadata, use_values=False,
                                status=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
672

673

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

686
    req = utils.get_json_body(request)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
687
    log.info('update_server_metadata %s %s', server_id, req)
688 689
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True,
                     non_deleted=True)
690 691
    metadata = utils.get_attribute(req, "metadata", required=True,
                                   attr_type=dict)
692

693 694 695 696 697 698 699
    if len(metadata) + len(vm.metadata.all()) - \
       len(vm.metadata.all().filter(meta_key__in=metadata.keys())) > \
       settings.CYCLADES_VM_MAX_METADATA:
        raise faults.BadRequest("Virtual Machines cannot have more than %s "
                                "metadata items" %
                                settings.CYCLADES_VM_MAX_METADATA)

700
    for key, val in metadata.items():
701 702 703 704 705 706 707
        if len(key) > VirtualMachineMetadata.KEY_LENGTH:
            raise faults.BadRequest("Malformed Request. Metadata key is too"
                                    " long")
        if len(val) > VirtualMachineMetadata.VALUE_LENGTH:
            raise faults.BadRequest("Malformed Request. Metadata value is too"
                                    " long")

708 709 710
        if not isinstance(key, (basestring, int)) or\
           not isinstance(val, (basestring, int)):
            raise faults.BadRequest("Malformed Request. Invalid metadata.")
711 712 713
        meta, created = vm.metadata.get_or_create(meta_key=key)
        meta.meta_value = val
        meta.save()
714

715 716 717
    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
718

719

720
@api.api_method(http_method='GET', user_required=True, logger=log)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
721 722 723 724 725 726 727 728
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)
729

Christos Stavrakakis's avatar
Christos Stavrakakis committed
730
    log.debug('get_server_metadata_item %s %s', server_id, key)
731
    vm = util.get_vm(server_id, request.user_uniq)
732
    meta = util.get_vm_meta(vm, key)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
733 734
    d = {meta.meta_key: meta.meta_value}
    return util.render_meta(request, d, status=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
735

736

737
@api.api_method(http_method='PUT', user_required=True, logger=log)
738
@transaction.commit_on_success
Giorgos Verigakis's avatar
Giorgos Verigakis committed
739 740 741 742 743 744 745 746 747 748
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)
749

750
    req = utils.get_json_body(request)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
751
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
752 753
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True,
                     non_deleted=True)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
754 755 756 757 758 759
    try:
        metadict = req['meta']
        assert isinstance(metadict, dict)
        assert len(metadict) == 1
        assert key in metadict
    except (KeyError, AssertionError):
760
        raise faults.BadRequest("Malformed request")
761

762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777
    value = metadict[key]

    # Check key, value length
    if len(key) > VirtualMachineMetadata.KEY_LENGTH:
        raise faults.BadRequest("Malformed Request. Metadata key is too long")
    if len(value) > VirtualMachineMetadata.VALUE_LENGTH:
        raise faults.BadRequest("Malformed Request. Metadata value is too"
                                " long")

    # Check number of metadata items
    if vm.metadata.exclude(meta_key=key).count() == \
       settings.CYCLADES_VM_MAX_METADATA:
        raise faults.BadRequest("Virtual Machines cannot have more than %s"
                                " metadata items" %
                                settings.CYCLADES_VM_MAX_METADATA)

778 779 780
    meta, created = VirtualMachineMetadata.objects.get_or_create(
        meta_key=key,
        vm=vm)
781

782
    meta.meta_value = value
Giorgos Verigakis's avatar
Giorgos Verigakis committed
783
    meta.save()
784
    vm.save()
Giorgos Verigakis's avatar
Giorgos Verigakis committed
785 786
    d = {meta.meta_key: meta.meta_value}
    return util.render_meta(request, d, status=201)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
787

788

789
@api.api_method(http_method='DELETE', user_required=True, logger=log)
790
@transaction.commit_on_success
Giorgos Verigakis's avatar
Giorgos Verigakis committed
791 792 793 794 795 796 797 798 799 800
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),
801

Christos Stavrakakis's avatar
Christos Stavrakakis committed
802
    log.info('delete_server_metadata_item %s %s', server_id, key)
803 804
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True,
                     non_deleted=True)
805
    meta = util.get_vm_meta(vm, key)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
806
    meta.delete()
807
    vm.save()
Giorgos Verigakis's avatar
Giorgos Verigakis committed
808
    return HttpResponse(status=204)
809

810

811
@api.api_method(http_method='GET', user_required=True, logger=log)
812 813 814 815 816 817 818 819
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)
820

Christos Stavrakakis's avatar
Christos Stavrakakis committed
821
    log.debug('server_stats %s', server_id)
822
    vm = util.get_vm(server_id, request.user_uniq)
823
    secret = util.stats_encrypt(vm.backend_vm_id)
824

825 826 827
    stats = {
        'serverRef': vm.id,
        'refresh': settings.STATS_REFRESH_PERIOD,
828 829 830 831
        '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}
832

833 834 835 836 837 838
    if request.serialization == 'xml':
        data = render_to_string('server_stats.xml', stats)
    else:
        data = json.dumps({'stats': stats})

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


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

900 901
    reboot_type = args.get("type", "SOFT")
    if reboot_type not in ["SOFT", "HARD"]:
902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920
        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")
921 922 923
    nic_id = args.get("nic")
    if nic_id is None:
        raise faults.BadRequest("Missing 'nic' attribute")
924
    nic = util.get_vm_nic(vm, nic_id)
925
    servers.set_firewall_profile(vm, profile=profile, nic=nic)
926 927 928 929 930