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 <tomaz@apache.org>
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 <bernieke@bernieke.com>
Authored: Fri Jun 28 15:14:59 2013 +0200
Committer: Tomaz Muraus <tomaz@apache.org>
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.
|