Updated Branches: refs/heads/0.13.x 36f8a94d2 -> b3a7467bf refs/heads/trunk c0cc8f986 -> b44a0f658
Issue LIBCLOUD-353: Implement storageVolume methods for openstack driver. Signed-off-by: Tomaz Muraus <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/91159b3e Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/91159b3e Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/91159b3e Branch: refs/heads/trunk Commit: 91159b3e094eb4b8f50c89d475f897cda217b2c3 Parents: c0cc8f9 Author: Bernard Kerckenaere <[email protected]> Authored: Fri Jun 28 15:14:59 2013 +0200 Committer: Tomaz Muraus <[email protected]> Committed: Thu Jul 11 15:57:55 2013 +0200 ---------------------------------------------------------------------- libcloud/compute/drivers/openstack.py | 87 +++++++++++++++++++- libcloud/test/compute/__init__.py | 12 ++- .../fixtures/openstack_v1.1/_os_volumes.json | 39 +++++++++ ...es_cd76a3a1_c4ce_40f6_9b9f_07a61508938d.json | 22 +++++ .../openstack_v1.1/_os_volumes_create.json | 17 ++++ .../_servers_12065_os_volume_attachments.json | 8 ++ libcloud/test/compute/test_openstack.py | 80 +++++++++++++++++- 7 files changed, 261 insertions(+), 4 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/91159b3e/libcloud/compute/drivers/openstack.py ---------------------------------------------------------------------- diff --git a/libcloud/compute/drivers/openstack.py b/libcloud/compute/drivers/openstack.py index 8c2401a..4904443 100644 --- a/libcloud/compute/drivers/openstack.py +++ b/libcloud/compute/drivers/openstack.py @@ -34,10 +34,10 @@ from xml.etree import ElementTree as ET from libcloud.common.openstack import OpenStackBaseConnection from libcloud.common.openstack import OpenStackDriverMixin -from libcloud.common.types import MalformedResponseError +from libcloud.common.types import MalformedResponseError, ProviderError from libcloud.compute.types import NodeState, Provider from libcloud.compute.base import NodeSize, NodeImage -from libcloud.compute.base import NodeDriver, Node, NodeLocation +from libcloud.compute.base import NodeDriver, Node, NodeLocation, StorageVolume from libcloud.pricing import get_size_price from libcloud.common.base import Response from libcloud.utils.xml import findall @@ -59,6 +59,10 @@ ATOM_NAMESPACE = "http://www.w3.org/2005/Atom" DEFAULT_API_VERSION = '1.1' +class OpenStackException(ProviderError): + pass + + class OpenStackResponse(Response): node_driver = None @@ -206,6 +210,67 @@ class OpenStackNodeDriver(NodeDriver, OpenStackDriverMixin): return self._to_nodes( self.connection.request('/servers/detail').object) + def create_volume(self, size, name, location=None, snapshot=None): + if snapshot: + raise NotImplementedError( + "create_volume does not yet support create from snapshot") + return self.connection.request('/os-volumes', + method='POST', + data={ + 'volume': { + 'display_name': name, + 'display_description': name, + 'size': size, + 'volume_type': None, + 'metadata': { + 'contents': name, + }, + 'availability_zone': location, + } + }).success() + + def destroy_volume(self, volume): + return self.connection.request('/os-volumes/%s' % volume.id, + method='DELETE').success() + + def attach_volume(self, node, volume, device="auto"): + # when "auto" or None is provided for device, openstack will let + # the guest OS pick the next available device (fi. /dev/vdb) + return self.connection.request( + '/servers/%s/os-volume_attachments' % node.id, + method='POST', + data={ + 'volumeAttachment': { + 'volumeId': volume.id, + 'device': device, + } + }).success() + + def detach_volume(self, volume, ex_node=None): + # when ex_node is not provided, volume is detached from all nodes + failed_nodes = [] + for attachment in volume.extra['attachments']: + if not ex_node or ex_node.id == attachment['serverId']: + if not self.connection.request( + '/servers/%s/os-volume_attachments/%s' % + (attachment['serverId'], attachment['id']), + method='DELETE').success(): + failed_nodes.append(attachment['serverId']) + if failed_nodes: + raise OpenStackException( + 'detach_volume failed for nodes with id: %s' % + ', '.join(failed_nodes), 500, self + ) + return True + + def list_volumes(self): + return self._to_volumes( + self.connection.request('/os-volumes').object) + + def ex_get_volume(self, volumeId): + return self._to_volume( + self.connection.request('/os-volumes/%s' % volumeId).object) + def list_images(self, location=None, ex_only_active=True): """ @inherits: L{NodeDriver.list_images} @@ -1153,6 +1218,10 @@ class OpenStack_1_1_NodeDriver(OpenStackNodeDriver): servers = obj['servers'] return [self._to_node(server) for server in servers] + def _to_volumes(self, obj): + volumes = obj['volumes'] + return [self._to_volume(volume) for volume in volumes] + def _to_sizes(self, obj): flavors = obj['flavors'] return [self._to_size(flavor) for flavor in flavors] @@ -1658,6 +1727,20 @@ class OpenStack_1_1_NodeDriver(OpenStackNodeDriver): ), ) + def _to_volume(self, api_node): + if 'volume' in api_node: + api_node = api_node['volume'] + return StorageVolume( + id=api_node['id'], + name=api_node['displayName'], + size=api_node['size'], + driver=self, + extra={ + 'description': api_node['displayDescription'], + 'attachments': [att for att in api_node['attachments'] if att], + } + ) + def _to_size(self, api_flavor, price=None, bandwidth=None): # if provider-specific subclasses can get better values for # price/bandwidth, then can pass them in when they super(). http://git-wip-us.apache.org/repos/asf/libcloud/blob/91159b3e/libcloud/test/compute/__init__.py ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/__init__.py b/libcloud/test/compute/__init__.py index ae754cf..1ac4a11 100644 --- a/libcloud/test/compute/__init__.py +++ b/libcloud/test/compute/__init__.py @@ -13,13 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from libcloud.compute.base import Node, NodeImage, NodeLocation +from libcloud.compute.base import Node, NodeImage, NodeLocation, StorageVolume from libcloud.pricing import get_pricing class TestCaseMixin(object): should_list_locations = True should_have_pricing = False + should_list_volumes = False def test_list_nodes_response(self): nodes = self.driver.list_nodes() @@ -46,6 +47,15 @@ class TestCaseMixin(object): for image in images: self.assertTrue(isinstance(image, NodeImage)) + def test_list_volumes_response(self): + if not self.should_list_volumes: + return None + + volumes = self.driver.list_volumes() + self.assertTrue(isinstance(volumes, list)) + for volume in volumes: + self.assertTrue(isinstance(volume, StorageVolume)) + def test_list_locations_response(self): if not self.should_list_locations: return None http://git-wip-us.apache.org/repos/asf/libcloud/blob/91159b3e/libcloud/test/compute/fixtures/openstack_v1.1/_os_volumes.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/openstack_v1.1/_os_volumes.json b/libcloud/test/compute/fixtures/openstack_v1.1/_os_volumes.json new file mode 100644 index 0000000..d92e3e8 --- /dev/null +++ b/libcloud/test/compute/fixtures/openstack_v1.1/_os_volumes.json @@ -0,0 +1,39 @@ +{ + "volumes": [ + { + "attachments": [ + { + "device": "/dev/vdb", + "id": "cd76a3a1-c4ce-40f6-9b9f-07a61508938d", + "serverId": "12065", + "volumeId": "cd76a3a1-c4ce-40f6-9b9f-07a61508938d" + } + ], + "availabilityZone": "nova", + "createdAt": "2013-06-24T11:20:13.000000", + "displayDescription": "", + "displayName": "test_volume_2", + "id": "cd76a3a1-c4ce-40f6-9b9f-07a61508938d", + "metadata": {}, + "size": 2, + "snapshotId": null, + "status": "available", + "volumeType": "None" + }, + { + "attachments": [ + {} + ], + "availabilityZone": "nova", + "createdAt": "2013-06-21T12:39:02.000000", + "displayDescription": "some description", + "displayName": "test_volume", + "id": "cfcec3bc-b736-4db5-9535-4c24112691b5", + "metadata": {}, + "size": 50, + "snapshotId": null, + "status": "available", + "volumeType": "None" + } + ] +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/91159b3e/libcloud/test/compute/fixtures/openstack_v1.1/_os_volumes_cd76a3a1_c4ce_40f6_9b9f_07a61508938d.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/openstack_v1.1/_os_volumes_cd76a3a1_c4ce_40f6_9b9f_07a61508938d.json b/libcloud/test/compute/fixtures/openstack_v1.1/_os_volumes_cd76a3a1_c4ce_40f6_9b9f_07a61508938d.json new file mode 100644 index 0000000..b2c580f --- /dev/null +++ b/libcloud/test/compute/fixtures/openstack_v1.1/_os_volumes_cd76a3a1_c4ce_40f6_9b9f_07a61508938d.json @@ -0,0 +1,22 @@ +{ + "volume": { + "attachments": [ + { + "device": "/dev/vdb", + "id": "cd76a3a1-c4ce-40f6-9b9f-07a61508938d", + "serverId": "12065", + "volumeId": "cd76a3a1-c4ce-40f6-9b9f-07a61508938d" + } + ], + "availabilityZone": "nova", + "createdAt": "2013-06-24T11:20:13.000000", + "displayDescription": "", + "displayName": "test_volume_2", + "id": "cd76a3a1-c4ce-40f6-9b9f-07a61508938d", + "metadata": {}, + "size": 2, + "snapshotId": null, + "status": "in-use", + "volumeType": "None" + } +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/91159b3e/libcloud/test/compute/fixtures/openstack_v1.1/_os_volumes_create.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/openstack_v1.1/_os_volumes_create.json b/libcloud/test/compute/fixtures/openstack_v1.1/_os_volumes_create.json new file mode 100644 index 0000000..d1a0cdf --- /dev/null +++ b/libcloud/test/compute/fixtures/openstack_v1.1/_os_volumes_create.json @@ -0,0 +1,17 @@ +{ + "volume": { + "attachments": [ + {} + ], + "availabilityZone": "nova", + "createdAt": "2013-06-28T12:22:39.616660", + "displayDescription": null, + "displayName": "test", + "id": "43b7db44-0497-40fa-b817-c906f13bbea3", + "metadata": {}, + "size": 1, + "snapshotId": null, + "status": "creating", + "volumeType": "None" + } +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/91159b3e/libcloud/test/compute/fixtures/openstack_v1.1/_servers_12065_os_volume_attachments.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/openstack_v1.1/_servers_12065_os_volume_attachments.json b/libcloud/test/compute/fixtures/openstack_v1.1/_servers_12065_os_volume_attachments.json new file mode 100644 index 0000000..c381384 --- /dev/null +++ b/libcloud/test/compute/fixtures/openstack_v1.1/_servers_12065_os_volume_attachments.json @@ -0,0 +1,8 @@ +{ + "volumeAttachment": { + "device": "/dev/vdb", + "id": "cd76a3a1-c4ce-40f6-9b9f-07a61508938d", + "serverId": "12065", + "volumeId": "cd76a3a1-c4ce-40f6-9b9f-07a61508938d" + } +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/91159b3e/libcloud/test/compute/test_openstack.py ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/test_openstack.py b/libcloud/test/compute/test_openstack.py index 09ce48d..730dced 100644 --- a/libcloud/test/compute/test_openstack.py +++ b/libcloud/test/compute/test_openstack.py @@ -39,7 +39,7 @@ from libcloud.compute.drivers.openstack import ( OpenStack_1_0_NodeDriver, OpenStack_1_0_Response, OpenStack_1_1_NodeDriver, OpenStackSecurityGroup, OpenStackSecurityGroupRule ) -from libcloud.compute.base import Node, NodeImage, NodeSize +from libcloud.compute.base import Node, NodeImage, NodeSize, StorageVolume from libcloud.pricing import set_pricing, clear_pricing_data from libcloud.test import MockResponse, MockHttpTestCase, XML_HEADERS @@ -226,6 +226,7 @@ class OpenStackAuthConnectionTests(unittest.TestCase): class OpenStack_1_0_Tests(unittest.TestCase, TestCaseMixin): should_list_locations = False + should_list_volumes = False driver_klass = OpenStack_1_0_NodeDriver driver_args = OPENSTACK_PARAMS @@ -529,6 +530,7 @@ class OpenStack_1_0_Tests(unittest.TestCase, TestCaseMixin): class OpenStack_1_0_FactoryMethodTests(OpenStack_1_0_Tests): should_list_locations = False + should_list_volumes = False driver_klass = OpenStack_1_0_NodeDriver driver_type = get_driver(Provider.OPENSTACK) @@ -698,6 +700,7 @@ class OpenStackMockHttp(MockHttpTestCase): class OpenStack_1_1_Tests(unittest.TestCase, TestCaseMixin): should_list_locations = False + should_list_volumes = True driver_klass = OpenStack_1_1_NodeDriver driver_type = OpenStack_1_1_NodeDriver @@ -823,6 +826,26 @@ class OpenStack_1_1_Tests(unittest.TestCase, TestCaseMixin): self.assertEqual(node.extra['updated'], '2011-10-11T00:50:04Z') self.assertEqual(node.extra['created'], '2011-10-11T00:51:39Z') + def test_list_volumes(self): + volumes = self.driver.list_volumes() + self.assertEqual(len(volumes), 2) + volume = volumes[0] + + self.assertEqual('cd76a3a1-c4ce-40f6-9b9f-07a61508938d', volume.id) + self.assertEqual('test_volume_2', volume.name) + self.assertEqual(2, volume.size) + + self.assertEqual(volume.extra['description'], '') + self.assertEqual(volume.extra['attachments'][0]['id'], 'cd76a3a1-c4ce-40f6-9b9f-07a61508938d') + + volume = volumes[1] + self.assertEqual('cfcec3bc-b736-4db5-9535-4c24112691b5', volume.id) + self.assertEqual('test_volume', volume.name) + self.assertEqual(50, volume.size) + + self.assertEqual(volume.extra['description'], 'some description') + self.assertEqual(volume.extra['attachments'], []) + def test_list_sizes(self): sizes = self.driver.list_sizes() self.assertEqual(len(sizes), 8, 'Wrong sizes count') @@ -891,6 +914,24 @@ class OpenStack_1_1_Tests(unittest.TestCase, TestCaseMixin): def test_reboot_node(self): self.assertTrue(self.node.reboot()) + def test_create_volume(self): + self.assertEqual(self.driver.create_volume(1, 'test'), True) + + def test_destroy_volume(self): + volume = self.driver.ex_get_volume('cd76a3a1-c4ce-40f6-9b9f-07a61508938d') + self.assertEqual(self.driver.destroy_volume(volume), True) + + def test_attach_volume(self): + node = self.driver.list_nodes()[0] + volume = self.driver.ex_get_volume('cd76a3a1-c4ce-40f6-9b9f-07a61508938d') + self.assertEqual(self.driver.attach_volume(node, volume, '/dev/sdb'), True) + + def test_detach_volume(self): + node = self.driver.list_nodes()[0] + volume = self.driver.ex_get_volume('cd76a3a1-c4ce-40f6-9b9f-07a61508938d') + self.assertEqual(self.driver.attach_volume(node, volume, '/dev/sdb'), True) + self.assertEqual(self.driver.detach_volume(volume), True) + def test_ex_set_password(self): try: self.driver.ex_set_password(self.node, 'New1&53jPass') @@ -1102,6 +1143,7 @@ class OpenStack_1_1_Tests(unittest.TestCase, TestCaseMixin): class OpenStack_1_1_FactoryMethodTests(OpenStack_1_1_Tests): should_list_locations = False + should_list_volumes = True driver_klass = OpenStack_1_1_NodeDriver driver_type = get_driver(Provider.OPENSTACK) @@ -1291,6 +1333,42 @@ class OpenStack_1_1_MockHttp(MockHttpTestCase): else: raise NotImplementedError() + def _v1_1_slug_os_volumes(self, method, url, body, headers): + if method == "GET": + body = self.fixtures.load('_os_volumes.json') + elif method == "POST": + body = self.fixtures.load('_os_volumes_create.json') + else: + raise NotImplementedError() + + return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) + + def _v1_1_slug_os_volumes_cd76a3a1_c4ce_40f6_9b9f_07a61508938d(self, method, url, body, headers): + if method == "GET": + body = self.fixtures.load('_os_volumes_cd76a3a1_c4ce_40f6_9b9f_07a61508938d.json') + elif method == "DELETE": + body = '' + else: + raise NotImplementedError() + + return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) + + def _v1_1_slug_servers_12065_os_volume_attachments(self, method, url, body, headers): + if method == "POST": + body = self.fixtures.load('_servers_12065_os_volume_attachments.json') + else: + raise NotImplementedError() + + return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) + + def _v1_1_slug_servers_12065_os_volume_attachments_cd76a3a1_c4ce_40f6_9b9f_07a61508938d(self, method, url, body, headers): + if method == "DELETE": + body = '' + else: + raise NotImplementedError() + + return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) + # This exists because the nova compute url in devstack has v2 in there but the v1.1 fixtures # work fine.
