Statistics
| Branch: | Tag: | Revision:

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

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
    ip_type = "floating" if nic.is_floating_ip else "fixed"
109
    d = {'id': util.construct_nic_id(nic),
110
         'network_id': str(nic.network.id),
111
         'mac_address': nic.mac,
112
         'ipv4': nic.ipv4,
113
         'ipv6': nic.ipv6,
114
         'OS-EXT-IPS:type': ip_type}
115

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

    
120

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

    
135

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

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

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

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

    
178
    return d
179

    
180

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

    
201

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

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

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

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

    
240

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

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

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

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

    
264
        entries.append(entry)
265

    
266
    return entries
267

    
268

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

    
278

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

    
285

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

    
296

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

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

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

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

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

    
321
    return HttpResponse(data, status=200)
322

    
323

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

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

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

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

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

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

    
375
    return response
376

    
377

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

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

    
393

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

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

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

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

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

    
420
    return HttpResponse(status=204)
421

    
422

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

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

    
440

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

    
444

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

    
456

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

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

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

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

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

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

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

    
482

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

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

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

    
502
    return HttpResponse(data, status=200)
503

    
504

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

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

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

    
526
    return HttpResponse(data, status=200)
527

    
528

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

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

    
544

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

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

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

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

    
575

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

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

    
592

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

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

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

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

    
627

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

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

    
648

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

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

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

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

    
677
    return HttpResponse(data, status=200)
678

    
679

    
680
# ACTIONS
681

    
682

    
683
server_actions = {}
684
network_actions = {}
685

    
686

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

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

    
697

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

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

    
708

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

    
717

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

    
726

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

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

    
745

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

    
764

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

    
785

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

    
798
    log.info("Get console  VM %s: %s", vm, args)
799

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

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

    
814
    return HttpResponse(data, mimetype=mimetype, status=200)
815

    
816

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

    
821

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

    
826

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

    
831

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

    
836

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

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

    
857

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

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

    
881
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
882
    servers.disconnect(vm, nic_index=nic_index)
883

    
884
    return HttpResponse(status=202)
885

    
886

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

    
893
    servers.add_floating_ip(vm, address)
894
    return HttpResponse(status=202)
895

    
896

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

    
903
    servers.remove_floating_ip(vm, address)
904
    return HttpResponse(status=202)