Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / servers.py @ 2aba7764

History | View | Annotate | Download (33.3 kB)

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

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

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

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

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

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

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

    
66

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

    
76

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

    
90

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

    
100

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

    
114

    
115
def nic_to_attachments(nic):
116
    """Convert a NIC object to 'attachments attribute.
117

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

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

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

    
132
    if nic.firewall_profile:
133
        d['firewallProfile'] = nic.firewall_profile
134

    
135
    for ip in nic.ips.all():
136
        if not ip.deleted:
137
            ip_type = "floating" if ip.floating_ip else "fixed"
138
            if ip.ipversion == 4:
139
                d["ipv4"] = ip.address
140
                d["OS-EXT-IPS:type"] = ip_type
141
            else:
142
                d["ipv6"] = ip.address
143
                d["OS-EXT-IPS:type"] = ip_type
144
    return d
145

    
146

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

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

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

    
168

    
169
def vm_to_dict(vm, detail=False):
170
    d = dict(id=vm.id, name=vm.name)
171
    d['links'] = util.vm_to_links(vm.id)
172
    if detail:
173
        d['user_id'] = vm.userid
174
        d['tenant_id'] = vm.userid
175
        d['status'] = logic_utils.get_rsapi_state(vm)
176
        d['SNF:task_state'] = logic_utils.get_task_state(vm)
177
        d['progress'] = 100 if d['status'] == 'ACTIVE' else vm.buildpercentage
178
        d['hostId'] = vm.hostid
179
        d['updated'] = utils.isoformat(vm.updated)
180
        d['created'] = utils.isoformat(vm.created)
181
        d['flavor'] = {"id": vm.flavor.id,
182
                       "links": util.flavor_to_links(vm.flavor.id)}
183
        d['image'] = {"id": vm.imageid,
184
                      "links": util.image_to_links(vm.imageid)}
185
        d['suspended'] = vm.suspended
186

    
187
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
188
        d['metadata'] = metadata
189

    
190
        nics = vm.nics.all()
191
        active_nics = filter(lambda nic: nic.state == "ACTIVE", nics)
192
        active_nics.sort(key=lambda nic: nic.id)
193
        attachments = map(nic_to_attachments, active_nics)
194
        d['attachments'] = attachments
195
        d['addresses'] = attachments_to_addresses(attachments)
196

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

    
215

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

219
    NOTE: 'vm_nics' objects have prefetched the ips
220
    """
221
    for version in [4, 6]:
222
        for nic in vm_nics:
223
            for ip in nic.ips.all():
224
                if ip.ipversion == version and ip.public:
225
                    return ip
226
    return None
227

    
228

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

    
240

    
241
def get_server_port_forwarding(vm, vm_nics, fqdn):
242
    """Create API 'port_forwarding' attribute from corresponding setting.
243

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

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

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

    
275

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

    
282
    for diagnostic in diagnostics:
283
        # format source date if set
284
        formatted_source_date = None
285
        if diagnostic.source_date:
286
            formatted_source_date = utils.isoformat(diagnostic.source_date)
287

    
288
        entry = {
289
            'source': diagnostic.source,
290
            'created': utils.isoformat(diagnostic.created),
291
            'message': diagnostic.message,
292
            'details': diagnostic.details,
293
            'level': diagnostic.level,
294
        }
295

    
296
        if formatted_source_date:
297
            entry['source_date'] = formatted_source_date
298

    
299
        entries.append(entry)
300

    
301
    return entries
302

    
303

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

    
313

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

    
320

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

    
331

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

    
341
    log.debug('list_servers detail=%s', detail)
342
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
343
    if detail:
344
        user_vms = user_vms.prefetch_related("nics__ips")
345

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

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

    
351
    if request.serialization == 'xml':
352
        data = render_to_string('list_servers.xml', {
353
            'servers': servers_dict,
354
            'detail': detail})
355
    else:
356
        data = json.dumps({'servers': servers_dict})
357

    
358
    return HttpResponse(data, status=200)
359

    
360

    
361
@api.api_method(http_method='POST', user_required=True, logger=log)
362
def create_server(request):
363
    # Normal Response Code: 202
364
    # Error Response Codes: computeFault (400, 500),
365
    #                       serviceUnavailable (503),
366
    #                       unauthorized (401),
367
    #                       badMediaType(415),
368
    #                       itemNotFound (404),
369
    #                       badRequest (400),
370
    #                       serverCapacityUnavailable (503),
371
    #                       overLimit (413)
372
    req = utils.get_request_dict(request)
373
    log.info('create_server %s', req)
374
    user_id = request.user_uniq
375

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

    
391
    # Verify that personalities are well-formed
392
    util.verify_personality(personality)
393
    # Get image information
394
    image = util.get_image_dict(image_id, user_id)
395
    # Get flavor (ensure it is active)
396
    flavor = util.get_flavor(flavor_id, include_deleted=False)
397
    # Generate password
398
    password = util.random_password()
399

    
400
    vm = servers.create(user_id, name, password, flavor, image,
401
                        metadata=metadata, personality=personality,
402
                        networks=networks)
403

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

    
408
    response = render_server(request, server, status=202)
409

    
410
    return response
411

    
412

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

    
423
    log.debug('get_server_details %s', server_id)
424
    vm = util.get_vm(server_id, request.user_uniq,
425
                     prefetch_related="nics__ips")
426
    server = vm_to_dict(vm, detail=True)
427
    return render_server(request, server)
428

    
429

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

    
443
    req = utils.get_request_dict(request)
444
    log.info('update_server_name %s %s', server_id, req)
445

    
446
    try:
447
        name = req['server']['name']
448
    except (TypeError, KeyError):
449
        raise faults.BadRequest("Malformed request")
450

    
451
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
452
                     non_suspended=True)
453

    
454
    servers.rename(vm, new_name=name)
455

    
456
    return HttpResponse(status=204)
457

    
458

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

    
470
    log.info('delete_server %s', server_id)
471
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
472
                     non_suspended=True)
473
    vm = servers.destroy(vm)
474
    return HttpResponse(status=204)
475

    
476

    
477
# additional server actions
478
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
479

    
480

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

    
492

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

    
499
    if len(req) != 1:
500
        raise faults.BadRequest("Malformed request")
501

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

    
506
    action = req.keys()[0]
507

    
508
    if key_to_action(action) not in [x[0] for x in VirtualMachine.ACTIONS]:
509
        if action not in ARBITRARY_ACTIONS:
510
            raise faults.BadRequest("Action %s not supported" % action)
511
    action_args = req[action]
512

    
513
    if not isinstance(action_args, dict):
514
        raise faults.BadRequest("Invalid argument")
515

    
516
    return server_actions[action](request, vm, action_args)
517

    
518

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

    
528
    log.debug('list_addresses %s', server_id)
529
    vm = util.get_vm(server_id, request.user_uniq, prefetch_related="nic__ips")
530
    attachments = [nic_to_attachments(nic)
531
                   for nic in vm.nics.filter(state="ACTIVE")]
532
    addresses = attachments_to_addresses(attachments)
533

    
534
    if request.serialization == 'xml':
535
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
536
    else:
537
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
538

    
539
    return HttpResponse(data, status=200)
540

    
541

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

    
552
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
553
    machine = util.get_vm(server_id, request.user_uniq)
554
    network = util.get_network(network_id, request.user_uniq)
555
    nics = machine.nics.filter(network=network, state="ACTIVE")
556
    addresses = attachments_to_addresses(map(nic_to_attachments, nics))
557

    
558
    if request.serialization == 'xml':
559
        data = render_to_string('address.xml', {'addresses': addresses})
560
    else:
561
        data = json.dumps({'network': addresses})
562

    
563
    return HttpResponse(data, status=200)
564

    
565

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

    
575
    log.debug('list_server_metadata %s', server_id)
576
    vm = util.get_vm(server_id, request.user_uniq)
577
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
578
    return util.render_metadata(request, metadata, use_values=False,
579
                                status=200)
580

    
581

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

    
594
    req = utils.get_request_dict(request)
595
    log.info('update_server_metadata %s %s', server_id, req)
596
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
597
    try:
598
        metadata = req['metadata']
599
        assert isinstance(metadata, dict)
600
    except (KeyError, AssertionError):
601
        raise faults.BadRequest("Malformed request")
602

    
603
    for key, val in metadata.items():
604
        meta, created = vm.metadata.get_or_create(meta_key=key)
605
        meta.meta_value = val
606
        meta.save()
607

    
608
    vm.save()
609
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
610
    return util.render_metadata(request, vm_meta, status=201)
611

    
612

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

    
623
    log.debug('get_server_metadata_item %s %s', server_id, key)
624
    vm = util.get_vm(server_id, request.user_uniq)
625
    meta = util.get_vm_meta(vm, key)
626
    d = {meta.meta_key: meta.meta_value}
627
    return util.render_meta(request, d, status=200)
628

    
629

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

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

    
654
    meta, created = VirtualMachineMetadata.objects.get_or_create(
655
        meta_key=key,
656
        vm=vm)
657

    
658
    meta.meta_value = metadict[key]
659
    meta.save()
660
    vm.save()
661
    d = {meta.meta_key: meta.meta_value}
662
    return util.render_meta(request, d, status=201)
663

    
664

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

    
678
    log.info('delete_server_metadata_item %s %s', server_id, key)
679
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
680
    meta = util.get_vm_meta(vm, key)
681
    meta.delete()
682
    vm.save()
683
    return HttpResponse(status=204)
684

    
685

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

    
696
    log.debug('server_stats %s', server_id)
697
    vm = util.get_vm(server_id, request.user_uniq)
698
    secret = util.stats_encrypt(vm.backend_vm_id)
699

    
700
    stats = {
701
        'serverRef': vm.id,
702
        'refresh': settings.STATS_REFRESH_PERIOD,
703
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
704
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
705
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
706
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
707

    
708
    if request.serialization == 'xml':
709
        data = render_to_string('server_stats.xml', stats)
710
    else:
711
        data = json.dumps({'stats': stats})
712

    
713
    return HttpResponse(data, status=200)
714

    
715

    
716
# ACTIONS
717

    
718

    
719
server_actions = {}
720
network_actions = {}
721

    
722

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

    
728
    def decorator(func):
729
        server_actions[name] = func
730
        return func
731
    return decorator
732

    
733

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

    
739
    def decorator(func):
740
        network_actions[name] = func
741
        return func
742
    return decorator
743

    
744

    
745
@server_action('start')
746
def start(request, vm, args):
747
    # Normal Response Code: 202
748
    # Error Response Codes: serviceUnavailable (503),
749
    #                       itemNotFound (404)
750
    vm = servers.start(vm)
751
    return HttpResponse(status=202)
752

    
753

    
754
@server_action('shutdown')
755
def shutdown(request, vm, args):
756
    # Normal Response Code: 202
757
    # Error Response Codes: serviceUnavailable (503),
758
    #                       itemNotFound (404)
759
    vm = servers.stop(vm)
760
    return HttpResponse(status=202)
761

    
762

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

    
775
    reboot_type = args.get("type", "SOFT")
776
    if reboot_type not in ["SOFT", "HARD"]:
777
        raise faults.BadRequest("Invalid 'type' attribute.")
778
    vm = servers.reboot(vm, reboot_type=reboot_type)
779
    return HttpResponse(status=202)
780

    
781

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

    
803

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

    
824

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

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

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

    
846
    if request.serialization == 'xml':
847
        mimetype = 'application/xml'
848
        data = render_to_string('console.xml', {'console': console_info})
849
    else:
850
        mimetype = 'application/json'
851
        data = json.dumps({'console': console_info})
852

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

    
855

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

    
860

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

    
865

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

    
870

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

    
875

    
876
@network_action('add')
877
@transaction.commit_on_success
878
def add(request, net, args):
879
    # Normal Response Code: 202
880
    # Error Response Codes: computeFault (400, 500),
881
    #                       serviceUnavailable (503),
882
    #                       unauthorized (401),
883
    #                       badRequest (400),
884
    #                       buildInProgress (409),
885
    #                       badMediaType(415),
886
    #                       itemNotFound (404),
887
    #                       overLimit (413)
888
    server_id = args.get('serverRef', None)
889
    if not server_id:
890
        raise faults.BadRequest('Malformed Request.')
891

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

    
896

    
897
@network_action('remove')
898
@transaction.commit_on_success
899
def remove(request, net, args):
900
    # Normal Response Code: 202
901
    # Error Response Codes: computeFault (400, 500),
902
    #                       serviceUnavailable (503),
903
    #                       unauthorized (401),
904
    #                       badRequest (400),
905
    #                       badMediaType(415),
906
    #                       itemNotFound (404),
907
    #                       overLimit (413)
908

    
909
    attachment = args.get("attachment")
910
    if attachment is None:
911
        raise faults.BadRequest("Missing 'attachment' attribute.")
912
    try:
913
        nic_id = int(attachment)
914
    except (ValueError, TypeError):
915
        raise faults.BadRequest("Invalid 'attachment' attribute.")
916

    
917
    nic = util.get_nic(nic_id=nic_id)
918
    server_id = nic.machine_id
919
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
920

    
921
    servers.disconnect(vm, nic)
922

    
923
    return HttpResponse(status=202)
924

    
925

    
926
@server_action("addFloatingIp")
927
def add_floating_ip(request, vm, args):
928
    address = args.get("address")
929
    if address is None:
930
        raise faults.BadRequest("Missing 'address' attribute")
931

    
932
    userid = vm.userid
933
    floating_ip = util.get_floating_ip_by_address(userid, address,
934
                                                  for_update=True)
935
    servers.create_port(userid, floating_ip.network, machine=vm,
936
                        user_ipaddress=floating_ip)
937
    return HttpResponse(status=202)
938

    
939

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