Statistics
| Branch: | Tag: | Revision:

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

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 import query as db_query
47
from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata)
48
from synnefo.logic import servers, utils as logic_utils
49

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

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

    
67

    
68
def demux(request):
69
    if request.method == 'GET':
70
        return list_servers(request)
71
    elif request.method == 'POST':
72
        return create_server(request)
73
    else:
74
        return api.api_method_not_allowed(request)
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

    
87

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

    
96

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

    
107

    
108
def nic_to_dict(nic):
109
    d = {'id': nic.id,
110
         'network_id': str(nic.network.id),
111
         'mac_address': nic.mac,
112
         'ipv4': '',
113
         'ipv6': ''}
114
    for ip in nic.ips.filter(deleted=False).select_related("subnet"):
115
        ip_type = "floating" if ip.floating_ip else "fixed"
116
        if ip.subnet.ipversion == 4:
117
            d["ipv4"] = ip.address
118
            d["OS-EXT-IPS:type"] = ip_type
119
        else:
120
            d["ipv6"] = ip.address
121
            d["OS-EXT-IPS:type"] = ip_type
122

    
123
    if nic.firewall_profile:
124
        d['firewallProfile'] = nic.firewall_profile
125
    return d
126

    
127

    
128
def attachments_to_addresses(attachments):
129
    addresses = {}
130
    for nic in attachments:
131
        net_nics = []
132
        if nic["ipv4"]:
133
            net_nics.append({"version": 4,
134
                             "addr": nic["ipv4"],
135
                             "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
136
        if nic["ipv6"]:
137
            net_nics.append({"version": 6,
138
                             "addr": nic["ipv6"],
139
                             "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
140
        addresses[nic["network_id"]] = net_nics
141
    return addresses
142

    
143

    
144
def vm_to_dict(vm, detail=False):
145
    d = dict(id=vm.id, name=vm.name)
146
    d['links'] = util.vm_to_links(vm.id)
147
    if detail:
148
        d['user_id'] = vm.userid
149
        d['tenant_id'] = vm.userid
150
        d['status'] = logic_utils.get_rsapi_state(vm)
151
        d['SNF:task_state'] = logic_utils.get_task_state(vm)
152
        d['progress'] = 100 if d['status'] == 'ACTIVE' else vm.buildpercentage
153
        d['hostId'] = vm.hostid
154
        d['updated'] = utils.isoformat(vm.updated)
155
        d['created'] = utils.isoformat(vm.created)
156
        d['flavor'] = {"id": vm.flavor.id,
157
                       "links": util.flavor_to_links(vm.flavor.id)}
158
        d['image'] = {"id": vm.imageid,
159
                      "links": util.image_to_links(vm.imageid)}
160
        d['suspended'] = vm.suspended
161

    
162
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
163
        d['metadata'] = metadata
164

    
165
        vm_nics = vm.nics.filter(state="ACTIVE").order_by("id")
166
        attachments = map(nic_to_dict, vm_nics)
167
        d['attachments'] = attachments
168
        d['addresses'] = attachments_to_addresses(attachments)
169

    
170
        # include the latest vm diagnostic, if set
171
        diagnostic = vm.get_last_diagnostic()
172
        if diagnostic:
173
            d['diagnostics'] = diagnostics_to_dict([diagnostic])
174
        else:
175
            d['diagnostics'] = []
176
        # Fixed
177
        d["security_groups"] = [{"name": "default"}]
178
        d["key_name"] = None
179
        d["config_drive"] = ""
180
        d["accessIPv4"] = ""
181
        d["accessIPv6"] = ""
182
        fqdn = get_server_fqdn(vm)
183
        d["SNF:fqdn"] = fqdn
184
        d["SNF:port_forwarding"] = get_server_port_forwarding(vm, fqdn)
185

    
186
    return d
187

    
188

    
189
def get_server_fqdn(vm):
190
    fqdn_setting = settings.CYCLADES_SERVERS_FQDN
191
    if fqdn_setting is None:
192
        # Return the first public IPv4 address if exists
193
        address = db_query.get_server_public_ip(server=vm, version=4)
194
        if address is None:
195
            # Else return the first public IPv6 address if exists
196
            address = db_query.get_server_public_ip(server=vm, version=6)
197
        if address is None:
198
            return ""
199
        else:
200
            return address
201
    elif isinstance(fqdn_setting, basestring):
202
        return fqdn_setting % {"id": vm.id}
203
    else:
204
        msg = ("Invalid setting: CYCLADES_SERVERS_FQDN."
205
               " Value must be a string.")
206
        raise faults.InternalServerError(msg)
207

    
208

    
209
def get_server_port_forwarding(vm, fqdn):
210
    """Create API 'port_forwarding' attribute from corresponding setting.
211

212
    Create the 'port_forwarding' API vm attribute based on the corresponding
213
    setting (CYCLADES_PORT_FORWARDING), which can be either a tuple
214
    of the form (host, port) or a callable object returning such tuple. In
215
    case of callable object, must be called with the following arguments:
216
    * ip_address
217
    * server_id
218
    * fqdn
219
    * owner UUID
220

221
    """
222
    port_forwarding = {}
223
    for dport, to_dest in settings.CYCLADES_PORT_FORWARDING.items():
224
        if hasattr(to_dest, "__call__"):
225
            address = db_query.get_server_public_ip(server=vm, version=4)
226
            to_dest = to_dest(address, vm.id, fqdn, vm.userid)
227
        msg = ("Invalid setting: CYCLADES_PORT_FOWARDING."
228
               " Value must be a tuple of two elements (host, port).")
229
        if to_dest is None:
230
            continue
231
        if not isinstance(to_dest, tuple) or len(to_dest) != 2:
232
                raise faults.InternalServerError(msg)
233
        else:
234
            try:
235
                host, port = to_dest
236
            except (TypeError, ValueError):
237
                raise faults.InternalServerError(msg)
238

    
239
        port_forwarding[dport] = {"host": host, "port": str(port)}
240
    return port_forwarding
241

    
242

    
243
def diagnostics_to_dict(diagnostics):
244
    """
245
    Extract api data from diagnostics QuerySet.
246
    """
247
    entries = list()
248

    
249
    for diagnostic in diagnostics:
250
        # format source date if set
251
        formatted_source_date = None
252
        if diagnostic.source_date:
253
            formatted_source_date = utils.isoformat(diagnostic.source_date)
254

    
255
        entry = {
256
            'source': diagnostic.source,
257
            'created': utils.isoformat(diagnostic.created),
258
            'message': diagnostic.message,
259
            'details': diagnostic.details,
260
            'level': diagnostic.level,
261
        }
262

    
263
        if formatted_source_date:
264
            entry['source_date'] = formatted_source_date
265

    
266
        entries.append(entry)
267

    
268
    return entries
269

    
270

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

    
280

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

    
287

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

    
298

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

    
308
    log.debug('list_servers detail=%s', detail)
309
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
310

    
311
    user_vms = utils.filter_modified_since(request, objects=user_vms)
312

    
313
    servers_dict = [vm_to_dict(server, detail)
314
                    for server in user_vms.order_by('id')]
315

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

    
323
    return HttpResponse(data, status=200)
324

    
325

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

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

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

    
366
    vm = servers.create(user_id, name, password, flavor, image,
367
                        metadata=metadata, personality=personality,
368
                        private_networks=private_networks,
369
                        floating_ips=floating_ips)
370

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

    
375
    response = render_server(request, server, status=202)
376

    
377
    return response
378

    
379

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

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

    
395

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

    
409
    req = utils.get_request_dict(request)
410
    log.info('update_server_name %s %s', server_id, req)
411

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

    
417
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
418
                     non_suspended=True)
419

    
420
    servers.rename(vm, new_name=name)
421

    
422
    return HttpResponse(status=204)
423

    
424

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

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

    
442

    
443
# additional server actions
444
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
445

    
446

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

    
458

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

    
465
    if len(req) != 1:
466
        raise faults.BadRequest("Malformed request")
467

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

    
472
    action = req.keys()[0]
473

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

    
479
    if not isinstance(action_args, dict):
480
        raise faults.BadRequest("Invalid argument")
481

    
482
    return server_actions[action](request, vm, action_args)
483

    
484

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

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

    
499
    if request.serialization == 'xml':
500
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
501
    else:
502
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
503

    
504
    return HttpResponse(data, status=200)
505

    
506

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

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

    
523
    if request.serialization == 'xml':
524
        data = render_to_string('address.xml', {'addresses': addresses})
525
    else:
526
        data = json.dumps({'network': addresses})
527

    
528
    return HttpResponse(data, status=200)
529

    
530

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

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

    
546

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

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

    
568
    for key, val in metadata.items():
569
        meta, created = vm.metadata.get_or_create(meta_key=key)
570
        meta.meta_value = val
571
        meta.save()
572

    
573
    vm.save()
574
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
575
    return util.render_metadata(request, vm_meta, status=201)
576

    
577

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

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

    
594

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

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

    
619
    meta, created = VirtualMachineMetadata.objects.get_or_create(
620
        meta_key=key,
621
        vm=vm)
622

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

    
629

    
630
@api.api_method(http_method='DELETE', user_required=True, logger=log)
631
@transaction.commit_on_success
632
def delete_metadata_item(request, server_id, key):
633
    # Normal Response Code: 204
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
    log.info('delete_server_metadata_item %s %s', server_id, key)
644
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
645
    meta = util.get_vm_meta(vm, key)
646
    meta.delete()
647
    vm.save()
648
    return HttpResponse(status=204)
649

    
650

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

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

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

    
674
    if request.serialization == 'xml':
675
        data = render_to_string('server_stats.xml', stats)
676
    else:
677
        data = json.dumps({'stats': stats})
678

    
679
    return HttpResponse(data, status=200)
680

    
681

    
682
# ACTIONS
683

    
684

    
685
server_actions = {}
686
network_actions = {}
687

    
688

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

    
694
    def decorator(func):
695
        server_actions[name] = func
696
        return func
697
    return decorator
698

    
699

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

    
705
    def decorator(func):
706
        network_actions[name] = func
707
        return func
708
    return decorator
709

    
710

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

    
719

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

    
728

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

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

    
747

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

    
769

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

    
790

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

    
803
    log.info("Get console  VM %s: %s", vm, args)
804

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

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

    
819
    return HttpResponse(data, mimetype=mimetype, status=200)
820

    
821

    
822
@server_action('changePassword')
823
def change_password(request, vm, args):
824
    raise faults.NotImplemented('Changing password is not supported.')
825

    
826

    
827
@server_action('rebuild')
828
def rebuild(request, vm, args):
829
    raise faults.NotImplemented('Rebuild not supported.')
830

    
831

    
832
@server_action('confirmResize')
833
def confirm_resize(request, vm, args):
834
    raise faults.NotImplemented('Resize not supported.')
835

    
836

    
837
@server_action('revertResize')
838
def revert_resize(request, vm, args):
839
    raise faults.NotImplemented('Resize not supported.')
840

    
841

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

    
858
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
859
    servers.connect(vm, network=net)
860
    return HttpResponse(status=202)
861

    
862

    
863
@network_action('remove')
864
@transaction.commit_on_success
865
def remove(request, net, args):
866
    # Normal Response Code: 202
867
    # Error Response Codes: computeFault (400, 500),
868
    #                       serviceUnavailable (503),
869
    #                       unauthorized (401),
870
    #                       badRequest (400),
871
    #                       badMediaType(415),
872
    #                       itemNotFound (404),
873
    #                       overLimit (413)
874

    
875
    attachment = args.get("attachment")
876
    if attachment is None:
877
        raise faults.BadRequest("Missing 'attachment' attribute.")
878
    try:
879
        nic_id = int(attachment)
880
    except (ValueError, TypeError):
881
        raise faults.BadRequest("Invalid 'attachment' attribute.")
882

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

    
887
    servers.disconnect(vm, nic)
888

    
889
    return HttpResponse(status=202)
890

    
891

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

    
898
    servers.add_floating_ip(vm, address)
899
    return HttpResponse(status=202)
900

    
901

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

    
908
    servers.remove_floating_ip(vm, address)
909
    return HttpResponse(status=202)