Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (33 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
        if networks is not None:
383
            assert isinstance(networks, list)
384
    except (KeyError, AssertionError):
385
        raise faults.BadRequest("Malformed request")
386

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

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

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

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

    
406
    return response
407

    
408

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

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

    
425

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

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

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

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

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

    
452
    return HttpResponse(status=204)
453

    
454

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

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

    
472

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

    
476

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

    
488

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

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

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

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

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

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

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

    
514

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

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

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

    
535
    return HttpResponse(data, status=200)
536

    
537

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

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

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

    
559
    return HttpResponse(data, status=200)
560

    
561

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

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

    
577

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

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

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

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

    
608

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

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

    
625

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

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

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

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

    
660

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

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

    
681

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

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

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

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

    
710
    return HttpResponse(data, status=200)
711

    
712

    
713
# ACTIONS
714

    
715

    
716
server_actions = {}
717
network_actions = {}
718

    
719

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

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

    
730

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

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

    
741

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

    
750

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

    
759

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

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

    
778

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

    
800

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

    
821

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

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

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

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

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

    
852

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

    
857

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

    
862

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

    
867

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

    
872

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

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

    
893

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

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

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

    
918
    servers.disconnect(vm, nic)
919

    
920
    return HttpResponse(status=202)
921

    
922

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

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

    
936

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