Commit 58235481 authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

cyclades: Implement API for volume attachments

Extend /servers API with 'os-volume_attachments' endpoint, containing
API calls for attaching and detaching volumes to servers, and
listing/showing the volumes of a server.
parent 5bb624cc
......@@ -44,7 +44,8 @@ from snf_django.lib.api import faults, utils
from synnefo.api import util
from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata)
from synnefo.logic import servers, utils as logic_utils
from synnefo.logic import servers, utils as logic_utils, server_attachments
from synnefo.volume.util import get_volume
from logging import getLogger
log = getLogger(__name__)
......@@ -61,6 +62,8 @@ urlpatterns = patterns(
(r'^/(\d+)/metadata/(.+?)(?:.json|.xml)?$', 'metadata_item_demux'),
(r'^/(\d+)/stats(?:.json|.xml)?$', 'server_stats'),
(r'^/(\d+)/diagnostics(?:.json)?$', 'get_server_diagnostics'),
(r'^/(\d+)/os-volume_attachments(?:.json)?$', 'demux_volumes'),
(r'^/(\d+)/os-volume_attachments/(\d+)(?:.json)?$', 'demux_volumes_item'),
)
......@@ -112,6 +115,26 @@ def metadata_item_demux(request, server_id, key):
'DELETE'])
def demux_volumes(request, server_id):
if request.method == 'GET':
return get_volumes(request, server_id)
elif request.method == 'POST':
return attach_volume(request, server_id)
else:
return api.api_method_not_allowed(request,
allowed_methods=['GET', 'POST'])
def demux_volumes_item(request, server_id, volume_id):
if request.method == 'GET':
return get_volume_info(request, server_id, volume_id)
elif request.method == 'DELETE':
return detach_volume(request, server_id, volume_id)
else:
return api.api_method_not_allowed(request,
allowed_methods=['GET', 'DELETE'])
def nic_to_attachments(nic):
"""Convert a NIC object to 'attachments attribute.
......@@ -965,3 +988,69 @@ def remove_floating_ip(request, vm, args):
% address)
servers.delete_port(floating_ip.nic)
return HttpResponse(status=202)
def volume_to_attachment(volume):
return {"id": volume.id,
"volumeId": volume.id,
"serverId": volume.machine_id,
"device": ""} # TODO: What device to return?
@api.api_method(http_method='GET', user_required=True, logger=log)
def get_volumes(request, server_id):
log.debug("get_volumes server_id %s", server_id)
vm = util.get_vm(server_id, request.user_uniq, for_update=False)
# TODO: Filter attachments!!
volumes = vm.volumes.filter(deleted=False).order_by("id")
attachments = [volume_to_attachment(v) for v in volumes]
data = json.dumps({'volumeAttachments': attachments})
return HttpResponse(data, status=200)
pass
@api.api_method(http_method='GET', user_required=True, logger=log)
def get_volume_info(request, server_id, volume_id):
log.debug("get_volume_info server_id %s volume_id", server_id, volume_id)
user_id = request.user_uniq
vm = util.get_vm(server_id, user_id)
volume = get_volume(user_id, volume_id, for_update=False,
exception=faults.BadRequest)
servers._check_attachment(vm, volume)
attachment = volume_to_attachment(volume)
data = json.dumps({'volumeAttachment': attachment})
return HttpResponse(data, status=200)
@api.api_method(http_method='POST', user_required=True, logger=log)
def attach_volume(request, server_id):
req = utils.get_request_dict(request)
log.debug("attach_volume server_id %s request", server_id, req)
user_id = request.user_uniq
vm = util.get_vm(server_id, user_id, for_update=True)
attachment_dict = api.utils.get_attribute(req, "volumeAttachment",
required=True)
# Get volume
volume_id = api.utils.get_attribute(attachment_dict, "volumeId")
volume = get_volume(user_id, volume_id, for_update=True,
exception=faults.BadRequest)
vm = server_attachments.attach_volume(vm, volume)
attachment = volume_to_attachment(volume)
data = json.dumps({'volumeAttachment': attachment})
return HttpResponse(data, status=202)
@api.api_method(http_method='DELETE', user_required=True, logger=log)
def detach_volume(request, server_id, volume_id):
log.debug("detach_volume server_id %s volume_id", server_id, volume_id)
user_id = request.user_uniq
vm = util.get_vm(server_id, user_id)
volume = get_volume(user_id, volume_id, for_update=True,
exception=faults.BadRequest)
vm = server_attachments.detach_volume(vm, volume)
# TODO: Check volume state, send job to detach volume
return HttpResponse(status=202)
......@@ -881,3 +881,82 @@ class ServerVNCConsole(ComputeAPITest):
response = self.mypost('servers/%d/action' % vm.id,
vm.userid, data, 'json')
self.assertBadRequest(response)
@patch('synnefo.logic.rapi_pool.GanetiRapiClient')
class ServerAttachments(ComputeAPITest):
def test_list_attachments(self, mrapi):
# Test default volume
vol = mfactory.VolumeFactory()
vm = vol.machine
response = self.myget("servers/%d/os-volume_attachments" % vm.id,
vm.userid)
self.assertSuccess(response)
attachments = json.loads(response.content)
self.assertEqual(len(attachments), 1)
self.assertEqual(attachments["volumeAttachments"][0],
{"volumeId": vol.id,
"serverId": vm.id,
"id": vol.id,
"device": ""})
# Test deleted Volume
dvol = mfactory.VolumeFactory(machine=vm, deleted=True)
response = self.myget("servers/%d/os-volume_attachments" % vm.id,
vm.userid)
self.assertSuccess(response)
attachments = json.loads(response.content)["volumeAttachments"]
self.assertEqual(len([d for d in attachments if d["id"] == dvol.id]),
0)
def test_attach_detach_volume(self, mrapi):
vol = mfactory.VolumeFactory(status="AVAILABLE")
vm = vol.machine
disk_template = vm.flavor.disk_template
# Test that we cannot detach the root volume
response = self.mydelete("servers/%d/os-volume_attachments/%d" %
(vm.id, vol.id), vm.userid)
self.assertBadRequest(response)
# Test that we cannot attach a used volume
vol1 = mfactory.VolumeFactory(status="IN_USE",
disk_template=disk_template,
userid=vm.userid)
request = json.dumps({"volumeAttachment": {"volumeId": vol1.id}})
response = self.mypost("servers/%d/os-volume_attachments" %
vm.id, vm.userid,
request, "json")
self.assertBadRequest(response)
# We cannot attach a volume of different disk template
vol1.status = "AVAILABLE"
vol1.disk_template = "lalalal"
vol1.save()
response = self.mypost("servers/%d/os-volume_attachments/" %
vm.id, vm.userid,
request, "json")
self.assertBadRequest(response)
vol1.disk_template = disk_template
vol1.save()
mrapi().ModifyInstance.return_value = 43
response = self.mypost("servers/%d/os-volume_attachments" %
vm.id, vm.userid,
request, "json")
self.assertEqual(response.status_code, 202, response.content)
attachment = json.loads(response.content)["volumeAttachment"]
self.assertEqual(attachment, {"volumeId": vol1.id,
"serverId": vm.id,
"id": vol1.id,
"device": ""})
# And we delete it...will fail because of status
response = self.mydelete("servers/%d/os-volume_attachments/%d" %
(vm.id, vol1.id), vm.userid)
self.assertBadRequest(response)
vm.task = None
vm.save()
vm.volumes.all().update(status="IN_USE")
response = self.mydelete("servers/%d/os-volume_attachments/%d" %
(vm.id, vol1.id), vm.userid)
self.assertEqual(response.status_code, 202, response.content)
......@@ -112,6 +112,18 @@ class VirtualMachineFactory(factory.DjangoModelFactory):
operstate = "STARTED"
class VolumeFactory(factory.DjangoModelFactory):
FACTORY_FOR = models.Volume
userid = factory.Sequence(user_seq())
size = factory.Sequence(lambda x: x, type=int)
name = factory.Sequence(lambda x: "volume-name-"+x, type=str)
machine = factory.SubFactory(VirtualMachineFactory,
userid=factory.SelfAttribute('..userid'))
disk_template = factory.LazyAttribute(lambda v:
v.machine.flavor.disk_template
if v.machine else "drbd")
class DeletedVirtualMachine(VirtualMachineFactory):
deleted = True
......@@ -287,10 +299,3 @@ class IPAddressLogFactory(factory.DjangoModelFactory):
server_id = 1
network_id = 1
active = True
class VolumeFactory(factory.DjangoModelFactory):
FACTORY_FOR = models.Volume
userid = factory.Sequence(user_seq())
size = factory.Sequence(lambda x: x, type=int)
name = factory.Sequence(lambda x: "volume-name-"+x, type=str)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment