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