Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (39.8 kB)

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

    
34
from django.conf import settings
35
from django.conf.urls import patterns
36

    
37
from django.db import transaction
38
from django.http import HttpResponse
39
from django.template.loader import render_to_string
40
from django.utils import simplejson as json
41

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

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

    
50
from logging import getLogger
51
log = getLogger(__name__)
52

    
53
urlpatterns = patterns(
54
    'synnefo.api.servers',
55
    (r'^(?:/|.json|.xml)?$', 'demux'),
56
    (r'^/detail(?:.json|.xml)?$', 'list_servers', {'detail': True}),
57
    (r'^/(\d+)(?:.json|.xml)?$', 'server_demux'),
58
    (r'^/(\d+)/action(?:.json|.xml)?$', 'demux_server_action'),
59
    (r'^/(\d+)/ips(?:.json|.xml)?$', 'list_addresses'),
60
    (r'^/(\d+)/ips/(.+?)(?:.json|.xml)?$', 'list_addresses_by_network'),
61
    (r'^/(\d+)/metadata(?:.json|.xml)?$', 'metadata_demux'),
62
    (r'^/(\d+)/metadata/(.+?)(?:.json|.xml)?$', 'metadata_item_demux'),
63
    (r'^/(\d+)/stats(?:.json|.xml)?$', 'server_stats'),
64
    (r'^/(\d+)/diagnostics(?:.json)?$', 'get_server_diagnostics'),
65
    (r'^/(\d+)/os-volume_attachments(?:.json)?$', 'demux_volumes'),
66
    (r'^/(\d+)/os-volume_attachments/(\d+)(?:.json)?$', 'demux_volumes_item'),
67
)
68

    
69
VOLUME_SOURCE_TYPES = [
70
    "snapshot",
71
    "image",
72
    "volume",
73
    "blank"
74
]
75

    
76

    
77
def demux(request):
78
    if request.method == 'GET':
79
        return list_servers(request)
80
    elif request.method == 'POST':
81
        return create_server(request)
82
    else:
83
        return api.api_method_not_allowed(request,
84
                                          allowed_methods=['GET', 'POST'])
85

    
86

    
87
def server_demux(request, server_id):
88
    if request.method == 'GET':
89
        return get_server_details(request, server_id)
90
    elif request.method == 'PUT':
91
        return update_server_name(request, server_id)
92
    elif request.method == 'DELETE':
93
        return delete_server(request, server_id)
94
    else:
95
        return api.api_method_not_allowed(request,
96
                                          allowed_methods=['GET',
97
                                                           'PUT',
98
                                                           'DELETE'])
99

    
100

    
101
def metadata_demux(request, server_id):
102
    if request.method == 'GET':
103
        return list_metadata(request, server_id)
104
    elif request.method == 'POST':
105
        return update_metadata(request, server_id)
106
    else:
107
        return api.api_method_not_allowed(request,
108
                                          allowed_methods=['GET', 'POST'])
109

    
110

    
111
def metadata_item_demux(request, server_id, key):
112
    if request.method == 'GET':
113
        return get_metadata_item(request, server_id, key)
114
    elif request.method == 'PUT':
115
        return create_metadata_item(request, server_id, key)
116
    elif request.method == 'DELETE':
117
        return delete_metadata_item(request, server_id, key)
118
    else:
119
        return api.api_method_not_allowed(request,
120
                                          allowed_methods=['GET',
121
                                                           'PUT',
122
                                                           'DELETE'])
123

    
124

    
125
def demux_volumes(request, server_id):
126
    if request.method == 'GET':
127
        return get_volumes(request, server_id)
128
    elif request.method == 'POST':
129
        return attach_volume(request, server_id)
130
    else:
131
        return api.api_method_not_allowed(request,
132
                                          allowed_methods=['GET', 'POST'])
133

    
134

    
135
def demux_volumes_item(request, server_id, volume_id):
136
    if request.method == 'GET':
137
        return get_volume_info(request, server_id, volume_id)
138
    elif request.method == 'DELETE':
139
        return detach_volume(request, server_id, volume_id)
140
    else:
141
        return api.api_method_not_allowed(request,
142
                                          allowed_methods=['GET', 'DELETE'])
143

    
144

    
145
def nic_to_attachments(nic):
146
    """Convert a NIC object to 'attachments attribute.
147

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

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

155
    """
156
    d = {'id': nic.id,
157
         'network_id': str(nic.network_id),
158
         'mac_address': nic.mac,
159
         'ipv4': '',
160
         'ipv6': ''}
161

    
162
    if nic.firewall_profile:
163
        d['firewallProfile'] = nic.firewall_profile
164

    
165
    for ip in nic.ips.all():
166
        if not ip.deleted:
167
            ip_type = "floating" if ip.floating_ip else "fixed"
168
            if ip.ipversion == 4:
169
                d["ipv4"] = ip.address
170
                d["OS-EXT-IPS:type"] = ip_type
171
            else:
172
                d["ipv6"] = ip.address
173
                d["OS-EXT-IPS:type"] = ip_type
174
    return d
175

    
176

    
177
def attachments_to_addresses(attachments):
178
    """Convert 'attachments' attribute to 'addresses'.
179

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

183
    """
184
    addresses = {}
185
    for nic in attachments:
186
        net_addrs = []
187
        if nic["ipv4"]:
188
            net_addrs.append({"version": 4,
189
                              "addr": nic["ipv4"],
190
                              "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
191
        if nic["ipv6"]:
192
            net_addrs.append({"version": 6,
193
                              "addr": nic["ipv6"],
194
                              "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
195
        addresses[nic["network_id"]] = net_addrs
196
    return addresses
197

    
198

    
199
def vm_to_dict(vm, detail=False):
200
    d = dict(id=vm.id, name=vm.name)
201
    d['links'] = util.vm_to_links(vm.id)
202
    if detail:
203
        d['user_id'] = vm.userid
204
        d['tenant_id'] = vm.userid
205
        d['status'] = logic_utils.get_rsapi_state(vm)
206
        d['SNF:task_state'] = logic_utils.get_task_state(vm)
207
        d['progress'] = 100 if d['status'] == 'ACTIVE' else vm.buildpercentage
208
        d['hostId'] = vm.hostid
209
        d['updated'] = utils.isoformat(vm.updated)
210
        d['created'] = utils.isoformat(vm.created)
211
        d['flavor'] = {"id": vm.flavor.id,
212
                       "links": util.flavor_to_links(vm.flavor.id)}
213
        d['image'] = {"id": vm.imageid,
214
                      "links": util.image_to_links(vm.imageid)}
215
        d['suspended'] = vm.suspended
216

    
217
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
218
        d['metadata'] = metadata
219

    
220
        nics = vm.nics.all()
221
        active_nics = filter(lambda nic: nic.state == "ACTIVE", nics)
222
        active_nics.sort(key=lambda nic: nic.id)
223
        attachments = map(nic_to_attachments, active_nics)
224
        d['attachments'] = attachments
225
        d['addresses'] = attachments_to_addresses(attachments)
226

    
227
        d['volumes'] = [v.id for v in vm.volumes.order_by('id')]
228

    
229
        # include the latest vm diagnostic, if set
230
        diagnostic = vm.get_last_diagnostic()
231
        if diagnostic:
232
            d['diagnostics'] = diagnostics_to_dict([diagnostic])
233
        else:
234
            d['diagnostics'] = []
235
        # Fixed
236
        d["security_groups"] = [{"name": "default"}]
237
        d["key_name"] = None
238
        d["config_drive"] = ""
239
        d["accessIPv4"] = ""
240
        d["accessIPv6"] = ""
241
        fqdn = get_server_fqdn(vm, active_nics)
242
        d["SNF:fqdn"] = fqdn
243
        d["SNF:port_forwarding"] = get_server_port_forwarding(vm, active_nics,
244
                                                              fqdn)
245
        d['deleted'] = vm.deleted
246
    return d
247

    
248

    
249
def get_server_public_ip(vm_nics, version=4):
250
    """Get the first public IP address of a server.
251

252
    NOTE: 'vm_nics' objects have prefetched the ips
253
    """
254
    for nic in vm_nics:
255
        for ip in nic.ips.all():
256
            if ip.ipversion == version and ip.public:
257
                return ip
258
    return None
259

    
260

    
261
def get_server_fqdn(vm, vm_nics):
262
    fqdn_setting = settings.CYCLADES_SERVERS_FQDN
263
    if fqdn_setting is None:
264
        return None
265
    elif isinstance(fqdn_setting, basestring):
266
        return fqdn_setting % {"id": vm.id}
267
    else:
268
        msg = ("Invalid setting: CYCLADES_SERVERS_FQDN."
269
               " Value must be a string.")
270
        raise faults.InternalServerError(msg)
271

    
272

    
273
def get_server_port_forwarding(vm, vm_nics, fqdn):
274
    """Create API 'port_forwarding' attribute from corresponding setting.
275

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

285
    NOTE: 'vm_nics' objects have prefetched the ips
286
    """
287
    port_forwarding = {}
288
    public_ip = get_server_public_ip(vm_nics)
289
    if public_ip is None:
290
        return port_forwarding
291
    for dport, to_dest in settings.CYCLADES_PORT_FORWARDING.items():
292
        if hasattr(to_dest, "__call__"):
293
            to_dest = to_dest(public_ip.address, vm.id, fqdn, vm.userid)
294
        msg = ("Invalid setting: CYCLADES_PORT_FOWARDING."
295
               " Value must be a tuple of two elements (host, port).")
296
        if not isinstance(to_dest, tuple) or len(to_dest) != 2:
297
                raise faults.InternalServerError(msg)
298
        else:
299
            try:
300
                host, port = to_dest
301
            except (TypeError, ValueError):
302
                raise faults.InternalServerError(msg)
303

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

    
307

    
308
def diagnostics_to_dict(diagnostics):
309
    """
310
    Extract api data from diagnostics QuerySet.
311
    """
312
    entries = list()
313

    
314
    for diagnostic in diagnostics:
315
        # format source date if set
316
        formatted_source_date = None
317
        if diagnostic.source_date:
318
            formatted_source_date = utils.isoformat(diagnostic.source_date)
319

    
320
        entry = {
321
            'source': diagnostic.source,
322
            'created': utils.isoformat(diagnostic.created),
323
            'message': diagnostic.message,
324
            'details': diagnostic.details,
325
            'level': diagnostic.level,
326
        }
327

    
328
        if formatted_source_date:
329
            entry['source_date'] = formatted_source_date
330

    
331
        entries.append(entry)
332

    
333
    return entries
334

    
335

    
336
def render_server(request, server, status=200):
337
    if request.serialization == 'xml':
338
        data = render_to_string('server.xml', {
339
            'server': server,
340
            'is_root': True})
341
    else:
342
        data = json.dumps({'server': server})
343
    return HttpResponse(data, status=status)
344

    
345

    
346
def render_diagnostics(request, diagnostics_dict, status=200):
347
    """
348
    Render diagnostics dictionary to json response.
349
    """
350
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
351

    
352

    
353
@api.api_method(http_method='GET', user_required=True, logger=log)
354
def get_server_diagnostics(request, server_id):
355
    """
356
    Virtual machine diagnostics api view.
357
    """
358
    log.debug('server_diagnostics %s', server_id)
359
    vm = util.get_vm(server_id, request.user_uniq)
360
    diagnostics = diagnostics_to_dict(vm.diagnostics.all())
361
    return render_diagnostics(request, diagnostics)
362

    
363

    
364
@api.api_method(http_method='GET', user_required=True, logger=log)
365
def list_servers(request, detail=False):
366
    # Normal Response Codes: 200, 203
367
    # Error Response Codes: computeFault (400, 500),
368
    #                       serviceUnavailable (503),
369
    #                       unauthorized (401),
370
    #                       badRequest (400),
371
    #                       overLimit (413)
372

    
373
    log.debug('list_servers detail=%s', detail)
374
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
375
    if detail:
376
        user_vms = user_vms.prefetch_related("nics__ips")
377

    
378
    user_vms = utils.filter_modified_since(request, objects=user_vms)
379

    
380
    servers_dict = [vm_to_dict(server, detail)
381
                    for server in user_vms.order_by('id')]
382

    
383
    if request.serialization == 'xml':
384
        data = render_to_string('list_servers.xml', {
385
            'servers': servers_dict,
386
            'detail': detail})
387
    else:
388
        data = json.dumps({'servers': servers_dict})
389

    
390
    return HttpResponse(data, status=200)
391

    
392

    
393
@api.api_method(http_method='POST', user_required=True, logger=log)
394
def create_server(request):
395
    # Normal Response Code: 202
396
    # Error Response Codes: computeFault (400, 500),
397
    #                       serviceUnavailable (503),
398
    #                       unauthorized (401),
399
    #                       badMediaType(415),
400
    #                       itemNotFound (404),
401
    #                       badRequest (400),
402
    #                       serverCapacityUnavailable (503),
403
    #                       overLimit (413)
404
    req = utils.get_request_dict(request)
405
    log.info('create_server %s', req)
406
    user_id = request.user_uniq
407

    
408
    try:
409
        server = req['server']
410
        name = server['name']
411
        metadata = server.get('metadata', {})
412
        assert isinstance(metadata, dict)
413
        image_id = server['imageRef']
414
        flavor_id = server['flavorRef']
415
        personality = server.get('personality', [])
416
        assert isinstance(personality, list)
417
        networks = server.get("networks")
418
        if networks is not None:
419
            assert isinstance(networks, list)
420
    except (KeyError, AssertionError):
421
        raise faults.BadRequest("Malformed request")
422

    
423
    volumes = None
424
    dev_map = server.get("block_device_mapping_v2")
425
    if dev_map is not None:
426
        volumes = parse_block_device_mapping(dev_map)
427

    
428
    # Verify that personalities are well-formed
429
    util.verify_personality(personality)
430
    # Get flavor (ensure it is active)
431
    flavor = util.get_flavor(flavor_id, include_deleted=False)
432
    if not flavor.allow_create:
433
        msg = ("It is not allowed to create a server from flavor with id '%d',"
434
               " see 'allow_create' flavor attribute")
435
        raise faults.Forbidden(msg % flavor.id)
436
    # Generate password
437
    password = util.random_password()
438

    
439
    vm = servers.create(user_id, name, password, flavor, image_id,
440
                        metadata=metadata, personality=personality,
441
                        networks=networks, volumes=volumes)
442

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

    
447
    response = render_server(request, server, status=202)
448

    
449
    return response
450

    
451

    
452
def parse_block_device_mapping(dev_map):
453
    """Parse 'block_device_mapping_v2' attribute"""
454
    if not isinstance(dev_map, list):
455
        raise faults.BadRequest("Block Device Mapping is Invalid")
456
    return [_parse_block_device(device) for device in dev_map]
457

    
458

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

    
464
    # Validate source type
465
    source_type = device.get("source_type")
466
    if source_type is None:
467
        raise faults.BadRequest("Block Device Mapping is Invalid: Invalid"
468
                                " source_type field")
469
    elif source_type not in VOLUME_SOURCE_TYPES:
470
        raise faults.BadRequest("Block Device Mapping is Invalid: source_type"
471
                                " must be on of %s"
472
                                % ", ".join(VOLUME_SOURCE_TYPES))
473

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

    
480
    # Validate volume size
481
    size = device.get("volume_size")
482
    if size is not None:
483
        try:
484
            size = int(size)
485
        except (TypeError, ValueError):
486
            raise faults.BadRequest("Block Device Mapping is Invalid: Invalid"
487
                                    " size field")
488

    
489
    # Validate 'delete_on_termination'
490
    delete_on_termination = device.get("delete_on_termination")
491
    if delete_on_termination is not None:
492
        if not isinstance(delete_on_termination, bool):
493
            raise faults.BadRequest("Block Device Mapping is Invalid: Invalid"
494
                                    " delete_on_termination field")
495
    else:
496
        if source_type == "volume":
497
            delete_on_termination = False
498
        else:
499
            delete_on_termination = True
500

    
501
    # Unused API Attributes
502
    # boot_index = device.get("boot_index")
503
    # destination_type = device.get("destination_type")
504

    
505
    return {"source_type": source_type,
506
            "source_uuid": uuid,
507
            "size": size,
508
            "delete_on_termination": delete_on_termination}
509

    
510

    
511
@api.api_method(http_method='GET', user_required=True, logger=log)
512
def get_server_details(request, server_id):
513
    # Normal Response Codes: 200, 203
514
    # Error Response Codes: computeFault (400, 500),
515
    #                       serviceUnavailable (503),
516
    #                       unauthorized (401),
517
    #                       badRequest (400),
518
    #                       itemNotFound (404),
519
    #                       overLimit (413)
520

    
521
    log.debug('get_server_details %s', server_id)
522
    vm = util.get_vm(server_id, request.user_uniq,
523
                     prefetch_related="nics__ips")
524
    server = vm_to_dict(vm, detail=True)
525
    return render_server(request, server)
526

    
527

    
528
@api.api_method(http_method='PUT', user_required=True, logger=log)
529
@transaction.commit_on_success
530
def update_server_name(request, server_id):
531
    # Normal Response Code: 204
532
    # Error Response Codes: computeFault (400, 500),
533
    #                       serviceUnavailable (503),
534
    #                       unauthorized (401),
535
    #                       badRequest (400),
536
    #                       badMediaType(415),
537
    #                       itemNotFound (404),
538
    #                       buildInProgress (409),
539
    #                       overLimit (413)
540

    
541
    req = utils.get_request_dict(request)
542
    log.info('update_server_name %s %s', server_id, req)
543

    
544
    req = utils.get_attribute(req, "server", attr_type=dict, required=True)
545
    name = utils.get_attribute(req, "name", attr_type=basestring,
546
                               required=True)
547

    
548
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
549
                     non_suspended=True)
550

    
551
    servers.rename(vm, new_name=name)
552

    
553
    return HttpResponse(status=204)
554

    
555

    
556
@api.api_method(http_method='DELETE', user_required=True, logger=log)
557
def delete_server(request, server_id):
558
    # Normal Response Codes: 204
559
    # Error Response Codes: computeFault (400, 500),
560
    #                       serviceUnavailable (503),
561
    #                       unauthorized (401),
562
    #                       itemNotFound (404),
563
    #                       unauthorized (401),
564
    #                       buildInProgress (409),
565
    #                       overLimit (413)
566

    
567
    log.info('delete_server %s', server_id)
568
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
569
                     non_suspended=True)
570
    vm = servers.destroy(vm)
571
    return HttpResponse(status=204)
572

    
573

    
574
# additional server actions
575
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
576

    
577

    
578
def key_to_action(key):
579
    """Map HTTP request key to a VM Action"""
580
    if key == "shutdown":
581
        return "STOP"
582
    if key == "delete":
583
        return "DESTROY"
584
    if key in ARBITRARY_ACTIONS:
585
        return None
586
    else:
587
        return key.upper()
588

    
589

    
590
@api.api_method(http_method='POST', user_required=True, logger=log)
591
@transaction.commit_on_success
592
def demux_server_action(request, server_id):
593
    req = utils.get_request_dict(request)
594
    log.debug('server_action %s %s', server_id, req)
595

    
596
    if not isinstance(req, dict) and len(req) != 1:
597
        raise faults.BadRequest("Malformed request")
598

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

    
603
    action = req.keys()[0]
604
    if not isinstance(action, basestring):
605
        raise faults.BadRequest("Malformed Request. Invalid action.")
606

    
607
    if key_to_action(action) not in [x[0] for x in VirtualMachine.ACTIONS]:
608
        if action not in ARBITRARY_ACTIONS:
609
            raise faults.BadRequest("Action %s not supported" % action)
610
    action_args = utils.get_attribute(req, action, required=True,
611
                                      attr_type=dict)
612

    
613
    return server_actions[action](request, vm, action_args)
614

    
615

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

    
625
    log.debug('list_addresses %s', server_id)
626
    vm = util.get_vm(server_id, request.user_uniq,
627
                     prefetch_related="nics__ips")
628
    attachments = [nic_to_attachments(nic)
629
                   for nic in vm.nics.filter(state="ACTIVE")]
630
    addresses = attachments_to_addresses(attachments)
631

    
632
    if request.serialization == 'xml':
633
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
634
    else:
635
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
636

    
637
    return HttpResponse(data, status=200)
638

    
639

    
640
@api.api_method(http_method='GET', user_required=True, logger=log)
641
def list_addresses_by_network(request, server_id, network_id):
642
    # Normal Response Codes: 200, 203
643
    # Error Response Codes: computeFault (400, 500),
644
    #                       serviceUnavailable (503),
645
    #                       unauthorized (401),
646
    #                       badRequest (400),
647
    #                       itemNotFound (404),
648
    #                       overLimit (413)
649

    
650
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
651
    machine = util.get_vm(server_id, request.user_uniq)
652
    network = util.get_network(network_id, request.user_uniq)
653
    nics = machine.nics.filter(network=network, state="ACTIVE")
654
    addresses = attachments_to_addresses(map(nic_to_attachments, nics))
655

    
656
    if request.serialization == 'xml':
657
        data = render_to_string('address.xml', {'addresses': addresses})
658
    else:
659
        data = json.dumps({'network': addresses})
660

    
661
    return HttpResponse(data, status=200)
662

    
663

    
664
@api.api_method(http_method='GET', user_required=True, logger=log)
665
def list_metadata(request, server_id):
666
    # Normal Response Codes: 200, 203
667
    # Error Response Codes: computeFault (400, 500),
668
    #                       serviceUnavailable (503),
669
    #                       unauthorized (401),
670
    #                       badRequest (400),
671
    #                       overLimit (413)
672

    
673
    log.debug('list_server_metadata %s', server_id)
674
    vm = util.get_vm(server_id, request.user_uniq)
675
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
676
    return util.render_metadata(request, metadata, use_values=False,
677
                                status=200)
678

    
679

    
680
@api.api_method(http_method='POST', user_required=True, logger=log)
681
@transaction.commit_on_success
682
def update_metadata(request, server_id):
683
    # Normal Response Code: 201
684
    # Error Response Codes: computeFault (400, 500),
685
    #                       serviceUnavailable (503),
686
    #                       unauthorized (401),
687
    #                       badRequest (400),
688
    #                       buildInProgress (409),
689
    #                       badMediaType(415),
690
    #                       overLimit (413)
691

    
692
    req = utils.get_request_dict(request)
693
    log.info('update_server_metadata %s %s', server_id, req)
694
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
695
    metadata = utils.get_attribute(req, "metadata", required=True,
696
                                   attr_type=dict)
697

    
698
    for key, val in metadata.items():
699
        if not isinstance(key, (basestring, int)) or\
700
           not isinstance(val, (basestring, int)):
701
            raise faults.BadRequest("Malformed Request. Invalid metadata.")
702
        meta, created = vm.metadata.get_or_create(meta_key=key)
703
        meta.meta_value = val
704
        meta.save()
705

    
706
    vm.save()
707
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
708
    return util.render_metadata(request, vm_meta, status=201)
709

    
710

    
711
@api.api_method(http_method='GET', user_required=True, logger=log)
712
def get_metadata_item(request, server_id, key):
713
    # Normal Response Codes: 200, 203
714
    # Error Response Codes: computeFault (400, 500),
715
    #                       serviceUnavailable (503),
716
    #                       unauthorized (401),
717
    #                       itemNotFound (404),
718
    #                       badRequest (400),
719
    #                       overLimit (413)
720

    
721
    log.debug('get_server_metadata_item %s %s', server_id, key)
722
    vm = util.get_vm(server_id, request.user_uniq)
723
    meta = util.get_vm_meta(vm, key)
724
    d = {meta.meta_key: meta.meta_value}
725
    return util.render_meta(request, d, status=200)
726

    
727

    
728
@api.api_method(http_method='PUT', user_required=True, logger=log)
729
@transaction.commit_on_success
730
def create_metadata_item(request, server_id, key):
731
    # Normal Response Code: 201
732
    # Error Response Codes: computeFault (400, 500),
733
    #                       serviceUnavailable (503),
734
    #                       unauthorized (401),
735
    #                       itemNotFound (404),
736
    #                       badRequest (400),
737
    #                       buildInProgress (409),
738
    #                       badMediaType(415),
739
    #                       overLimit (413)
740

    
741
    req = utils.get_request_dict(request)
742
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
743
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
744
    try:
745
        metadict = req['meta']
746
        assert isinstance(metadict, dict)
747
        assert len(metadict) == 1
748
        assert key in metadict
749
    except (KeyError, AssertionError):
750
        raise faults.BadRequest("Malformed request")
751

    
752
    meta, created = VirtualMachineMetadata.objects.get_or_create(
753
        meta_key=key,
754
        vm=vm)
755

    
756
    meta.meta_value = metadict[key]
757
    meta.save()
758
    vm.save()
759
    d = {meta.meta_key: meta.meta_value}
760
    return util.render_meta(request, d, status=201)
761

    
762

    
763
@api.api_method(http_method='DELETE', user_required=True, logger=log)
764
@transaction.commit_on_success
765
def delete_metadata_item(request, server_id, key):
766
    # Normal Response Code: 204
767
    # Error Response Codes: computeFault (400, 500),
768
    #                       serviceUnavailable (503),
769
    #                       unauthorized (401),
770
    #                       itemNotFound (404),
771
    #                       badRequest (400),
772
    #                       buildInProgress (409),
773
    #                       badMediaType(415),
774
    #                       overLimit (413),
775

    
776
    log.info('delete_server_metadata_item %s %s', server_id, key)
777
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
778
    meta = util.get_vm_meta(vm, key)
779
    meta.delete()
780
    vm.save()
781
    return HttpResponse(status=204)
782

    
783

    
784
@api.api_method(http_method='GET', user_required=True, logger=log)
785
def server_stats(request, server_id):
786
    # Normal Response Codes: 200
787
    # Error Response Codes: computeFault (400, 500),
788
    #                       serviceUnavailable (503),
789
    #                       unauthorized (401),
790
    #                       badRequest (400),
791
    #                       itemNotFound (404),
792
    #                       overLimit (413)
793

    
794
    log.debug('server_stats %s', server_id)
795
    vm = util.get_vm(server_id, request.user_uniq)
796
    secret = util.stats_encrypt(vm.backend_vm_id)
797

    
798
    stats = {
799
        'serverRef': vm.id,
800
        'refresh': settings.STATS_REFRESH_PERIOD,
801
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
802
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
803
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
804
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
805

    
806
    if request.serialization == 'xml':
807
        data = render_to_string('server_stats.xml', stats)
808
    else:
809
        data = json.dumps({'stats': stats})
810

    
811
    return HttpResponse(data, status=200)
812

    
813

    
814
# ACTIONS
815

    
816

    
817
server_actions = {}
818
network_actions = {}
819

    
820

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

    
826
    def decorator(func):
827
        server_actions[name] = func
828
        return func
829
    return decorator
830

    
831

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

    
837
    def decorator(func):
838
        network_actions[name] = func
839
        return func
840
    return decorator
841

    
842

    
843
@server_action('start')
844
def start(request, vm, args):
845
    # Normal Response Code: 202
846
    # Error Response Codes: serviceUnavailable (503),
847
    #                       itemNotFound (404)
848
    vm = servers.start(vm)
849
    return HttpResponse(status=202)
850

    
851

    
852
@server_action('shutdown')
853
def shutdown(request, vm, args):
854
    # Normal Response Code: 202
855
    # Error Response Codes: serviceUnavailable (503),
856
    #                       itemNotFound (404)
857
    vm = servers.stop(vm)
858
    return HttpResponse(status=202)
859

    
860

    
861
@server_action('reboot')
862
def reboot(request, vm, args):
863
    # Normal Response Code: 202
864
    # Error Response Codes: computeFault (400, 500),
865
    #                       serviceUnavailable (503),
866
    #                       unauthorized (401),
867
    #                       badRequest (400),
868
    #                       badMediaType(415),
869
    #                       itemNotFound (404),
870
    #                       buildInProgress (409),
871
    #                       overLimit (413)
872

    
873
    reboot_type = args.get("type", "SOFT")
874
    if reboot_type not in ["SOFT", "HARD"]:
875
        raise faults.BadRequest("Invalid 'type' attribute.")
876
    vm = servers.reboot(vm, reboot_type=reboot_type)
877
    return HttpResponse(status=202)
878

    
879

    
880
@server_action('firewallProfile')
881
def set_firewall_profile(request, vm, args):
882
    # Normal Response Code: 200
883
    # Error Response Codes: computeFault (400, 500),
884
    #                       serviceUnavailable (503),
885
    #                       unauthorized (401),
886
    #                       badRequest (400),
887
    #                       badMediaType(415),
888
    #                       itemNotFound (404),
889
    #                       buildInProgress (409),
890
    #                       overLimit (413)
891
    profile = args.get("profile")
892
    if profile is None:
893
        raise faults.BadRequest("Missing 'profile' attribute")
894
    nic_id = args.get("nic")
895
    if nic_id is None:
896
        raise faults.BadRequest("Missing 'nic' attribute")
897
    nic = util.get_vm_nic(vm, nic_id)
898
    servers.set_firewall_profile(vm, profile=profile, nic=nic)
899
    return HttpResponse(status=202)
900

    
901

    
902
@server_action('resize')
903
def resize(request, vm, args):
904
    # Normal Response Code: 202
905
    # Error Response Codes: computeFault (400, 500),
906
    #                       serviceUnavailable (503),
907
    #                       unauthorized (401),
908
    #                       badRequest (400),
909
    #                       badMediaType(415),
910
    #                       itemNotFound (404),
911
    #                       buildInProgress (409),
912
    #                       serverCapacityUnavailable (503),
913
    #                       overLimit (413),
914
    #                       resizeNotAllowed (403)
915
    flavorRef = args.get("flavorRef")
916
    if flavorRef is None:
917
        raise faults.BadRequest("Missing 'flavorRef' attribute.")
918
    flavor = util.get_flavor(flavor_id=flavorRef, include_deleted=False)
919
    servers.resize(vm, flavor=flavor)
920
    return HttpResponse(status=202)
921

    
922

    
923
@server_action('console')
924
def get_console(request, vm, args):
925
    # Normal Response Code: 200
926
    # Error Response Codes: computeFault (400, 500),
927
    #                       serviceUnavailable (503),
928
    #                       unauthorized (401),
929
    #                       badRequest (400),
930
    #                       badMediaType(415),
931
    #                       itemNotFound (404),
932
    #                       buildInProgress (409),
933
    #                       overLimit (413)
934

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

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

    
944
    if request.serialization == 'xml':
945
        mimetype = 'application/xml'
946
        data = render_to_string('console.xml', {'console': console_info})
947
    else:
948
        mimetype = 'application/json'
949
        data = json.dumps({'console': console_info})
950

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

    
953

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

    
958

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

    
963

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

    
968

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

    
973

    
974
@network_action('add')
975
@transaction.commit_on_success
976
def add(request, net, args):
977
    # Normal Response Code: 202
978
    # Error Response Codes: computeFault (400, 500),
979
    #                       serviceUnavailable (503),
980
    #                       unauthorized (401),
981
    #                       badRequest (400),
982
    #                       buildInProgress (409),
983
    #                       badMediaType(415),
984
    #                       itemNotFound (404),
985
    #                       overLimit (413)
986
    server_id = args.get('serverRef', None)
987
    if not server_id:
988
        raise faults.BadRequest('Malformed Request.')
989

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

    
994

    
995
@network_action('remove')
996
@transaction.commit_on_success
997
def remove(request, net, args):
998
    # Normal Response Code: 202
999
    # Error Response Codes: computeFault (400, 500),
1000
    #                       serviceUnavailable (503),
1001
    #                       unauthorized (401),
1002
    #                       badRequest (400),
1003
    #                       badMediaType(415),
1004
    #                       itemNotFound (404),
1005
    #                       overLimit (413)
1006

    
1007
    attachment = args.get("attachment")
1008
    if attachment is None:
1009
        raise faults.BadRequest("Missing 'attachment' attribute.")
1010
    try:
1011
        nic_id = int(attachment)
1012
    except (ValueError, TypeError):
1013
        raise faults.BadRequest("Invalid 'attachment' attribute.")
1014

    
1015
    nic = util.get_nic(nic_id=nic_id)
1016
    server_id = nic.machine_id
1017
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
1018

    
1019
    servers.disconnect(vm, nic)
1020

    
1021
    return HttpResponse(status=202)
1022

    
1023

    
1024
@server_action("addFloatingIp")
1025
def add_floating_ip(request, vm, args):
1026
    address = args.get("address")
1027
    if address is None:
1028
        raise faults.BadRequest("Missing 'address' attribute")
1029

    
1030
    userid = vm.userid
1031
    floating_ip = util.get_floating_ip_by_address(userid, address,
1032
                                                  for_update=True)
1033
    servers.create_port(userid, floating_ip.network, machine=vm,
1034
                        user_ipaddress=floating_ip)
1035
    return HttpResponse(status=202)
1036

    
1037

    
1038
@server_action("removeFloatingIp")
1039
def remove_floating_ip(request, vm, args):
1040
    address = args.get("address")
1041
    if address is None:
1042
        raise faults.BadRequest("Missing 'address' attribute")
1043
    floating_ip = util.get_floating_ip_by_address(vm.userid, address,
1044
                                                  for_update=True)
1045
    if floating_ip.nic is None:
1046
        raise faults.BadRequest("Floating IP %s not attached to instance"
1047
                                % address)
1048
    servers.delete_port(floating_ip.nic)
1049
    return HttpResponse(status=202)
1050

    
1051

    
1052
def volume_to_attachment(volume):
1053
    return {"id": volume.id,
1054
            "volumeId": volume.id,
1055
            "serverId": volume.machine_id,
1056
            "device": ""}  # TODO: What device to return?
1057

    
1058

    
1059
@api.api_method(http_method='GET', user_required=True, logger=log)
1060
def get_volumes(request, server_id):
1061
    log.debug("get_volumes server_id %s", server_id)
1062
    vm = util.get_vm(server_id, request.user_uniq, for_update=False)
1063

    
1064
    # TODO: Filter attachments!!
1065
    volumes = vm.volumes.filter(deleted=False).order_by("id")
1066
    attachments = [volume_to_attachment(v) for v in volumes]
1067

    
1068
    data = json.dumps({'volumeAttachments': attachments})
1069
    return HttpResponse(data, status=200)
1070
    pass
1071

    
1072

    
1073
@api.api_method(http_method='GET', user_required=True, logger=log)
1074
def get_volume_info(request, server_id, volume_id):
1075
    log.debug("get_volume_info server_id %s volume_id", server_id, volume_id)
1076
    user_id = request.user_uniq
1077
    vm = util.get_vm(server_id, user_id)
1078
    volume = get_volume(user_id, volume_id, for_update=False,
1079
                        exception=faults.BadRequest)
1080
    servers._check_attachment(vm, volume)
1081
    attachment = volume_to_attachment(volume)
1082
    data = json.dumps({'volumeAttachment': attachment})
1083
    return HttpResponse(data, status=200)
1084

    
1085

    
1086
@api.api_method(http_method='POST', user_required=True, logger=log)
1087
def attach_volume(request, server_id):
1088
    req = utils.get_request_dict(request)
1089
    log.debug("attach_volume server_id %s request", server_id, req)
1090
    user_id = request.user_uniq
1091
    vm = util.get_vm(server_id, user_id, for_update=True)
1092

    
1093
    attachment_dict = api.utils.get_attribute(req, "volumeAttachment",
1094
                                              required=True)
1095
    # Get volume
1096
    volume_id = api.utils.get_attribute(attachment_dict, "volumeId")
1097
    volume = get_volume(user_id, volume_id, for_update=True,
1098
                        exception=faults.BadRequest)
1099
    vm = server_attachments.attach_volume(vm, volume)
1100
    attachment = volume_to_attachment(volume)
1101
    data = json.dumps({'volumeAttachment': attachment})
1102

    
1103
    return HttpResponse(data, status=202)
1104

    
1105

    
1106
@api.api_method(http_method='DELETE', user_required=True, logger=log)
1107
def detach_volume(request, server_id, volume_id):
1108
    log.debug("detach_volume server_id %s volume_id", server_id, volume_id)
1109
    user_id = request.user_uniq
1110
    vm = util.get_vm(server_id, user_id)
1111
    volume = get_volume(user_id, volume_id, for_update=True,
1112
                        exception=faults.BadRequest)
1113
    vm = server_attachments.detach_volume(vm, volume)
1114
    # TODO: Check volume state, send job to detach volume
1115
    return HttpResponse(status=202)