Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (32.9 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_attachments(nic):
108
    """Convert a NIC object to 'attachments attribute.
109

110
    Convert a NIC object to match the format of 'attachments' attribute of the
111
    response to the /servers API call.
112

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

117
    """
118
    d = {'id': nic.id,
119
         'network_id': str(nic.network_id),
120
         'mac_address': nic.mac,
121
         'ipv4': '',
122
         'ipv6': ''}
123

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

    
127
    for ip in nic.ips.all():
128
        if not ip.deleted:
129
            ip_type = "floating" if ip.floating_ip else "fixed"
130
            if ip.ipversion == 4:
131
                d["ipv4"] = ip.address
132
                d["OS-EXT-IPS:type"] = ip_type
133
            else:
134
                d["ipv6"] = ip.address
135
                d["OS-EXT-IPS:type"] = ip_type
136
    return d
137

    
138

    
139
def attachments_to_addresses(attachments):
140
    """Convert 'attachments' attribute to 'addresses'.
141

142
    Convert a a list of 'attachments' attribute to a list of 'addresses'
143
    attribute, as expected in the response to /servers API call.
144

145
    """
146
    addresses = {}
147
    for nic in attachments:
148
        net_addrs = []
149
        if nic["ipv4"]:
150
            net_addrs.append({"version": 4,
151
                              "addr": nic["ipv4"],
152
                              "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
153
        if nic["ipv6"]:
154
            net_addrs.append({"version": 6,
155
                              "addr": nic["ipv6"],
156
                              "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
157
        addresses[nic["network_id"]] = net_addrs
158
    return addresses
159

    
160

    
161
def vm_to_dict(vm, detail=False):
162
    d = dict(id=vm.id, name=vm.name)
163
    d['links'] = util.vm_to_links(vm.id)
164
    if detail:
165
        d['user_id'] = vm.userid
166
        d['tenant_id'] = vm.userid
167
        d['status'] = logic_utils.get_rsapi_state(vm)
168
        d['SNF:task_state'] = logic_utils.get_task_state(vm)
169
        d['progress'] = 100 if d['status'] == 'ACTIVE' else vm.buildpercentage
170
        d['hostId'] = vm.hostid
171
        d['updated'] = utils.isoformat(vm.updated)
172
        d['created'] = utils.isoformat(vm.created)
173
        d['flavor'] = {"id": vm.flavor.id,
174
                       "links": util.flavor_to_links(vm.flavor.id)}
175
        d['image'] = {"id": vm.imageid,
176
                      "links": util.image_to_links(vm.imageid)}
177
        d['suspended'] = vm.suspended
178

    
179
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
180
        d['metadata'] = metadata
181

    
182
        nics = vm.nics.all()
183
        active_nics = filter(lambda nic: nic.state == "ACTIVE", nics)
184
        active_nics.sort(key=lambda nic: nic.id)
185
        attachments = map(nic_to_attachments, active_nics)
186
        d['attachments'] = attachments
187
        d['addresses'] = attachments_to_addresses(attachments)
188

    
189
        # include the latest vm diagnostic, if set
190
        diagnostic = vm.get_last_diagnostic()
191
        if diagnostic:
192
            d['diagnostics'] = diagnostics_to_dict([diagnostic])
193
        else:
194
            d['diagnostics'] = []
195
        # Fixed
196
        d["security_groups"] = [{"name": "default"}]
197
        d["key_name"] = None
198
        d["config_drive"] = ""
199
        d["accessIPv4"] = ""
200
        d["accessIPv6"] = ""
201
        fqdn = get_server_fqdn(vm, active_nics)
202
        d["SNF:fqdn"] = fqdn
203
        d["SNF:port_forwarding"] = get_server_port_forwarding(vm, active_nics,
204
                                                              fqdn)
205
    return d
206

    
207

    
208
def get_server_public_ip(vm_nics, version=4):
209
    """Get the first public IP address of a server.
210

211
    NOTE: 'vm_nics' objects have prefetched the ips
212
    """
213
    for version in [4, 6]:
214
        for nic in vm_nics:
215
            for ip in nic.ips.all():
216
                if ip.ipversion == version and ip.public:
217
                    return ip
218
    return None
219

    
220

    
221
def get_server_fqdn(vm, vm_nics):
222
    public_ip = get_server_public_ip(vm_nics)
223
    if public_ip is None:
224
        return ""
225

    
226
    fqdn_setting = settings.CYCLADES_SERVERS_FQDN
227
    if fqdn_setting is None:
228
        return public_ip.address
229
    elif isinstance(fqdn_setting, basestring):
230
        return fqdn_setting % {"id": vm.id}
231
    else:
232
        msg = ("Invalid setting: CYCLADES_SERVERS_FQDN."
233
               " Value must be a string.")
234
        raise faults.InternalServerError(msg)
235

    
236

    
237
def get_server_port_forwarding(vm, vm_nics, fqdn):
238
    """Create API 'port_forwarding' attribute from corresponding setting.
239

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

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

    
268
        port_forwarding[dport] = {"host": host, "port": str(port)}
269
    return port_forwarding
270

    
271

    
272
def diagnostics_to_dict(diagnostics):
273
    """
274
    Extract api data from diagnostics QuerySet.
275
    """
276
    entries = list()
277

    
278
    for diagnostic in diagnostics:
279
        # format source date if set
280
        formatted_source_date = None
281
        if diagnostic.source_date:
282
            formatted_source_date = utils.isoformat(diagnostic.source_date)
283

    
284
        entry = {
285
            'source': diagnostic.source,
286
            'created': utils.isoformat(diagnostic.created),
287
            'message': diagnostic.message,
288
            'details': diagnostic.details,
289
            'level': diagnostic.level,
290
        }
291

    
292
        if formatted_source_date:
293
            entry['source_date'] = formatted_source_date
294

    
295
        entries.append(entry)
296

    
297
    return entries
298

    
299

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

    
309

    
310
def render_diagnostics(request, diagnostics_dict, status=200):
311
    """
312
    Render diagnostics dictionary to json response.
313
    """
314
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
315

    
316

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

    
327

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

    
337
    log.debug('list_servers detail=%s', detail)
338
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
339
    if detail:
340
        user_vms = user_vms.prefetch_related("nics__ips")
341

    
342
    user_vms = utils.filter_modified_since(request, objects=user_vms)
343

    
344
    servers_dict = [vm_to_dict(server, detail)
345
                    for server in user_vms.order_by('id')]
346

    
347
    if request.serialization == 'xml':
348
        data = render_to_string('list_servers.xml', {
349
            'servers': servers_dict,
350
            'detail': detail})
351
    else:
352
        data = json.dumps({'servers': servers_dict})
353

    
354
    return HttpResponse(data, status=200)
355

    
356

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

    
372
    try:
373
        server = req['server']
374
        name = server['name']
375
        metadata = server.get('metadata', {})
376
        assert isinstance(metadata, dict)
377
        image_id = server['imageRef']
378
        flavor_id = server['flavorRef']
379
        personality = server.get('personality', [])
380
        assert isinstance(personality, list)
381
        networks = server.get("networks", [])
382
        assert isinstance(networks, list)
383
    except (KeyError, AssertionError):
384
        raise faults.BadRequest("Malformed request")
385

    
386
    # Verify that personalities are well-formed
387
    util.verify_personality(personality)
388
    # Get image information
389
    image = util.get_image_dict(image_id, user_id)
390
    # Get flavor (ensure it is active)
391
    flavor = util.get_flavor(flavor_id, include_deleted=False)
392
    # Generate password
393
    password = util.random_password()
394

    
395
    vm = servers.create(user_id, name, password, flavor, image,
396
                        metadata=metadata, personality=personality,
397
                        networks=networks)
398

    
399
    server = vm_to_dict(vm, detail=True)
400
    server['status'] = 'BUILD'
401
    server['adminPass'] = password
402

    
403
    response = render_server(request, server, status=202)
404

    
405
    return response
406

    
407

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

    
418
    log.debug('get_server_details %s', server_id)
419
    vm = util.get_vm(server_id, request.user_uniq,
420
                     prefetch_related="nics__ips")
421
    server = vm_to_dict(vm, detail=True)
422
    return render_server(request, server)
423

    
424

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

    
438
    req = utils.get_request_dict(request)
439
    log.info('update_server_name %s %s', server_id, req)
440

    
441
    try:
442
        name = req['server']['name']
443
    except (TypeError, KeyError):
444
        raise faults.BadRequest("Malformed request")
445

    
446
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
447
                     non_suspended=True)
448

    
449
    servers.rename(vm, new_name=name)
450

    
451
    return HttpResponse(status=204)
452

    
453

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

    
465
    log.info('delete_server %s', server_id)
466
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
467
                     non_suspended=True)
468
    vm = servers.destroy(vm)
469
    return HttpResponse(status=204)
470

    
471

    
472
# additional server actions
473
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
474

    
475

    
476
def key_to_action(key):
477
    """Map HTTP request key to a VM Action"""
478
    if key == "shutdown":
479
        return "STOP"
480
    if key == "delete":
481
        return "DESTROY"
482
    if key in ARBITRARY_ACTIONS:
483
        return None
484
    else:
485
        return key.upper()
486

    
487

    
488
@api.api_method(http_method='POST', user_required=True, logger=log)
489
@transaction.commit_on_success
490
def demux_server_action(request, server_id):
491
    req = utils.get_request_dict(request)
492
    log.debug('server_action %s %s', server_id, req)
493

    
494
    if len(req) != 1:
495
        raise faults.BadRequest("Malformed request")
496

    
497
    # Do not allow any action on deleted or suspended VMs
498
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
499
                     non_deleted=True, non_suspended=True)
500

    
501
    action = req.keys()[0]
502

    
503
    if key_to_action(action) not in [x[0] for x in VirtualMachine.ACTIONS]:
504
        if action not in ARBITRARY_ACTIONS:
505
            raise faults.BadRequest("Action %s not supported" % action)
506
    action_args = req[action]
507

    
508
    if not isinstance(action_args, dict):
509
        raise faults.BadRequest("Invalid argument")
510

    
511
    return server_actions[action](request, vm, action_args)
512

    
513

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

    
523
    log.debug('list_addresses %s', server_id)
524
    vm = util.get_vm(server_id, request.user_uniq, prefetch_related="nic__ips")
525
    attachments = [nic_to_attachments(nic)
526
                   for nic in vm.nics.filter(state="ACTIVE")]
527
    addresses = attachments_to_addresses(attachments)
528

    
529
    if request.serialization == 'xml':
530
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
531
    else:
532
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
533

    
534
    return HttpResponse(data, status=200)
535

    
536

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

    
547
    log.debug('list_addresses_by_network %s %s', server_id, network_id)
548
    machine = util.get_vm(server_id, request.user_uniq)
549
    network = util.get_network(network_id, request.user_uniq)
550
    nics = machine.nics.filter(network=network, state="ACTIVE")
551
    addresses = attachments_to_addresses(map(nic_to_attachments, nics))
552

    
553
    if request.serialization == 'xml':
554
        data = render_to_string('address.xml', {'addresses': addresses})
555
    else:
556
        data = json.dumps({'network': addresses})
557

    
558
    return HttpResponse(data, status=200)
559

    
560

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

    
570
    log.debug('list_server_metadata %s', server_id)
571
    vm = util.get_vm(server_id, request.user_uniq)
572
    metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
573
    return util.render_metadata(request, metadata, use_values=False,
574
                                status=200)
575

    
576

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

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

    
598
    for key, val in metadata.items():
599
        meta, created = vm.metadata.get_or_create(meta_key=key)
600
        meta.meta_value = val
601
        meta.save()
602

    
603
    vm.save()
604
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
605
    return util.render_metadata(request, vm_meta, status=201)
606

    
607

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

    
618
    log.debug('get_server_metadata_item %s %s', server_id, key)
619
    vm = util.get_vm(server_id, request.user_uniq)
620
    meta = util.get_vm_meta(vm, key)
621
    d = {meta.meta_key: meta.meta_value}
622
    return util.render_meta(request, d, status=200)
623

    
624

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

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

    
649
    meta, created = VirtualMachineMetadata.objects.get_or_create(
650
        meta_key=key,
651
        vm=vm)
652

    
653
    meta.meta_value = metadict[key]
654
    meta.save()
655
    vm.save()
656
    d = {meta.meta_key: meta.meta_value}
657
    return util.render_meta(request, d, status=201)
658

    
659

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

    
673
    log.info('delete_server_metadata_item %s %s', server_id, key)
674
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
675
    meta = util.get_vm_meta(vm, key)
676
    meta.delete()
677
    vm.save()
678
    return HttpResponse(status=204)
679

    
680

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

    
691
    log.debug('server_stats %s', server_id)
692
    vm = util.get_vm(server_id, request.user_uniq)
693
    #secret = util.encrypt(vm.backend_vm_id)
694
    secret = vm.backend_vm_id      # XXX disable backend id encryption
695

    
696
    stats = {
697
        'serverRef': vm.id,
698
        'refresh': settings.STATS_REFRESH_PERIOD,
699
        'cpuBar': settings.CPU_BAR_GRAPH_URL % secret,
700
        'cpuTimeSeries': settings.CPU_TIMESERIES_GRAPH_URL % secret,
701
        'netBar': settings.NET_BAR_GRAPH_URL % secret,
702
        'netTimeSeries': settings.NET_TIMESERIES_GRAPH_URL % secret}
703

    
704
    if request.serialization == 'xml':
705
        data = render_to_string('server_stats.xml', stats)
706
    else:
707
        data = json.dumps({'stats': stats})
708

    
709
    return HttpResponse(data, status=200)
710

    
711

    
712
# ACTIONS
713

    
714

    
715
server_actions = {}
716
network_actions = {}
717

    
718

    
719
def server_action(name):
720
    '''Decorator for functions implementing server actions.
721
    `name` is the key in the dict passed by the client.
722
    '''
723

    
724
    def decorator(func):
725
        server_actions[name] = func
726
        return func
727
    return decorator
728

    
729

    
730
def network_action(name):
731
    '''Decorator for functions implementing network actions.
732
    `name` is the key in the dict passed by the client.
733
    '''
734

    
735
    def decorator(func):
736
        network_actions[name] = func
737
        return func
738
    return decorator
739

    
740

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

    
749

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

    
758

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

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

    
777

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

    
799

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

    
820

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

    
833
    log.info("Get console  VM %s: %s", vm, args)
834

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

    
842
    if request.serialization == 'xml':
843
        mimetype = 'application/xml'
844
        data = render_to_string('console.xml', {'console': console_info})
845
    else:
846
        mimetype = 'application/json'
847
        data = json.dumps({'console': console_info})
848

    
849
    return HttpResponse(data, mimetype=mimetype, status=200)
850

    
851

    
852
@server_action('changePassword')
853
def change_password(request, vm, args):
854
    raise faults.NotImplemented('Changing password is not supported.')
855

    
856

    
857
@server_action('rebuild')
858
def rebuild(request, vm, args):
859
    raise faults.NotImplemented('Rebuild not supported.')
860

    
861

    
862
@server_action('confirmResize')
863
def confirm_resize(request, vm, args):
864
    raise faults.NotImplemented('Resize not supported.')
865

    
866

    
867
@server_action('revertResize')
868
def revert_resize(request, vm, args):
869
    raise faults.NotImplemented('Resize not supported.')
870

    
871

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

    
888
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
889
    servers.connect(vm, network=net)
890
    return HttpResponse(status=202)
891

    
892

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

    
905
    attachment = args.get("attachment")
906
    if attachment is None:
907
        raise faults.BadRequest("Missing 'attachment' attribute.")
908
    try:
909
        nic_id = int(attachment)
910
    except (ValueError, TypeError):
911
        raise faults.BadRequest("Invalid 'attachment' attribute.")
912

    
913
    nic = util.get_nic(nic_id=nic_id)
914
    server_id = nic.machine_id
915
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
916

    
917
    servers.disconnect(vm, nic)
918

    
919
    return HttpResponse(status=202)
920

    
921

    
922
@server_action("addFloatingIp")
923
def add_floating_ip(request, vm, args):
924
    address = args.get("address")
925
    if address is None:
926
        raise faults.BadRequest("Missing 'address' attribute")
927

    
928
    userid = vm.userid
929
    floating_ip = util.get_floating_ip_by_address(userid, address,
930
                                                  for_update=True)
931
    servers.create_port(userid, floating_ip.network, machine=vm,
932
                        user_ipaddress=floating_ip)
933
    return HttpResponse(status=202)
934

    
935

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