Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (32.8 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.stats_encrypt(vm.backend_vm_id)
691

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

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

    
705
    return HttpResponse(data, status=200)
706

    
707

    
708
# ACTIONS
709

    
710

    
711
server_actions = {}
712
network_actions = {}
713

    
714

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

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

    
725

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

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

    
736

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

    
745

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

    
754

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

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

    
773

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

    
795

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

    
816

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

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

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

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

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

    
847

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

    
852

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

    
857

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

    
862

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

    
867

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

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

    
888

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

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

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

    
913
    servers.disconnect(vm, nic)
914

    
915
    return HttpResponse(status=202)
916

    
917

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

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

    
931

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