Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (31.4 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.defaults import patterns
36
from django.db import transaction
37
from django.http import HttpResponse
38
from django.template.loader import render_to_string
39
from django.utils import simplejson as json
40

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

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

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

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

    
65

    
66
def demux(request):
67
    if request.method == 'GET':
68
        return list_servers(request)
69
    elif request.method == 'POST':
70
        return create_server(request)
71
    else:
72
        return api.api_method_not_allowed(request)
73

    
74

    
75
def server_demux(request, server_id):
76
    if request.method == 'GET':
77
        return get_server_details(request, server_id)
78
    elif request.method == 'PUT':
79
        return update_server_name(request, server_id)
80
    elif request.method == 'DELETE':
81
        return delete_server(request, server_id)
82
    else:
83
        return api.api_method_not_allowed(request)
84

    
85

    
86
def metadata_demux(request, server_id):
87
    if request.method == 'GET':
88
        return list_metadata(request, server_id)
89
    elif request.method == 'POST':
90
        return update_metadata(request, server_id)
91
    else:
92
        return api.api_method_not_allowed(request)
93

    
94

    
95
def metadata_item_demux(request, server_id, key):
96
    if request.method == 'GET':
97
        return get_metadata_item(request, server_id, key)
98
    elif request.method == 'PUT':
99
        return create_metadata_item(request, server_id, key)
100
    elif request.method == 'DELETE':
101
        return delete_metadata_item(request, server_id, key)
102
    else:
103
        return api.api_method_not_allowed(request)
104

    
105

    
106
def nic_to_dict(nic):
107
    ip_type = "floating" if nic.is_floating_ip else "fixed"
108
    d = {'id': util.construct_nic_id(nic),
109
         'network_id': str(nic.network.id),
110
         'mac_address': nic.mac,
111
         'ipv4': nic.ipv4,
112
         'ipv6': nic.ipv6,
113
         'OS-EXT-IPS:type': ip_type}
114

    
115
    if nic.firewall_profile:
116
        d['firewallProfile'] = nic.firewall_profile
117
    return d
118

    
119

    
120
def attachments_to_addresses(attachments):
121
    addresses = {}
122
    for nic in attachments:
123
        net_nics = []
124
        net_nics.append({"version": 4,
125
                         "addr": nic["ipv4"],
126
                         "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
127
        if nic["ipv6"]:
128
            net_nics.append({"version": 6,
129
                             "addr": nic["ipv6"],
130
                             "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
131
        addresses[nic["network_id"]] = net_nics
132
    return addresses
133

    
134

    
135
def vm_to_dict(vm, detail=False):
136
    d = dict(id=vm.id, name=vm.name)
137
    d['links'] = util.vm_to_links(vm.id)
138
    if detail:
139
        d['user_id'] = vm.userid
140
        d['tenant_id'] = vm.userid
141
        d['status'] = logic_utils.get_rsapi_state(vm)
142
        d['SNF:task_state'] = logic_utils.get_task_state(vm)
143
        d['progress'] = 100 if d['status'] == 'ACTIVE' else vm.buildpercentage
144
        d['hostId'] = vm.hostid
145
        d['updated'] = utils.isoformat(vm.updated)
146
        d['created'] = utils.isoformat(vm.created)
147
        d['flavor'] = {"id": vm.flavor.id,
148
                       "links": util.flavor_to_links(vm.flavor.id)}
149
        d['image'] = {"id": vm.imageid,
150
                      "links": util.image_to_links(vm.imageid)}
151
        d['suspended'] = vm.suspended
152

    
153
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
154
        d['metadata'] = metadata
155

    
156
        vm_nics = vm.nics.filter(state="ACTIVE").order_by("index")
157
        attachments = map(nic_to_dict, vm_nics)
158
        d['attachments'] = attachments
159
        d['addresses'] = attachments_to_addresses(attachments)
160

    
161
        # include the latest vm diagnostic, if set
162
        diagnostic = vm.get_last_diagnostic()
163
        if diagnostic:
164
            d['diagnostics'] = diagnostics_to_dict([diagnostic])
165
        else:
166
            d['diagnostics'] = []
167
        # Fixed
168
        d["security_groups"] = [{"name": "default"}]
169
        d["key_name"] = None
170
        d["config_drive"] = ""
171
        d["accessIPv4"] = ""
172
        d["accessIPv6"] = ""
173
        fqdn = get_server_fqdn(vm)
174
        d["SNF:fqdn"] = fqdn
175
        d["SNF:port_forwarding"] = get_server_port_forwarding(vm, fqdn)
176

    
177
    return d
178

    
179

    
180
def get_server_fqdn(vm):
181
    fqdn_setting = settings.CYCLADES_SERVERS_FQDN
182
    if fqdn_setting is None:
183
        public_nics = vm.nics.filter(network__public=True, state="ACTIVE")
184
        # Return the first public IPv4 address if exists
185
        ipv4_nics = public_nics.exclude(ipv4=None)
186
        if ipv4_nics:
187
            return ipv4_nics[0].ipv4
188
        # Else return the first public IPv6 address if exists
189
        ipv6_nics = public_nics.exclude(ipv6=None)
190
        if ipv6_nics:
191
            return ipv6_nics[0].ipv6
192
        return ""
193
    elif isinstance(fqdn_setting, basestring):
194
        return fqdn_setting % {"id": vm.id}
195
    else:
196
        msg = ("Invalid setting: CYCLADES_SERVERS_FQDN."
197
               " Value must be a string.")
198
        raise faults.InternalServerError(msg)
199

    
200

    
201
def get_server_port_forwarding(vm, fqdn):
202
    """Create API 'port_forwarding' attribute from corresponding setting.
203

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

213
    """
214
    port_forwarding = {}
215
    for dport, to_dest in settings.CYCLADES_PORT_FORWARDING.items():
216
        if hasattr(to_dest, "__call__"):
217
            public_nics = vm.nics.filter(network__public=True, state="ACTIVE")\
218
                                 .exclude(ipv4=None).order_by('index')
219
            if public_nics:
220
                vm_ipv4 = public_nics[0].ipv4
221
            else:
222
                vm_ipv4 = None
223
            to_dest = to_dest(vm_ipv4, vm.id, fqdn, vm.userid)
224
        msg = ("Invalid setting: CYCLADES_PORT_FOWARDING."
225
               " Value must be a tuple of two elements (host, port).")
226
        if to_dest is None:
227
            continue
228
        if not isinstance(to_dest, tuple) or len(to_dest) != 2:
229
                raise faults.InternalServerError(msg)
230
        else:
231
            try:
232
                host, port = to_dest
233
            except (TypeError, ValueError):
234
                raise faults.InternalServerError(msg)
235

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

    
239

    
240
def diagnostics_to_dict(diagnostics):
241
    """
242
    Extract api data from diagnostics QuerySet.
243
    """
244
    entries = list()
245

    
246
    for diagnostic in diagnostics:
247
        # format source date if set
248
        formatted_source_date = None
249
        if diagnostic.source_date:
250
            formatted_source_date = utils.isoformat(diagnostic.source_date)
251

    
252
        entry = {
253
            'source': diagnostic.source,
254
            'created': utils.isoformat(diagnostic.created),
255
            'message': diagnostic.message,
256
            'details': diagnostic.details,
257
            'level': diagnostic.level,
258
        }
259

    
260
        if formatted_source_date:
261
            entry['source_date'] = formatted_source_date
262

    
263
        entries.append(entry)
264

    
265
    return entries
266

    
267

    
268
def render_server(request, server, status=200):
269
    if request.serialization == 'xml':
270
        data = render_to_string('server.xml', {
271
            'server': server,
272
            'is_root': True})
273
    else:
274
        data = json.dumps({'server': server})
275
    return HttpResponse(data, status=status)
276

    
277

    
278
def render_diagnostics(request, diagnostics_dict, status=200):
279
    """
280
    Render diagnostics dictionary to json response.
281
    """
282
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
283

    
284

    
285
@api.api_method(http_method='GET', user_required=True, logger=log)
286
def get_server_diagnostics(request, server_id):
287
    """
288
    Virtual machine diagnostics api view.
289
    """
290
    log.debug('server_diagnostics %s', server_id)
291
    vm = util.get_vm(server_id, request.user_uniq)
292
    diagnostics = diagnostics_to_dict(vm.diagnostics.all())
293
    return render_diagnostics(request, diagnostics)
294

    
295

    
296
@api.api_method(http_method='GET', user_required=True, logger=log)
297
def list_servers(request, detail=False):
298
    # Normal Response Codes: 200, 203
299
    # Error Response Codes: computeFault (400, 500),
300
    #                       serviceUnavailable (503),
301
    #                       unauthorized (401),
302
    #                       badRequest (400),
303
    #                       overLimit (413)
304

    
305
    log.debug('list_servers detail=%s', detail)
306
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
307

    
308
    user_vms = utils.filter_modified_since(request, objects=user_vms)
309

    
310
    servers_dict = [vm_to_dict(server, detail)
311
                    for server in user_vms.order_by('id')]
312

    
313
    if request.serialization == 'xml':
314
        data = render_to_string('list_servers.xml', {
315
            'servers': servers_dict,
316
            'detail': detail})
317
    else:
318
        data = json.dumps({'servers': servers_dict})
319

    
320
    return HttpResponse(data, status=200)
321

    
322

    
323
@api.api_method(http_method='POST', user_required=True, logger=log)
324
def create_server(request):
325
    # Normal Response Code: 202
326
    # Error Response Codes: computeFault (400, 500),
327
    #                       serviceUnavailable (503),
328
    #                       unauthorized (401),
329
    #                       badMediaType(415),
330
    #                       itemNotFound (404),
331
    #                       badRequest (400),
332
    #                       serverCapacityUnavailable (503),
333
    #                       overLimit (413)
334
    req = utils.get_request_dict(request)
335
    log.info('create_server %s', req)
336
    user_id = request.user_uniq
337

    
338
    try:
339
        server = req['server']
340
        name = server['name']
341
        metadata = server.get('metadata', {})
342
        assert isinstance(metadata, dict)
343
        image_id = server['imageRef']
344
        flavor_id = server['flavorRef']
345
        personality = server.get('personality', [])
346
        assert isinstance(personality, list)
347
        private_networks = server.get("networks", [])
348
        assert isinstance(private_networks, list)
349
        floating_ips = server.get("floating_ips", [])
350
        assert isinstance(floating_ips, list)
351
    except (KeyError, AssertionError):
352
        raise faults.BadRequest("Malformed request")
353

    
354
    # Verify that personalities are well-formed
355
    util.verify_personality(personality)
356
    # Get image information
357
    image = util.get_image_dict(image_id, user_id)
358
    # Get flavor (ensure it is active)
359
    flavor = util.get_flavor(flavor_id, include_deleted=False)
360
    # Generate password
361
    password = util.random_password()
362

    
363
    vm = servers.create(user_id, name, password, flavor, image,
364
                        metadata=metadata, personality=personality,
365
                        private_networks=private_networks,
366
                        floating_ips=floating_ips)
367

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

    
372
    response = render_server(request, server, status=202)
373

    
374
    return response
375

    
376

    
377
@api.api_method(http_method='GET', user_required=True, logger=log)
378
def get_server_details(request, server_id):
379
    # Normal Response Codes: 200, 203
380
    # Error Response Codes: computeFault (400, 500),
381
    #                       serviceUnavailable (503),
382
    #                       unauthorized (401),
383
    #                       badRequest (400),
384
    #                       itemNotFound (404),
385
    #                       overLimit (413)
386

    
387
    log.debug('get_server_details %s', server_id)
388
    vm = util.get_vm(server_id, request.user_uniq)
389
    server = vm_to_dict(vm, detail=True)
390
    return render_server(request, server)
391

    
392

    
393
@api.api_method(http_method='PUT', user_required=True, logger=log)
394
def update_server_name(request, server_id):
395
    # Normal Response Code: 204
396
    # Error Response Codes: computeFault (400, 500),
397
    #                       serviceUnavailable (503),
398
    #                       unauthorized (401),
399
    #                       badRequest (400),
400
    #                       badMediaType(415),
401
    #                       itemNotFound (404),
402
    #                       buildInProgress (409),
403
    #                       overLimit (413)
404

    
405
    req = utils.get_request_dict(request)
406
    log.info('update_server_name %s %s', server_id, req)
407

    
408
    try:
409
        name = req['server']['name']
410
    except (TypeError, KeyError):
411
        raise faults.BadRequest("Malformed request")
412

    
413
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
414
                     non_suspended=True)
415
    vm.name = name
416
    vm.save()
417

    
418
    return HttpResponse(status=204)
419

    
420

    
421
@api.api_method(http_method='DELETE', user_required=True, logger=log)
422
def delete_server(request, server_id):
423
    # Normal Response Codes: 204
424
    # Error Response Codes: computeFault (400, 500),
425
    #                       serviceUnavailable (503),
426
    #                       unauthorized (401),
427
    #                       itemNotFound (404),
428
    #                       unauthorized (401),
429
    #                       buildInProgress (409),
430
    #                       overLimit (413)
431

    
432
    log.info('delete_server %s', server_id)
433
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
434
                     non_suspended=True)
435
    vm = servers.destroy(vm)
436
    return HttpResponse(status=204)
437

    
438

    
439
# additional server actions
440
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
441

    
442

    
443
def key_to_action(key):
444
    """Map HTTP request key to a VM Action"""
445
    if key == "shutdown":
446
        return "STOP"
447
    if key == "delete":
448
        return "DESTROY"
449
    if key in ARBITRARY_ACTIONS:
450
        return None
451
    else:
452
        return key.upper()
453

    
454

    
455
@api.api_method(http_method='POST', user_required=True, logger=log)
456
@transaction.commit_on_success
457
def demux_server_action(request, server_id):
458
    req = utils.get_request_dict(request)
459
    log.debug('server_action %s %s', server_id, req)
460

    
461
    if len(req) != 1:
462
        raise faults.BadRequest("Malformed request")
463

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

    
468
    action = req.keys()[0]
469

    
470
    if key_to_action(action) not in [x[0] for x in VirtualMachine.ACTIONS]:
471
        if action not in ARBITRARY_ACTIONS:
472
            raise faults.BadRequest("Action %s not supported" % action)
473
    action_args = req[action]
474

    
475
    if not isinstance(action_args, dict):
476
        raise faults.BadRequest("Invalid argument")
477

    
478
    return server_actions[action](request, vm, action_args)
479

    
480

    
481
@api.api_method(http_method='GET', user_required=True, logger=log)
482
def list_addresses(request, server_id):
483
    # Normal Response Codes: 200, 203
484
    # Error Response Codes: computeFault (400, 500),
485
    #                       serviceUnavailable (503),
486
    #                       unauthorized (401),
487
    #                       badRequest (400),
488
    #                       overLimit (413)
489

    
490
    log.debug('list_addresses %s', server_id)
491
    vm = util.get_vm(server_id, request.user_uniq)
492
    attachments = [nic_to_dict(nic) for nic in vm.nics.all()]
493
    addresses = attachments_to_addresses(attachments)
494

    
495
    if request.serialization == 'xml':
496
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
497
    else:
498
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
499

    
500
    return HttpResponse(data, status=200)
501

    
502

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

    
513
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
514
    machine = util.get_vm(server_id, request.user_uniq)
515
    network = util.get_network(network_id, request.user_uniq)
516
    nics = machine.nics.filter(network=network).all()
517
    addresses = attachments_to_addresses(map(nic_to_dict, nics))
518

    
519
    if request.serialization == 'xml':
520
        data = render_to_string('address.xml', {'addresses': addresses})
521
    else:
522
        data = json.dumps({'network': addresses})
523

    
524
    return HttpResponse(data, status=200)
525

    
526

    
527
@api.api_method(http_method='GET', user_required=True, logger=log)
528
def list_metadata(request, server_id):
529
    # Normal Response Codes: 200, 203
530
    # Error Response Codes: computeFault (400, 500),
531
    #                       serviceUnavailable (503),
532
    #                       unauthorized (401),
533
    #                       badRequest (400),
534
    #                       overLimit (413)
535

    
536
    log.debug('list_server_metadata %s', server_id)
537
    vm = util.get_vm(server_id, request.user_uniq)
538
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
539
    return util.render_metadata(request, metadata, use_values=False,
540
                                status=200)
541

    
542

    
543
@api.api_method(http_method='POST', user_required=True, logger=log)
544
def update_metadata(request, server_id):
545
    # Normal Response Code: 201
546
    # Error Response Codes: computeFault (400, 500),
547
    #                       serviceUnavailable (503),
548
    #                       unauthorized (401),
549
    #                       badRequest (400),
550
    #                       buildInProgress (409),
551
    #                       badMediaType(415),
552
    #                       overLimit (413)
553

    
554
    req = utils.get_request_dict(request)
555
    log.info('update_server_metadata %s %s', server_id, req)
556
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
557
    try:
558
        metadata = req['metadata']
559
        assert isinstance(metadata, dict)
560
    except (KeyError, AssertionError):
561
        raise faults.BadRequest("Malformed request")
562

    
563
    for key, val in metadata.items():
564
        meta, created = vm.metadata.get_or_create(meta_key=key)
565
        meta.meta_value = val
566
        meta.save()
567

    
568
    vm.save()
569
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
570
    return util.render_metadata(request, vm_meta, status=201)
571

    
572

    
573
@api.api_method(http_method='GET', user_required=True, logger=log)
574
def get_metadata_item(request, server_id, key):
575
    # Normal Response Codes: 200, 203
576
    # Error Response Codes: computeFault (400, 500),
577
    #                       serviceUnavailable (503),
578
    #                       unauthorized (401),
579
    #                       itemNotFound (404),
580
    #                       badRequest (400),
581
    #                       overLimit (413)
582

    
583
    log.debug('get_server_metadata_item %s %s', server_id, key)
584
    vm = util.get_vm(server_id, request.user_uniq)
585
    meta = util.get_vm_meta(vm, key)
586
    d = {meta.meta_key: meta.meta_value}
587
    return util.render_meta(request, d, status=200)
588

    
589

    
590
@api.api_method(http_method='PUT', user_required=True, logger=log)
591
@transaction.commit_on_success
592
def create_metadata_item(request, server_id, key):
593
    # Normal Response Code: 201
594
    # Error Response Codes: computeFault (400, 500),
595
    #                       serviceUnavailable (503),
596
    #                       unauthorized (401),
597
    #                       itemNotFound (404),
598
    #                       badRequest (400),
599
    #                       buildInProgress (409),
600
    #                       badMediaType(415),
601
    #                       overLimit (413)
602

    
603
    req = utils.get_request_dict(request)
604
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
605
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
606
    try:
607
        metadict = req['meta']
608
        assert isinstance(metadict, dict)
609
        assert len(metadict) == 1
610
        assert key in metadict
611
    except (KeyError, AssertionError):
612
        raise faults.BadRequest("Malformed request")
613

    
614
    meta, created = VirtualMachineMetadata.objects.get_or_create(
615
        meta_key=key,
616
        vm=vm)
617

    
618
    meta.meta_value = metadict[key]
619
    meta.save()
620
    vm.save()
621
    d = {meta.meta_key: meta.meta_value}
622
    return util.render_meta(request, d, status=201)
623

    
624

    
625
@api.api_method(http_method='DELETE', user_required=True, logger=log)
626
@transaction.commit_on_success
627
def delete_metadata_item(request, server_id, key):
628
    # Normal Response Code: 204
629
    # Error Response Codes: computeFault (400, 500),
630
    #                       serviceUnavailable (503),
631
    #                       unauthorized (401),
632
    #                       itemNotFound (404),
633
    #                       badRequest (400),
634
    #                       buildInProgress (409),
635
    #                       badMediaType(415),
636
    #                       overLimit (413),
637

    
638
    log.info('delete_server_metadata_item %s %s', server_id, key)
639
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
640
    meta = util.get_vm_meta(vm, key)
641
    meta.delete()
642
    vm.save()
643
    return HttpResponse(status=204)
644

    
645

    
646
@api.api_method(http_method='GET', user_required=True, logger=log)
647
def server_stats(request, server_id):
648
    # Normal Response Codes: 200
649
    # Error Response Codes: computeFault (400, 500),
650
    #                       serviceUnavailable (503),
651
    #                       unauthorized (401),
652
    #                       badRequest (400),
653
    #                       itemNotFound (404),
654
    #                       overLimit (413)
655

    
656
    log.debug('server_stats %s', server_id)
657
    vm = util.get_vm(server_id, request.user_uniq)
658
    #secret = util.encrypt(vm.backend_vm_id)
659
    secret = vm.backend_vm_id      # XXX disable backend id encryption
660

    
661
    stats = {
662
        'serverRef': vm.id,
663
        'refresh': settings.STATS_REFRESH_PERIOD,
664
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
665
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
666
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
667
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
668

    
669
    if request.serialization == 'xml':
670
        data = render_to_string('server_stats.xml', stats)
671
    else:
672
        data = json.dumps({'stats': stats})
673

    
674
    return HttpResponse(data, status=200)
675

    
676

    
677
# ACTIONS
678

    
679

    
680
server_actions = {}
681
network_actions = {}
682

    
683

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

    
689
    def decorator(func):
690
        server_actions[name] = func
691
        return func
692
    return decorator
693

    
694

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

    
700
    def decorator(func):
701
        network_actions[name] = func
702
        return func
703
    return decorator
704

    
705

    
706
@server_action('start')
707
def start(request, vm, args):
708
    # Normal Response Code: 202
709
    # Error Response Codes: serviceUnavailable (503),
710
    #                       itemNotFound (404)
711
    vm = servers.start(vm)
712
    return HttpResponse(status=202)
713

    
714

    
715
@server_action('shutdown')
716
def shutdown(request, vm, args):
717
    # Normal Response Code: 202
718
    # Error Response Codes: serviceUnavailable (503),
719
    #                       itemNotFound (404)
720
    vm = servers.stop(vm)
721
    return HttpResponse(status=202)
722

    
723

    
724
@server_action('reboot')
725
def reboot(request, vm, args):
726
    # Normal Response Code: 202
727
    # Error Response Codes: computeFault (400, 500),
728
    #                       serviceUnavailable (503),
729
    #                       unauthorized (401),
730
    #                       badRequest (400),
731
    #                       badMediaType(415),
732
    #                       itemNotFound (404),
733
    #                       buildInProgress (409),
734
    #                       overLimit (413)
735

    
736
    reboot_type = args.get("type", "SOFT")
737
    if reboot_type not in ["SOFT", "HARD"]:
738
        raise faults.BadRequest("Invalid 'type' attribute.")
739
    vm = servers.reboot(vm, reboot_type=reboot_type)
740
    return HttpResponse(status=202)
741

    
742

    
743
@server_action('firewallProfile')
744
def set_firewall_profile(request, vm, args):
745
    # Normal Response Code: 200
746
    # Error Response Codes: computeFault (400, 500),
747
    #                       serviceUnavailable (503),
748
    #                       unauthorized (401),
749
    #                       badRequest (400),
750
    #                       badMediaType(415),
751
    #                       itemNotFound (404),
752
    #                       buildInProgress (409),
753
    #                       overLimit (413)
754
    profile = args.get("profile")
755
    if profile is None:
756
        raise faults.BadRequest("Missing 'profile' attribute")
757
    index = args.get("index", 0)
758
    servers.set_firewall_profile(vm, profile=profile, index=index)
759
    return HttpResponse(status=202)
760

    
761

    
762
@server_action('resize')
763
def resize(request, vm, args):
764
    # Normal Response Code: 202
765
    # Error Response Codes: computeFault (400, 500),
766
    #                       serviceUnavailable (503),
767
    #                       unauthorized (401),
768
    #                       badRequest (400),
769
    #                       badMediaType(415),
770
    #                       itemNotFound (404),
771
    #                       buildInProgress (409),
772
    #                       serverCapacityUnavailable (503),
773
    #                       overLimit (413),
774
    #                       resizeNotAllowed (403)
775
    flavorRef = args.get("flavorRef")
776
    if flavorRef is None:
777
        raise faults.BadRequest("Missing 'flavorRef' attribute.")
778
    flavor = util.get_flavor(flavor_id=flavorRef, include_deleted=False)
779
    servers.resize(vm, flavor=flavor)
780
    return HttpResponse(status=202)
781

    
782

    
783
@server_action('console')
784
def get_console(request, vm, args):
785
    # Normal Response Code: 200
786
    # Error Response Codes: computeFault (400, 500),
787
    #                       serviceUnavailable (503),
788
    #                       unauthorized (401),
789
    #                       badRequest (400),
790
    #                       badMediaType(415),
791
    #                       itemNotFound (404),
792
    #                       buildInProgress (409),
793
    #                       overLimit (413)
794

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

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

    
804
    if request.serialization == 'xml':
805
        mimetype = 'application/xml'
806
        data = render_to_string('console.xml', {'console': console_info})
807
    else:
808
        mimetype = 'application/json'
809
        data = json.dumps({'console': console_info})
810

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

    
813

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

    
818

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

    
823

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

    
828

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

    
833

    
834
@network_action('add')
835
@transaction.commit_on_success
836
def add(request, net, args):
837
    # Normal Response Code: 202
838
    # Error Response Codes: computeFault (400, 500),
839
    #                       serviceUnavailable (503),
840
    #                       unauthorized (401),
841
    #                       badRequest (400),
842
    #                       buildInProgress (409),
843
    #                       badMediaType(415),
844
    #                       itemNotFound (404),
845
    #                       overLimit (413)
846
    server_id = args.get('serverRef', None)
847
    if not server_id:
848
        raise faults.BadRequest('Malformed Request.')
849

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

    
854

    
855
@network_action('remove')
856
@transaction.commit_on_success
857
def remove(request, net, args):
858
    # Normal Response Code: 202
859
    # Error Response Codes: computeFault (400, 500),
860
    #                       serviceUnavailable (503),
861
    #                       unauthorized (401),
862
    #                       badRequest (400),
863
    #                       badMediaType(415),
864
    #                       itemNotFound (404),
865
    #                       overLimit (413)
866

    
867
    attachment = args.get("attachment")
868
    if attachment is None:
869
        raise faults.BadRequest("Missing 'attachment' attribute.")
870
    try:
871
        # attachment string: nic-<vm-id>-<nic-index>
872
        _, server_id, nic_index = attachment.split("-", 2)
873
        server_id = int(server_id)
874
        nic_index = int(nic_index)
875
    except (ValueError, TypeError):
876
        raise faults.BadRequest("Invalid 'attachment' attribute.")
877

    
878
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
879
    servers.disconnect(vm, nic_index=nic_index)
880

    
881
    return HttpResponse(status=202)
882

    
883

    
884
@server_action("addFloatingIp")
885
def add_floating_ip(request, vm, args):
886
    address = args.get("address")
887
    if address is None:
888
        raise faults.BadRequest("Missing 'address' attribute")
889

    
890
    servers.add_floating_ip(vm, address)
891
    return HttpResponse(status=202)
892

    
893

    
894
@server_action("removeFloatingIp")
895
def remove_floating_ip(request, vm, args):
896
    address = args.get("address")
897
    if address is None:
898
        raise faults.BadRequest("Missing 'address' attribute")
899

    
900
    servers.remove_floating_ip(vm, address)
901
    return HttpResponse(status=202)