Revision 41a7fae7 snf-cyclades-app/synnefo/api/servers.py

b/snf-cyclades-app/synnefo/api/servers.py
31 31
# interpreted as representing official policies, either expressed
32 32
# or implied, of GRNET S.A.
33 33

  
34
from django import dispatch
35 34
from django.conf import settings
36 35
from django.conf.urls.defaults import patterns
37 36
from django.db import transaction
......
42 41
from snf_django.lib import api
43 42
from snf_django.lib.api import faults, utils
44 43
from synnefo.api import util
45
from synnefo.api.actions import server_actions
46
from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata,
47
                               NetworkInterface)
48
from synnefo.logic.backend import create_instance, delete_instance
49
from synnefo.logic.utils import get_rsapi_state
50
from synnefo.logic.rapi import GanetiApiError
51
from synnefo.logic.backend_allocator import BackendAllocator
52
from synnefo import quotas
53

  
54
# server creation signal
55
server_created = dispatch.Signal(providing_args=["created_vm_params"])
44
from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata)
45
from synnefo.logic import servers, utils as logic_utils
56 46

  
57 47
from logging import getLogger
58 48
log = getLogger(__name__)
......
62 52
    (r'^(?:/|.json|.xml)?$', 'demux'),
63 53
    (r'^/detail(?:.json|.xml)?$', 'list_servers', {'detail': True}),
64 54
    (r'^/(\d+)(?:.json|.xml)?$', 'server_demux'),
65
    (r'^/(\d+)/action(?:.json|.xml)?$', 'server_action'),
55
    (r'^/(\d+)/action(?:.json|.xml)?$', 'demux_server_action'),
66 56
    (r'^/(\d+)/ips(?:.json|.xml)?$', 'list_addresses'),
67 57
    (r'^/(\d+)/ips/(.+?)(?:.json|.xml)?$', 'list_addresses_by_network'),
68 58
    (r'^/(\d+)/metadata(?:.json|.xml)?$', 'metadata_demux'),
......
145 135
    if detail:
146 136
        d['user_id'] = vm.userid
147 137
        d['tenant_id'] = vm.userid
148
        d['status'] = get_rsapi_state(vm)
149
        d['progress'] = 100 if get_rsapi_state(vm) == 'ACTIVE' \
150
            else vm.buildpercentage
138
        d['status'] = logic_utils.get_rsapi_state(vm)
139
        d['SNF:task_state'] = logic_utils.get_task_state(vm)
140
        d['progress'] = 100 if d['status'] == 'ACTIVE' else vm.buildpercentage
151 141
        d['hostId'] = vm.hostid
152 142
        d['updated'] = utils.isoformat(vm.updated)
153 143
        d['created'] = utils.isoformat(vm.created)
......
258 248
    else:
259 249
        user_vms = user_vms.filter(deleted=False)
260 250

  
261
    servers = [vm_to_dict(server, detail)
262
               for server in user_vms.order_by('id')]
251
    servers_dict = [vm_to_dict(server, detail)
252
                    for server in user_vms.order_by('id')]
263 253

  
264 254
    if request.serialization == 'xml':
265 255
        data = render_to_string('list_servers.xml', {
266
            'servers': servers,
256
            'servers': servers_dict,
267 257
            'detail': detail})
268 258
    else:
269
        data = json.dumps({'servers': servers})
259
        data = json.dumps({'servers': servers_dict})
270 260

  
271 261
    return HttpResponse(data, status=200)
272 262

  
......
307 297
    # Generate password
308 298
    password = util.random_password()
309 299

  
310
    vm = do_create_server(user_id, name, password, flavor, image,
311
                          metadata=metadata, personality=personality)
300
    vm = servers.create(user_id, name, password, flavor, image,
301
                        metadata=metadata, personality=personality)
312 302

  
313 303
    server = vm_to_dict(vm, detail=True)
314 304
    server['status'] = 'BUILD'
......
319 309
    return response
320 310

  
321 311

  
322
@transaction.commit_manually
323
def do_create_server(userid, name, password, flavor, image, metadata={},
324
                     personality=[], network=None, backend=None):
325
    # Fix flavor for archipelago
326
    disk_template, provider = util.get_flavor_provider(flavor)
327
    if provider:
328
        flavor.disk_template = disk_template
329
        flavor.disk_provider = provider
330
        flavor.disk_origin = image['checksum']
331
        image['backend_id'] = 'null'
332
    else:
333
        flavor.disk_provider = None
334
        flavor.disk_origin = None
335

  
336
    try:
337
        if backend is None:
338
            # Allocate backend to host the server.
339
            backend_allocator = BackendAllocator()
340
            backend = backend_allocator.allocate(userid, flavor)
341
            if backend is None:
342
                log.error("No available backend for VM with flavor %s", flavor)
343
                raise faults.ServiceUnavailable("No available backends")
344

  
345
        if network is None:
346
            # Allocate IP from public network
347
            (network, address) = util.get_public_ip(backend)
348
            nic = {'ip': address, 'network': network.backend_id}
349
        else:
350
            address = util.get_network_free_address(network)
351

  
352
        # We must save the VM instance now, so that it gets a valid
353
        # vm.backend_vm_id.
354
        vm = VirtualMachine.objects.create(
355
            name=name,
356
            backend=backend,
357
            userid=userid,
358
            imageid=image["id"],
359
            flavor=flavor,
360
            action="CREATE")
361

  
362
        # Create VM's public NIC. Do not wait notification form ganeti hooks to
363
        # create this NIC, because if the hooks never run (e.g. building error)
364
        # the VM's public IP address will never be released!
365
        NetworkInterface.objects.create(machine=vm, network=network, index=0,
366
                                        ipv4=address, state="BUILDING")
367

  
368
        log.info("Created entry in DB for VM '%s'", vm)
369

  
370
        # dispatch server created signal
371
        server_created.send(sender=vm, created_vm_params={
372
            'img_id': image['backend_id'],
373
            'img_passwd': password,
374
            'img_format': str(image['format']),
375
            'img_personality': json.dumps(personality),
376
            'img_properties': json.dumps(image['metadata']),
377
        })
378

  
379
        # Also we must create the VM metadata in the same transaction.
380
        for key, val in metadata.items():
381
            VirtualMachineMetadata.objects.create(
382
                meta_key=key,
383
                meta_value=val,
384
                vm=vm)
385
        # Issue commission to Quotaholder and accept it since at the end of
386
        # this transaction the VirtualMachine object will be created in the DB.
387
        # Note: the following call does a commit!
388
        quotas.issue_and_accept_commission(vm)
389
    except:
390
        transaction.rollback()
391
        raise
392
    else:
393
        transaction.commit()
394

  
395
    try:
396
        jobID = create_instance(vm, nic, flavor, image)
397
        # At this point the job is enqueued in the Ganeti backend
398
        vm.backendjobid = jobID
399
        vm.save()
400
        transaction.commit()
401
        log.info("User %s created VM %s, NIC %s, Backend %s, JobID %s",
402
                 userid, vm, nic, backend, str(jobID))
403
    except GanetiApiError as e:
404
        log.exception("Can not communicate to backend %s: %s.",
405
                      backend, e)
406
        # Failed while enqueuing OP_INSTANCE_CREATE to backend. Restore
407
        # already reserved quotas by issuing a negative commission
408
        vm.operstate = "ERROR"
409
        vm.backendlogmsg = "Can not communicate to backend."
410
        vm.deleted = True
411
        vm.save()
412
        quotas.issue_and_accept_commission(vm, delete=True)
413
        raise
414
    except:
415
        transaction.rollback()
416
        raise
417

  
418
    return vm
419

  
420

  
421 312
@api.api_method(http_method='GET', user_required=True, logger=log)
422 313
def get_server_details(request, server_id):
423 314
    # Normal Response Codes: 200, 203
......
463 354

  
464 355

  
465 356
@api.api_method(http_method='DELETE', user_required=True, logger=log)
466
@transaction.commit_on_success
467 357
def delete_server(request, server_id):
468 358
    # Normal Response Codes: 204
469 359
    # Error Response Codes: computeFault (400, 500),
......
477 367
    log.info('delete_server %s', server_id)
478 368
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
479 369
                     non_suspended=True)
480
    start_action(vm, 'DESTROY')
481
    delete_instance(vm)
370
    vm = servers.destroy(vm)
482 371
    return HttpResponse(status=204)
483 372

  
484 373

  
......
486 375
ARBITRARY_ACTIONS = ['console', 'firewallProfile']
487 376

  
488 377

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

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

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

  
501
    try:
502
        key = req.keys()[0]
503
        if key not in ARBITRARY_ACTIONS:
504
            start_action(vm, key_to_action(key))
505
        val = req[key]
506
        assert isinstance(val, dict)
507
        return server_actions[key](request, vm, val)
508
    except KeyError:
509
        raise faults.BadRequest("Unknown action")
510
    except AssertionError:
511
        raise faults.BadRequest("Invalid argument")
512

  
513

  
514 378
def key_to_action(key):
515 379
    """Map HTTP request key to a VM Action"""
516 380
    if key == "shutdown":
......
523 387
        return key.upper()
524 388

  
525 389

  
526
def start_action(vm, action):
527
    log.debug("Applying action %s to VM %s", action, vm)
528
    if not action:
529
        return
390
@api.api_method(http_method='POST', user_required=True, logger=log)
391
@transaction.commit_on_success
392
def demux_server_action(request, server_id):
393
    req = utils.get_request_dict(request)
394
    log.debug('server_action %s %s', server_id, req)
530 395

  
531
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
532
        raise faults.ServiceUnavailable("Action %s not supported" % action)
396
    if len(req) != 1:
397
        raise faults.BadRequest("Malformed request")
398

  
399
    # Do not allow any action on deleted or suspended VMs
400
    vm = util.get_vm(server_id, request.user_uniq, for_update=True,
401
                     non_deleted=True, non_suspended=True)
533 402

  
534
    # No actions to deleted VMs
535
    if vm.deleted:
536
        raise faults.BadRequest("VirtualMachine has been deleted.")
403
    try:
404
        action = req.keys()[0]
405
    except KeyError:
406
        raise faults.BadRequest("Unknown action")
537 407

  
538
    # No actions to machines being built. They may be destroyed, however.
539
    if vm.operstate == 'BUILD' and action != 'DESTROY':
540
        raise faults.BuildInProgress("Server is being build.")
408
    if key_to_action(action) not in [x[0] for x in VirtualMachine.ACTIONS]:
409
        if action not in ARBITRARY_ACTIONS:
410
            raise faults.BadRequest("Action %s not supported" % action)
411
    action_args = req[action]
541 412

  
542
    vm.action = action
543
    vm.backendjobid = None
544
    vm.backendopcode = None
545
    vm.backendjobstatus = None
546
    vm.backendlogmsg = None
413
    if not isinstance(action_args, dict):
414
        raise faults.BadRequest("Invalid argument")
547 415

  
548
    vm.save()
416
    return server_actions[action](request, vm, action_args)
549 417

  
550 418

  
551 419
@api.api_method(http_method='GET', user_required=True, logger=log)
......
742 610
        data = json.dumps({'stats': stats})
743 611

  
744 612
    return HttpResponse(data, status=200)
613

  
614

  
615
# ACTIONS
616

  
617

  
618
server_actions = {}
619
network_actions = {}
620

  
621

  
622
def server_action(name):
623
    '''Decorator for functions implementing server actions.
624
    `name` is the key in the dict passed by the client.
625
    '''
626

  
627
    def decorator(func):
628
        server_actions[name] = func
629
        return func
630
    return decorator
631

  
632

  
633
def network_action(name):
634
    '''Decorator for functions implementing network actions.
635
    `name` is the key in the dict passed by the client.
636
    '''
637

  
638
    def decorator(func):
639
        network_actions[name] = func
640
        return func
641
    return decorator
642

  
643

  
644
@server_action('start')
645
def start(request, vm, args):
646
    # Normal Response Code: 202
647
    # Error Response Codes: serviceUnavailable (503),
648
    #                       itemNotFound (404)
649
    vm = servers.start(vm)
650
    return HttpResponse(status=202)
651

  
652

  
653
@server_action('shutdown')
654
def shutdown(request, vm, args):
655
    # Normal Response Code: 202
656
    # Error Response Codes: serviceUnavailable (503),
657
    #                       itemNotFound (404)
658
    vm = servers.stop(vm)
659
    return HttpResponse(status=202)
660

  
661

  
662
@server_action('reboot')
663
def reboot(request, vm, args):
664
    # Normal Response Code: 202
665
    # Error Response Codes: computeFault (400, 500),
666
    #                       serviceUnavailable (503),
667
    #                       unauthorized (401),
668
    #                       badRequest (400),
669
    #                       badMediaType(415),
670
    #                       itemNotFound (404),
671
    #                       buildInProgress (409),
672
    #                       overLimit (413)
673

  
674
    reboot_type = args.get("type")
675
    if reboot_type is None:
676
        raise faults.BadRequest("Missing 'type' attribute.")
677
    elif reboot_type not in ["SOFT", "HARD"]:
678
        raise faults.BadRequest("Invalid 'type' attribute.")
679
    vm = servers.reboot(vm, reboot_type=reboot_type)
680
    return HttpResponse(status=202)
681

  
682

  
683
@server_action('firewallProfile')
684
def set_firewall_profile(request, vm, args):
685
    # Normal Response Code: 200
686
    # Error Response Codes: computeFault (400, 500),
687
    #                       serviceUnavailable (503),
688
    #                       unauthorized (401),
689
    #                       badRequest (400),
690
    #                       badMediaType(415),
691
    #                       itemNotFound (404),
692
    #                       buildInProgress (409),
693
    #                       overLimit (413)
694
    profile = args.get("profile")
695
    if profile is None:
696
        raise faults.BadRequest("Missing 'profile' attribute")
697
    servers.set_firewall_profile(vm, profile=profile)
698
    return HttpResponse(status=202)
699

  
700

  
701
@server_action('resize')
702
def resize(request, vm, args):
703
    # Normal Response Code: 202
704
    # Error Response Codes: computeFault (400, 500),
705
    #                       serviceUnavailable (503),
706
    #                       unauthorized (401),
707
    #                       badRequest (400),
708
    #                       badMediaType(415),
709
    #                       itemNotFound (404),
710
    #                       buildInProgress (409),
711
    #                       serverCapacityUnavailable (503),
712
    #                       overLimit (413),
713
    #                       resizeNotAllowed (403)
714
    flavorRef = args.get("flavorRef")
715
    if flavorRef is None:
716
        raise faults.BadRequest("Missing 'flavorRef' attribute.")
717
    flavor = util.get_flavor(flavor_id=flavorRef, include_deleted=False)
718
    servers.resize(vm, flavor=flavor)
719
    return HttpResponse(status=202)
720

  
721

  
722
@server_action('console')
723
def get_console(request, vm, args):
724
    # Normal Response Code: 200
725
    # Error Response Codes: computeFault (400, 500),
726
    #                       serviceUnavailable (503),
727
    #                       unauthorized (401),
728
    #                       badRequest (400),
729
    #                       badMediaType(415),
730
    #                       itemNotFound (404),
731
    #                       buildInProgress (409),
732
    #                       overLimit (413)
733

  
734
    log.info("Get console  VM %s: %s", vm, args)
735

  
736
    console_type = args.get("type")
737
    if console_type is None:
738
        raise faults.BadRequest("No console 'type' specified.")
739
    elif console_type != "vnc":
740
        raise faults.BadRequest("Console 'type' can only be 'vnc'.")
741
    console_info = servers.console(vm, console_type)
742

  
743
    if request.serialization == 'xml':
744
        mimetype = 'application/xml'
745
        data = render_to_string('console.xml', {'console': console_info})
746
    else:
747
        mimetype = 'application/json'
748
        data = json.dumps({'console': console_info})
749

  
750
    return HttpResponse(data, mimetype=mimetype, status=200)
751

  
752

  
753
@server_action('changePassword')
754
def change_password(request, vm, args):
755
    raise faults.NotImplemented('Changing password is not supported.')
756

  
757

  
758
@server_action('rebuild')
759
def rebuild(request, vm, args):
760
    raise faults.NotImplemented('Rebuild not supported.')
761

  
762

  
763
@server_action('confirmResize')
764
def confirm_resize(request, vm, args):
765
    raise faults.NotImplemented('Resize not supported.')
766

  
767

  
768
@server_action('revertResize')
769
def revert_resize(request, vm, args):
770
    raise faults.NotImplemented('Resize not supported.')
771

  
772

  
773
@network_action('add')
774
@transaction.commit_on_success
775
def add(request, net, args):
776
    # Normal Response Code: 202
777
    # Error Response Codes: computeFault (400, 500),
778
    #                       serviceUnavailable (503),
779
    #                       unauthorized (401),
780
    #                       badRequest (400),
781
    #                       buildInProgress (409),
782
    #                       badMediaType(415),
783
    #                       itemNotFound (404),
784
    #                       overLimit (413)
785
    server_id = args.get('serverRef', None)
786
    if not server_id:
787
        raise faults.BadRequest('Malformed Request.')
788

  
789
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
790
    servers.connect(vm, network=net)
791
    return HttpResponse(status=202)
792

  
793

  
794
@network_action('remove')
795
@transaction.commit_on_success
796
def remove(request, net, args):
797
    # Normal Response Code: 202
798
    # Error Response Codes: computeFault (400, 500),
799
    #                       serviceUnavailable (503),
800
    #                       unauthorized (401),
801
    #                       badRequest (400),
802
    #                       badMediaType(415),
803
    #                       itemNotFound (404),
804
    #                       overLimit (413)
805

  
806
    attachment = args.get("attachment")
807
    if attachment is None:
808
        raise faults.BadRequest("Missing 'attachment' attribute.")
809
    try:
810
        # attachment string: nic-<vm-id>-<nic-index>
811
        _, server_id, nic_index = attachment.split("-", 2)
812
        server_id = int(server_id)
813
        nic_index = int(nic_index)
814
    except (ValueError, TypeError):
815
        raise faults.BadRequest("Invalid 'attachment' attribute.")
816

  
817
    vm = util.get_vm(server_id, request.user_uniq, non_suspended=True)
818
    servers.disconnect(vm, nic_index=nic_index)
819

  
820
    return HttpResponse(status=202)

Also available in: Unified diff