Statistics
| Branch: | Tag: | Revision:

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

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
    fqdn_setting = settings.CYCLADES_SERVERS_FQDN
223
    if fqdn_setting is None:
224
        return None
225
    elif isinstance(fqdn_setting, basestring):
226
        return fqdn_setting % {"id": vm.id}
227
    else:
228
        msg = ("Invalid setting: CYCLADES_SERVERS_FQDN."
229
               " Value must be a string.")
230
        raise faults.InternalServerError(msg)
231

    
232

    
233
def get_server_port_forwarding(vm, vm_nics, fqdn):
234
    """Create API 'port_forwarding' attribute from corresponding setting.
235

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

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

    
264
        port_forwarding[dport] = {"host": host, "port": str(port)}
265
    return port_forwarding
266

    
267

    
268
def diagnostics_to_dict(diagnostics):
269
    """
270
    Extract api data from diagnostics QuerySet.
271
    """
272
    entries = list()
273

    
274
    for diagnostic in diagnostics:
275
        # format source date if set
276
        formatted_source_date = None
277
        if diagnostic.source_date:
278
            formatted_source_date = utils.isoformat(diagnostic.source_date)
279

    
280
        entry = {
281
            'source': diagnostic.source,
282
            'created': utils.isoformat(diagnostic.created),
283
            'message': diagnostic.message,
284
            'details': diagnostic.details,
285
            'level': diagnostic.level,
286
        }
287

    
288
        if formatted_source_date:
289
            entry['source_date'] = formatted_source_date
290

    
291
        entries.append(entry)
292

    
293
    return entries
294

    
295

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

    
305

    
306
def render_diagnostics(request, diagnostics_dict, status=200):
307
    """
308
    Render diagnostics dictionary to json response.
309
    """
310
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
311

    
312

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

    
323

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

    
333
    log.debug('list_servers detail=%s', detail)
334
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
335
    if detail:
336
        user_vms = user_vms.prefetch_related("nics__ips")
337

    
338
    user_vms = utils.filter_modified_since(request, objects=user_vms)
339

    
340
    servers_dict = [vm_to_dict(server, detail)
341
                    for server in user_vms.order_by('id')]
342

    
343
    if request.serialization == 'xml':
344
        data = render_to_string('list_servers.xml', {
345
            'servers': servers_dict,
346
            'detail': detail})
347
    else:
348
        data = json.dumps({'servers': servers_dict})
349

    
350
    return HttpResponse(data, status=200)
351

    
352

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

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

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

    
392
    vm = servers.create(user_id, name, password, flavor, image,
393
                        metadata=metadata, personality=personality,
394
                        networks=networks)
395

    
396
    server = vm_to_dict(vm, detail=True)
397
    server['status'] = 'BUILD'
398
    server['adminPass'] = password
399

    
400
    response = render_server(request, server, status=202)
401

    
402
    return response
403

    
404

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

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

    
421

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

    
435
    req = utils.get_request_dict(request)
436
    log.info('update_server_name %s %s', server_id, req)
437

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

    
443
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
444
                     non_suspended=True)
445

    
446
    servers.rename(vm, new_name=name)
447

    
448
    return HttpResponse(status=204)
449

    
450

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

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

    
468

    
469
# additional server actions
470
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
471

    
472

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

    
484

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

    
491
    if len(req) != 1:
492
        raise faults.BadRequest("Malformed request")
493

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

    
498
    action = req.keys()[0]
499

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

    
505
    if not isinstance(action_args, dict):
506
        raise faults.BadRequest("Invalid argument")
507

    
508
    return server_actions[action](request, vm, action_args)
509

    
510

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

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

    
526
    if request.serialization == 'xml':
527
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
528
    else:
529
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
530

    
531
    return HttpResponse(data, status=200)
532

    
533

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

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

    
550
    if request.serialization == 'xml':
551
        data = render_to_string('address.xml', {'addresses': addresses})
552
    else:
553
        data = json.dumps({'network': addresses})
554

    
555
    return HttpResponse(data, status=200)
556

    
557

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

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

    
573

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

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

    
595
    for key, val in metadata.items():
596
        meta, created = vm.metadata.get_or_create(meta_key=key)
597
        meta.meta_value = val
598
        meta.save()
599

    
600
    vm.save()
601
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
602
    return util.render_metadata(request, vm_meta, status=201)
603

    
604

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

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

    
621

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

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

    
646
    meta, created = VirtualMachineMetadata.objects.get_or_create(
647
        meta_key=key,
648
        vm=vm)
649

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

    
656

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

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

    
677

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

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

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

    
701
    if request.serialization == 'xml':
702
        data = render_to_string('server_stats.xml', stats)
703
    else:
704
        data = json.dumps({'stats': stats})
705

    
706
    return HttpResponse(data, status=200)
707

    
708

    
709
# ACTIONS
710

    
711

    
712
server_actions = {}
713
network_actions = {}
714

    
715

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

    
721
    def decorator(func):
722
        server_actions[name] = func
723
        return func
724
    return decorator
725

    
726

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

    
732
    def decorator(func):
733
        network_actions[name] = func
734
        return func
735
    return decorator
736

    
737

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

    
746

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

    
755

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

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

    
774

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

    
796

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

    
817

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

    
830
    log.info("Get console  VM %s: %s", vm, args)
831

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

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

    
846
    return HttpResponse(data, mimetype=mimetype, status=200)
847

    
848

    
849
@server_action('changePassword')
850
def change_password(request, vm, args):
851
    raise faults.NotImplemented('Changing password is not supported.')
852

    
853

    
854
@server_action('rebuild')
855
def rebuild(request, vm, args):
856
    raise faults.NotImplemented('Rebuild not supported.')
857

    
858

    
859
@server_action('confirmResize')
860
def confirm_resize(request, vm, args):
861
    raise faults.NotImplemented('Resize not supported.')
862

    
863

    
864
@server_action('revertResize')
865
def revert_resize(request, vm, args):
866
    raise faults.NotImplemented('Resize not supported.')
867

    
868

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

    
885
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
886
    servers.connect(vm, network=net)
887
    return HttpResponse(status=202)
888

    
889

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

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

    
910
    nic = util.get_nic(nic_id=nic_id)
911
    server_id = nic.machine_id
912
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
913

    
914
    servers.disconnect(vm, nic)
915

    
916
    return HttpResponse(status=202)
917

    
918

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

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

    
932

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