Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (31.5 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

    
75

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

    
86

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

    
95

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

    
106

    
107
def nic_to_dict(nic):
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': nic.ip_type.lower()}
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("id")
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
@transaction.commit_on_success
395
def update_server_name(request, server_id):
396
    # Normal Response Code: 204
397
    # Error Response Codes: computeFault (400, 500),
398
    #                       serviceUnavailable (503),
399
    #                       unauthorized (401),
400
    #                       badRequest (400),
401
    #                       badMediaType(415),
402
    #                       itemNotFound (404),
403
    #                       buildInProgress (409),
404
    #                       overLimit (413)
405

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

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

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

    
417
    servers.rename(vm, new_name=name)
418

    
419
    return HttpResponse(status=204)
420

    
421

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

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

    
439

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

    
443

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

    
455

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

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

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

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

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

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

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

    
481

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

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

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

    
501
    return HttpResponse(data, status=200)
502

    
503

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

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

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

    
525
    return HttpResponse(data, status=200)
526

    
527

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

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

    
543

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

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

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

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

    
574

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

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

    
591

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

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

    
616
    meta, created = VirtualMachineMetadata.objects.get_or_create(
617
        meta_key=key,
618
        vm=vm)
619

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

    
626

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

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

    
647

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

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

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

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

    
676
    return HttpResponse(data, status=200)
677

    
678

    
679
# ACTIONS
680

    
681

    
682
server_actions = {}
683
network_actions = {}
684

    
685

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

    
691
    def decorator(func):
692
        server_actions[name] = func
693
        return func
694
    return decorator
695

    
696

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

    
702
    def decorator(func):
703
        network_actions[name] = func
704
        return func
705
    return decorator
706

    
707

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

    
716

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

    
725

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

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

    
744

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

    
766

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

    
787

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

    
800
    log.info("Get console  VM %s: %s", vm, args)
801

    
802
    console_type = args.get("type")
803
    if console_type is None:
804
        raise faults.BadRequest("No console 'type' specified.")
805
    elif console_type != "vnc":
806
        raise faults.BadRequest("Console 'type' can only be 'vnc'.")
807
    console_info = servers.console(vm, console_type)
808

    
809
    if request.serialization == 'xml':
810
        mimetype = 'application/xml'
811
        data = render_to_string('console.xml', {'console': console_info})
812
    else:
813
        mimetype = 'application/json'
814
        data = json.dumps({'console': console_info})
815

    
816
    return HttpResponse(data, mimetype=mimetype, status=200)
817

    
818

    
819
@server_action('changePassword')
820
def change_password(request, vm, args):
821
    raise faults.NotImplemented('Changing password is not supported.')
822

    
823

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

    
828

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

    
833

    
834
@server_action('revertResize')
835
def revert_resize(request, vm, args):
836
    raise faults.NotImplemented('Resize not supported.')
837

    
838

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

    
855
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
856
    servers.connect(vm, network=net)
857
    return HttpResponse(status=202)
858

    
859

    
860
@network_action('remove')
861
@transaction.commit_on_success
862
def remove(request, net, 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
    #                       overLimit (413)
871

    
872
    attachment = args.get("attachment")
873
    if attachment is None:
874
        raise faults.BadRequest("Missing 'attachment' attribute.")
875
    try:
876
        # attachment string: nic-<vm-id>-<nic-id>
877
        _, server_id, nic_id = attachment.split("-", 2)
878
        server_id = int(server_id)
879
        nic_id = int(nic_id)
880
    except (ValueError, TypeError):
881
        raise faults.BadRequest("Invalid 'attachment' attribute.")
882

    
883
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
884
    nic = util.get_nic(vm, nic_id)
885
    servers.disconnect(vm, nic)
886

    
887
    return HttpResponse(status=202)
888

    
889

    
890
@server_action("addFloatingIp")
891
def add_floating_ip(request, vm, args):
892
    address = args.get("address")
893
    if address is None:
894
        raise faults.BadRequest("Missing 'address' attribute")
895

    
896
    servers.add_floating_ip(vm, address)
897
    return HttpResponse(status=202)
898

    
899

    
900
@server_action("removeFloatingIp")
901
def remove_floating_ip(request, vm, args):
902
    address = args.get("address")
903
    if address is None:
904
        raise faults.BadRequest("Missing 'address' attribute")
905

    
906
    servers.remove_floating_ip(vm, address)
907
    return HttpResponse(status=202)