Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (33.4 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
                                          allowed_methods=['GET', 'POST'])
75

    
76

    
77
def server_demux(request, server_id):
78
    if request.method == 'GET':
79
        return get_server_details(request, server_id)
80
    elif request.method == 'PUT':
81
        return update_server_name(request, server_id)
82
    elif request.method == 'DELETE':
83
        return delete_server(request, server_id)
84
    else:
85
        return api.api_method_not_allowed(request,
86
                                          allowed_methods=['GET',
87
                                                           'PUT',
88
                                                           'DELETE'])
89

    
90

    
91
def metadata_demux(request, server_id):
92
    if request.method == 'GET':
93
        return list_metadata(request, server_id)
94
    elif request.method == 'POST':
95
        return update_metadata(request, server_id)
96
    else:
97
        return api.api_method_not_allowed(request,
98
                                          allowed_methods=['GET', 'POST'])
99

    
100

    
101
def metadata_item_demux(request, server_id, key):
102
    if request.method == 'GET':
103
        return get_metadata_item(request, server_id, key)
104
    elif request.method == 'PUT':
105
        return create_metadata_item(request, server_id, key)
106
    elif request.method == 'DELETE':
107
        return delete_metadata_item(request, server_id, key)
108
    else:
109
        return api.api_method_not_allowed(request,
110
                                          allowed_methods=['GET',
111
                                                           'PUT',
112
                                                           'DELETE'])
113

    
114

    
115
def nic_to_attachments(nic):
116
    """Convert a NIC object to 'attachments attribute.
117

118
    Convert a NIC object to match the format of 'attachments' attribute of the
119
    response to the /servers API call.
120

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

125
    """
126
    d = {'id': nic.id,
127
         'network_id': str(nic.network_id),
128
         'mac_address': nic.mac,
129
         'ipv4': '',
130
         'ipv6': ''}
131

    
132
    if nic.firewall_profile:
133
        d['firewallProfile'] = nic.firewall_profile
134

    
135
    for ip in nic.ips.all():
136
        if not ip.deleted:
137
            ip_type = "floating" if ip.floating_ip else "fixed"
138
            if ip.ipversion == 4:
139
                d["ipv4"] = ip.address
140
                d["OS-EXT-IPS:type"] = ip_type
141
            else:
142
                d["ipv6"] = ip.address
143
                d["OS-EXT-IPS:type"] = ip_type
144
    return d
145

    
146

    
147
def attachments_to_addresses(attachments):
148
    """Convert 'attachments' attribute to 'addresses'.
149

150
    Convert a a list of 'attachments' attribute to a list of 'addresses'
151
    attribute, as expected in the response to /servers API call.
152

153
    """
154
    addresses = {}
155
    for nic in attachments:
156
        net_addrs = []
157
        if nic["ipv4"]:
158
            net_addrs.append({"version": 4,
159
                              "addr": nic["ipv4"],
160
                              "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
161
        if nic["ipv6"]:
162
            net_addrs.append({"version": 6,
163
                              "addr": nic["ipv6"],
164
                              "OS-EXT-IPS:type": nic["OS-EXT-IPS:type"]})
165
        addresses[nic["network_id"]] = net_addrs
166
    return addresses
167

    
168

    
169
def vm_to_dict(vm, detail=False):
170
    d = dict(id=vm.id, name=vm.name)
171
    d['links'] = util.vm_to_links(vm.id)
172
    if detail:
173
        d['user_id'] = vm.userid
174
        d['tenant_id'] = vm.userid
175
        d['status'] = logic_utils.get_rsapi_state(vm)
176
        d['SNF:task_state'] = logic_utils.get_task_state(vm)
177
        d['progress'] = 100 if d['status'] == 'ACTIVE' else vm.buildpercentage
178
        d['hostId'] = vm.hostid
179
        d['updated'] = utils.isoformat(vm.updated)
180
        d['created'] = utils.isoformat(vm.created)
181
        d['flavor'] = {"id": vm.flavor.id,
182
                       "links": util.flavor_to_links(vm.flavor.id)}
183
        d['image'] = {"id": vm.imageid,
184
                      "links": util.image_to_links(vm.imageid)}
185
        d['suspended'] = vm.suspended
186

    
187
        metadata = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
188
        d['metadata'] = metadata
189

    
190
        nics = vm.nics.all()
191
        active_nics = filter(lambda nic: nic.state == "ACTIVE", nics)
192
        active_nics.sort(key=lambda nic: nic.id)
193
        attachments = map(nic_to_attachments, active_nics)
194
        d['attachments'] = attachments
195
        d['addresses'] = attachments_to_addresses(attachments)
196

    
197
        d['volumes'] = [v.id for v in vm.volumes.order_by('id')]
198

    
199
        # include the latest vm diagnostic, if set
200
        diagnostic = vm.get_last_diagnostic()
201
        if diagnostic:
202
            d['diagnostics'] = diagnostics_to_dict([diagnostic])
203
        else:
204
            d['diagnostics'] = []
205
        # Fixed
206
        d["security_groups"] = [{"name": "default"}]
207
        d["key_name"] = None
208
        d["config_drive"] = ""
209
        d["accessIPv4"] = ""
210
        d["accessIPv6"] = ""
211
        fqdn = get_server_fqdn(vm, active_nics)
212
        d["SNF:fqdn"] = fqdn
213
        d["SNF:port_forwarding"] = get_server_port_forwarding(vm, active_nics,
214
                                                              fqdn)
215
    return d
216

    
217

    
218
def get_server_public_ip(vm_nics, version=4):
219
    """Get the first public IP address of a server.
220

221
    NOTE: 'vm_nics' objects have prefetched the ips
222
    """
223
    for nic in vm_nics:
224
        for ip in nic.ips.all():
225
            if ip.ipversion == version and ip.public:
226
                return ip
227
    return None
228

    
229

    
230
def get_server_fqdn(vm, vm_nics):
231
    fqdn_setting = settings.CYCLADES_SERVERS_FQDN
232
    if fqdn_setting is None:
233
        return None
234
    elif isinstance(fqdn_setting, basestring):
235
        return fqdn_setting % {"id": vm.id}
236
    else:
237
        msg = ("Invalid setting: CYCLADES_SERVERS_FQDN."
238
               " Value must be a string.")
239
        raise faults.InternalServerError(msg)
240

    
241

    
242
def get_server_port_forwarding(vm, vm_nics, fqdn):
243
    """Create API 'port_forwarding' attribute from corresponding setting.
244

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

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

    
273
        port_forwarding[dport] = {"host": host, "port": str(port)}
274
    return port_forwarding
275

    
276

    
277
def diagnostics_to_dict(diagnostics):
278
    """
279
    Extract api data from diagnostics QuerySet.
280
    """
281
    entries = list()
282

    
283
    for diagnostic in diagnostics:
284
        # format source date if set
285
        formatted_source_date = None
286
        if diagnostic.source_date:
287
            formatted_source_date = utils.isoformat(diagnostic.source_date)
288

    
289
        entry = {
290
            'source': diagnostic.source,
291
            'created': utils.isoformat(diagnostic.created),
292
            'message': diagnostic.message,
293
            'details': diagnostic.details,
294
            'level': diagnostic.level,
295
        }
296

    
297
        if formatted_source_date:
298
            entry['source_date'] = formatted_source_date
299

    
300
        entries.append(entry)
301

    
302
    return entries
303

    
304

    
305
def render_server(request, server, status=200):
306
    if request.serialization == 'xml':
307
        data = render_to_string('server.xml', {
308
            'server': server,
309
            'is_root': True})
310
    else:
311
        data = json.dumps({'server': server})
312
    return HttpResponse(data, status=status)
313

    
314

    
315
def render_diagnostics(request, diagnostics_dict, status=200):
316
    """
317
    Render diagnostics dictionary to json response.
318
    """
319
    return HttpResponse(json.dumps(diagnostics_dict), status=status)
320

    
321

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

    
332

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

    
342
    log.debug('list_servers detail=%s', detail)
343
    user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
344
    if detail:
345
        user_vms = user_vms.prefetch_related("nics__ips")
346

    
347
    user_vms = utils.filter_modified_since(request, objects=user_vms)
348

    
349
    servers_dict = [vm_to_dict(server, detail)
350
                    for server in user_vms.order_by('id')]
351

    
352
    if request.serialization == 'xml':
353
        data = render_to_string('list_servers.xml', {
354
            'servers': servers_dict,
355
            'detail': detail})
356
    else:
357
        data = json.dumps({'servers': servers_dict})
358

    
359
    return HttpResponse(data, status=200)
360

    
361

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

    
377
    try:
378
        server = req['server']
379
        name = server['name']
380
        metadata = server.get('metadata', {})
381
        assert isinstance(metadata, dict)
382
        image_id = server['imageRef']
383
        flavor_id = server['flavorRef']
384
        personality = server.get('personality', [])
385
        assert isinstance(personality, list)
386
        networks = server.get("networks")
387
        if networks is not None:
388
            assert isinstance(networks, list)
389
    except (KeyError, AssertionError):
390
        raise faults.BadRequest("Malformed request")
391

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

    
401
    vm = servers.create(user_id, name, password, flavor, image,
402
                        metadata=metadata, personality=personality,
403
                        networks=networks)
404

    
405
    server = vm_to_dict(vm, detail=True)
406
    server['status'] = 'BUILD'
407
    server['adminPass'] = password
408

    
409
    response = render_server(request, server, status=202)
410

    
411
    return response
412

    
413

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

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

    
430

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

    
444
    req = utils.get_request_dict(request)
445
    log.info('update_server_name %s %s', server_id, req)
446

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

    
452
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
453
                     non_suspended=True)
454

    
455
    servers.rename(vm, new_name=name)
456

    
457
    return HttpResponse(status=204)
458

    
459

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

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

    
477

    
478
# additional server actions
479
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
480

    
481

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

    
493

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

    
500
    if len(req) != 1:
501
        raise faults.BadRequest("Malformed request")
502

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

    
507
    action = req.keys()[0]
508

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

    
514
    if not isinstance(action_args, dict):
515
        raise faults.BadRequest("Invalid argument")
516

    
517
    return server_actions[action](request, vm, action_args)
518

    
519

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

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

    
535
    if request.serialization == 'xml':
536
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
537
    else:
538
        data = json.dumps({'addresses': addresses, 'attachments': attachments})
539

    
540
    return HttpResponse(data, status=200)
541

    
542

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

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

    
559
    if request.serialization == 'xml':
560
        data = render_to_string('address.xml', {'addresses': addresses})
561
    else:
562
        data = json.dumps({'network': addresses})
563

    
564
    return HttpResponse(data, status=200)
565

    
566

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

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

    
582

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

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

    
604
    for key, val in metadata.items():
605
        meta, created = vm.metadata.get_or_create(meta_key=key)
606
        meta.meta_value = val
607
        meta.save()
608

    
609
    vm.save()
610
    vm_meta = dict((m.meta_key, m.meta_value) for m in vm.metadata.all())
611
    return util.render_metadata(request, vm_meta, status=201)
612

    
613

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

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

    
630

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

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

    
655
    meta, created = VirtualMachineMetadata.objects.get_or_create(
656
        meta_key=key,
657
        vm=vm)
658

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

    
665

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

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

    
686

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

    
697
    log.debug('server_stats %s', server_id)
698
    vm = util.get_vm(server_id, request.user_uniq)
699
    secret = util.stats_encrypt(vm.backend_vm_id)
700

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

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

    
714
    return HttpResponse(data, status=200)
715

    
716

    
717
# ACTIONS
718

    
719

    
720
server_actions = {}
721
network_actions = {}
722

    
723

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

    
729
    def decorator(func):
730
        server_actions[name] = func
731
        return func
732
    return decorator
733

    
734

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

    
740
    def decorator(func):
741
        network_actions[name] = func
742
        return func
743
    return decorator
744

    
745

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

    
754

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

    
763

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

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

    
782

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

    
804

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

    
825

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

    
838
    log.info("Get console  VM %s: %s", vm, args)
839

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

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

    
854
    return HttpResponse(data, mimetype=mimetype, status=200)
855

    
856

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

    
861

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

    
866

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

    
871

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

    
876

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

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

    
897

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

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

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

    
922
    servers.disconnect(vm, nic)
923

    
924
    return HttpResponse(status=202)
925

    
926

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

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

    
940

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