Revision 910d960d

b/snf-cyclades-app/synnefo/api/servers.py
44 44

  
45 45
from synnefo.api import util
46 46
from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata)
47
from synnefo.logic import servers, utils as logic_utils
47
from synnefo.logic import servers, utils as logic_utils, server_attachments
48
from synnefo.volume.util import get_volume
48 49

  
49 50
from logging import getLogger
50 51
log = getLogger(__name__)
......
61 62
    (r'^/(\d+)/metadata/(.+?)(?:.json|.xml)?$', 'metadata_item_demux'),
62 63
    (r'^/(\d+)/stats(?:.json|.xml)?$', 'server_stats'),
63 64
    (r'^/(\d+)/diagnostics(?:.json)?$', 'get_server_diagnostics'),
65
    (r'^/(\d+)/os-volume_attachments(?:.json)?$', 'demux_volumes'),
66
    (r'^/(\d+)/os-volume_attachments/(\d+)(?:.json)?$', 'demux_volumes_item'),
64 67
)
65 68

  
66 69

  
......
112 115
                                                           'DELETE'])
113 116

  
114 117

  
118
def demux_volumes(request, server_id):
119
    if request.method == 'GET':
120
        return get_volumes(request, server_id)
121
    elif request.method == 'POST':
122
        return attach_volume(request, server_id)
123
    else:
124
        return api.api_method_not_allowed(request,
125
                                          allowed_methods=['GET', 'POST'])
126

  
127

  
128
def demux_volumes_item(request, server_id, volume_id):
129
    if request.method == 'GET':
130
        return get_volume_info(request, server_id, volume_id)
131
    elif request.method == 'DELETE':
132
        return detach_volume(request, server_id, volume_id)
133
    else:
134
        return api.api_method_not_allowed(request,
135
                                          allowed_methods=['GET', 'DELETE'])
136

  
137

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

  
......
955 978
                                % address)
956 979
    servers.delete_port(floating_ip.nic)
957 980
    return HttpResponse(status=202)
981

  
982

  
983
def volume_to_attachment(volume):
984
    return {"id": volume.id,
985
            "volumeId": volume.id,
986
            "serverId": volume.machine_id,
987
            "device": ""}  #  TODO: What device to return?
988

  
989

  
990
@api.api_method(http_method='GET', user_required=True, logger=log)
991
def get_volumes(request, server_id):
992
    log.debug("get_volumes server_id %s", server_id)
993
    vm = util.get_vm(server_id, request.user_uniq, for_update=False)
994

  
995
    # TODO: Filter attachments!!
996
    volumes = vm.volumes.filter(deleted=False).order_by("id")
997
    attachments = [volume_to_attachment(v) for v in volumes]
998

  
999
    data = json.dumps({'volumeAttachments': attachments})
1000
    return HttpResponse(data, status=200)
1001
    pass
1002

  
1003

  
1004
@api.api_method(http_method='GET', user_required=True, logger=log)
1005
def get_volume_info(request, server_id, volume_id):
1006
    log.debug("get_volume_info server_id %s volume_id", server_id, volume_id)
1007
    user_id = request.user_uniq
1008
    vm = util.get_vm(server_id, user_id)
1009
    volume = get_volume(user_id, volume_id, for_update=False,
1010
                        exception=faults.BadRequest)
1011
    servers._check_attachment(vm, volume)
1012
    attachment = volume_to_attachment(volume)
1013
    data = json.dumps({'volumeAttachment': attachment})
1014
    return HttpResponse(data, status=200)
1015

  
1016

  
1017
@api.api_method(http_method='POST', user_required=True, logger=log)
1018
def attach_volume(request, server_id):
1019
    req = utils.get_request_dict(request)
1020
    log.debug("attach_volume server_id %s request", server_id, req)
1021
    user_id = request.user_uniq
1022
    vm = util.get_vm(server_id, user_id, for_update=True)
1023

  
1024
    attachment_dict = api.utils.get_attribute(req, "volumeAttachment",
1025
                                              required=True)
1026
    # Get volume
1027
    volume_id = api.utils.get_attribute(attachment_dict, "volumeId")
1028
    volume = get_volume(user_id, volume_id, for_update=True,
1029
                        exception=faults.BadRequest)
1030
    vm = server_attachments.attach_volume(vm, volume)
1031
    attachment = volume_to_attachment(volume)
1032
    data = json.dumps({'volumeAttachment': attachment})
1033

  
1034
    return HttpResponse(data, status=202)
1035

  
1036

  
1037
@api.api_method(http_method='DELETE', user_required=True, logger=log)
1038
def detach_volume(request, server_id, volume_id):
1039
    log.debug("detach_volume server_id %s volume_id", server_id, volume_id)
1040
    user_id = request.user_uniq
1041
    vm = util.get_vm(server_id, user_id)
1042
    volume = get_volume(user_id, volume_id, for_update=True,
1043
                        exception=faults.BadRequest)
1044
    vm = server_attachments.detach_volume(vm, volume)
1045
    # TODO: Check volume state, send job to detach volume
1046
    return HttpResponse(status=202)
b/snf-cyclades-app/synnefo/api/tests/servers.py
858 858
        response = self.mypost('servers/%d/action' % vm.id,
859 859
                               vm.userid, data, 'json')
860 860
        self.assertBadRequest(response)
861

  
862

  
863
@patch('synnefo.logic.rapi_pool.GanetiRapiClient')
864
class ServerAttachments(ComputeAPITest):
865
    def test_list_attachments(self, mrapi):
866
        # Test default volume
867
        vol = mfactory.VolumeFactory()
868
        vm = vol.machine
869

  
870
        response = self.myget("servers/%d/os-volume_attachments" % vm.id,
871
                              vm.userid)
872
        self.assertSuccess(response)
873
        attachments = json.loads(response.content)
874
        self.assertEqual(len(attachments), 1)
875
        self.assertEqual(attachments["volumeAttachments"][0],
876
                         {"volumeId": vol.id,
877
                          "serverId": vm.id,
878
                          "id": vol.id,
879
                          "device": ""})
880

  
881
        # Test deleted Volume
882
        dvol = mfactory.VolumeFactory(machine=vm, deleted=True)
883
        response = self.myget("servers/%d/os-volume_attachments" % vm.id,
884
                              vm.userid)
885
        self.assertSuccess(response)
886
        attachments = json.loads(response.content)["volumeAttachments"]
887
        self.assertEqual(len([d for d in attachments if d["id"] == dvol.id]),
888
                         0)
889

  
890
    def test_attach_detach_volume(self, mrapi):
891
        vol = mfactory.VolumeFactory(status="AVAILABLE")
892
        vm = vol.machine
893
        disk_template = vm.flavor.disk_template
894
        # Test that we cannot detach the root volume
895
        response = self.mydelete("servers/%d/os-volume_attachments/%d" %
896
                                 (vm.id, vol.id), vm.userid)
897
        self.assertBadRequest(response)
898

  
899
        # Test that we cannot attach a used volume
900
        vol1 = mfactory.VolumeFactory(status="IN_USE",
901
                                      disk_template=disk_template,
902
                                      userid=vm.userid)
903
        request = json.dumps({"volumeAttachment": {"volumeId": vol1.id}})
904
        response = self.mypost("servers/%d/os-volume_attachments" %
905
                               vm.id, vm.userid,
906
                               request, "json")
907
        self.assertBadRequest(response)
908

  
909
        # We cannot attach a volume of different disk template
910
        vol1.status = "AVAILABLE"
911
        vol1.disk_template = "lalalal"
912
        vol1.save()
913
        response = self.mypost("servers/%d/os-volume_attachments/" %
914
                               vm.id, vm.userid,
915
                               request, "json")
916
        self.assertBadRequest(response)
917

  
918
        vol1.disk_template = disk_template
919
        vol1.save()
920
        mrapi().ModifyInstance.return_value = 43
921
        response = self.mypost("servers/%d/os-volume_attachments" %
922
                               vm.id, vm.userid,
923
                               request, "json")
924
        self.assertEqual(response.status_code, 202, response.content)
925
        attachment = json.loads(response.content)["volumeAttachment"]
926
        self.assertEqual(attachment, {"volumeId": vol1.id,
927
                                      "serverId": vm.id,
928
                                      "id": vol1.id,
929
                                      "device": ""})
930
        # And we delete it...will fail because of status
931
        response = self.mydelete("servers/%d/os-volume_attachments/%d" %
932
                                 (vm.id, vol1.id), vm.userid)
933
        self.assertBadRequest(response)
934
        vm.task = None
935
        vm.save()
936
        vm.volumes.all().update(status="IN_USE")
937
        response = self.mydelete("servers/%d/os-volume_attachments/%d" %
938
                                 (vm.id, vol1.id), vm.userid)
939
        self.assertEqual(response.status_code, 202, response.content)
b/snf-cyclades-app/synnefo/db/models_factory.py
112 112
    operstate = "STARTED"
113 113

  
114 114

  
115
class VolumeFactory(factory.DjangoModelFactory):
116
    FACTORY_FOR = models.Volume
117
    userid = factory.Sequence(user_seq())
118
    size = factory.Sequence(lambda x: x, type=int)
119
    name = factory.Sequence(lambda x: "volume-name-"+x, type=str)
120
    machine = factory.SubFactory(VirtualMachineFactory,
121
                                 userid=factory.SelfAttribute('..userid'))
122
    disk_template = factory.LazyAttribute(lambda v:
123
                                          v.machine.flavor.disk_template
124
                                          if v.machine else "drbd")
125

  
126

  
115 127
class DeletedVirtualMachine(VirtualMachineFactory):
116 128
    deleted = True
117 129

  
......
286 298
    server_id = 1
287 299
    network_id = 1
288 300
    active = True
289

  
290

  
291
class VolumeFactory(factory.DjangoModelFactory):
292
    FACTORY_FOR = models.Volume
293
    userid = factory.Sequence(user_seq())
294
    size = factory.Sequence(lambda x: x, type=int)
295
    name = factory.Sequence(lambda x: "volume-name-"+x, type=str)

Also available in: Unified diff