Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (33.1 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
        floating_ips = server.get("floating_ips", [])
384
        assert isinstance(floating_ips, list)
385
    except (KeyError, AssertionError):
386
        raise faults.BadRequest("Malformed request")
387

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

    
397
    vm = servers.create(user_id, name, password, flavor, image,
398
                        metadata=metadata, personality=personality,
399
                        networks=networks,
400
                        floating_ips=floating_ips)
401

    
402
    server = vm_to_dict(vm, detail=True)
403
    server['status'] = 'BUILD'
404
    server['adminPass'] = password
405

    
406
    response = render_server(request, server, status=202)
407

    
408
    return response
409

    
410

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

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

    
427

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

    
441
    req = utils.get_request_dict(request)
442
    log.info('update_server_name %s %s', server_id, req)
443

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

    
449
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
450
                     non_suspended=True)
451

    
452
    servers.rename(vm, new_name=name)
453

    
454
    return HttpResponse(status=204)
455

    
456

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

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

    
474

    
475
# additional server actions
476
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
477

    
478

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

    
490

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

    
497
    if len(req) != 1:
498
        raise faults.BadRequest("Malformed request")
499

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

    
504
    action = req.keys()[0]
505

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

    
511
    if not isinstance(action_args, dict):
512
        raise faults.BadRequest("Invalid argument")
513

    
514
    return server_actions[action](request, vm, action_args)
515

    
516

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

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

    
532
    if request.serialization == 'xml':
533
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
534
    else:
535
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
536

    
537
    return HttpResponse(data, status=200)
538

    
539

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

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

    
556
    if request.serialization == 'xml':
557
        data = render_to_string('address.xml', {'addresses': addresses})
558
    else:
559
        data = json.dumps({'network': addresses})
560

    
561
    return HttpResponse(data, status=200)
562

    
563

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

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

    
579

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

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

    
601
    for key, val in metadata.items():
602
        meta, created = vm.metadata.get_or_create(meta_key=key)
603
        meta.meta_value = val
604
        meta.save()
605

    
606
    vm.save()
607
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
608
    return util.render_metadata(request, vm_meta, status=201)
609

    
610

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

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

    
627

    
628
@api.api_method(http_method='PUT', user_required=True, logger=log)
629
@transaction.commit_on_success
630
def create_metadata_item(request, server_id, key):
631
    # Normal Response Code: 201
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
    req = utils.get_request_dict(request)
642
    log.info('create_server_metadata_item %s %s %s', server_id, key, req)
643
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
644
    try:
645
        metadict = req['meta']
646
        assert isinstance(metadict, dict)
647
        assert len(metadict) == 1
648
        assert key in metadict
649
    except (KeyError, AssertionError):
650
        raise faults.BadRequest("Malformed request")
651

    
652
    meta, created = VirtualMachineMetadata.objects.get_or_create(
653
        meta_key=key,
654
        vm=vm)
655

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

    
662

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

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

    
683

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

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

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

    
707
    if request.serialization == 'xml':
708
        data = render_to_string('server_stats.xml', stats)
709
    else:
710
        data = json.dumps({'stats': stats})
711

    
712
    return HttpResponse(data, status=200)
713

    
714

    
715
# ACTIONS
716

    
717

    
718
server_actions = {}
719
network_actions = {}
720

    
721

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

    
727
    def decorator(func):
728
        server_actions[name] = func
729
        return func
730
    return decorator
731

    
732

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

    
738
    def decorator(func):
739
        network_actions[name] = func
740
        return func
741
    return decorator
742

    
743

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

    
752

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

    
761

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

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

    
780

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

    
802

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

    
823

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

    
836
    log.info("Get console  VM %s: %s", vm, args)
837

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

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

    
852
    return HttpResponse(data, mimetype=mimetype, status=200)
853

    
854

    
855
@server_action('changePassword')
856
def change_password(request, vm, args):
857
    raise faults.NotImplemented('Changing password is not supported.')
858

    
859

    
860
@server_action('rebuild')
861
def rebuild(request, vm, args):
862
    raise faults.NotImplemented('Rebuild not supported.')
863

    
864

    
865
@server_action('confirmResize')
866
def confirm_resize(request, vm, args):
867
    raise faults.NotImplemented('Resize not supported.')
868

    
869

    
870
@server_action('revertResize')
871
def revert_resize(request, vm, args):
872
    raise faults.NotImplemented('Resize not supported.')
873

    
874

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

    
891
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
892
    servers.connect(vm, network=net)
893
    return HttpResponse(status=202)
894

    
895

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

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

    
916
    nic = util.get_nic(nic_id=nic_id)
917
    server_id = nic.machine_id
918
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
919

    
920
    servers.disconnect(vm, nic)
921

    
922
    return HttpResponse(status=202)
923

    
924

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

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

    
938

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