INFINIDAT: Use infinisdk for communication with storage array
The infinisdk package provides Python bindings to the InfiniBox REST API and enables easier development of the INFINIDAT driver. Change-Id: I7c469d92cea90592f5e9cdc8127328ed76ef025d Closes-Bug: 1691411
This commit is contained in:
parent
e2fda841d5
commit
921205a8f2
@ -14,14 +14,8 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
"""Unit tests for INFINIDAT InfiniBox volume driver."""
|
"""Unit tests for INFINIDAT InfiniBox volume driver."""
|
||||||
|
|
||||||
import copy
|
|
||||||
import json
|
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
from oslo_utils import units
|
from oslo_utils import units
|
||||||
import requests
|
|
||||||
import six
|
|
||||||
from six.moves import http_client
|
|
||||||
|
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder import test
|
from cinder import test
|
||||||
@ -29,67 +23,8 @@ from cinder.volume import configuration
|
|||||||
from cinder.volume.drivers import infinidat
|
from cinder.volume.drivers import infinidat
|
||||||
|
|
||||||
|
|
||||||
BASE_URL = 'http://mockbox/api/rest/'
|
|
||||||
GET_VOLUME_URL = BASE_URL + 'volumes?name=openstack-vol-1'
|
|
||||||
GET_SNAP_URL = BASE_URL + 'volumes?name=openstack-snap-2'
|
|
||||||
GET_CLONE_URL = BASE_URL + 'volumes?name=openstack-vol-3'
|
|
||||||
GET_INTERNAL_CLONE_URL = BASE_URL + 'volumes?name=openstack-vol-3-internal'
|
|
||||||
VOLUMES_URL = BASE_URL + 'volumes'
|
|
||||||
VOLUME_URL = BASE_URL + 'volumes/1'
|
|
||||||
VOLUME_MAPPING_URL = BASE_URL + 'volumes/1/luns'
|
|
||||||
SNAPSHOT_URL = BASE_URL + 'volumes/2'
|
|
||||||
TEST_WWN_1 = '00:11:22:33:44:55:66:77'
|
TEST_WWN_1 = '00:11:22:33:44:55:66:77'
|
||||||
TEST_WWN_2 = '11:11:22:33:44:55:66:77'
|
TEST_WWN_2 = '11:11:22:33:44:55:66:77'
|
||||||
GET_HOST_URL = BASE_URL + 'hosts?name=openstack-host-0011223344556677'
|
|
||||||
GET_HOST2_URL = BASE_URL + 'hosts?name=openstack-host-1111223344556677'
|
|
||||||
HOSTS_URL = BASE_URL + 'hosts'
|
|
||||||
GET_POOL_URL = BASE_URL + 'pools?name=mockpool'
|
|
||||||
MAP_URL = BASE_URL + 'hosts/10/luns'
|
|
||||||
MAP2_URL = BASE_URL + 'hosts/11/luns'
|
|
||||||
ADD_PORT_URL = BASE_URL + 'hosts/10/ports'
|
|
||||||
UNMAP_URL = BASE_URL + 'hosts/10/luns/volume_id/1'
|
|
||||||
FC_PORT_URL = BASE_URL + 'components/nodes?fields=fc_ports'
|
|
||||||
APPROVAL = '?approved=true'
|
|
||||||
|
|
||||||
VOLUME_RESULT = dict(id=1,
|
|
||||||
write_protected=False,
|
|
||||||
has_children=False,
|
|
||||||
parent_id=0)
|
|
||||||
VOLUME_RESULT_WP = dict(id=1,
|
|
||||||
write_protected=True,
|
|
||||||
has_children=False,
|
|
||||||
parent_id=0)
|
|
||||||
SNAPSHOT_RESULT = dict(id=2,
|
|
||||||
write_protected=True,
|
|
||||||
has_children=False,
|
|
||||||
parent_id=0)
|
|
||||||
HOST_RESULT = dict(id=10, luns=[])
|
|
||||||
HOST2_RESULT = dict(id=11, luns=[])
|
|
||||||
POOL_RESULT = dict(id=100,
|
|
||||||
free_physical_space=units.Gi,
|
|
||||||
physical_capacity=units.Gi)
|
|
||||||
|
|
||||||
GOOD_PATH_RESPONSES = dict(GET={GET_VOLUME_URL: [VOLUME_RESULT],
|
|
||||||
GET_HOST_URL: [HOST_RESULT],
|
|
||||||
GET_HOST2_URL: [HOST2_RESULT],
|
|
||||||
GET_POOL_URL: [POOL_RESULT],
|
|
||||||
GET_SNAP_URL: [SNAPSHOT_RESULT],
|
|
||||||
GET_CLONE_URL: [VOLUME_RESULT],
|
|
||||||
GET_INTERNAL_CLONE_URL: [VOLUME_RESULT],
|
|
||||||
VOLUME_MAPPING_URL: [],
|
|
||||||
SNAPSHOT_URL: SNAPSHOT_RESULT,
|
|
||||||
FC_PORT_URL: [],
|
|
||||||
MAP_URL: [],
|
|
||||||
MAP2_URL: []},
|
|
||||||
POST={VOLUMES_URL: VOLUME_RESULT,
|
|
||||||
HOSTS_URL: HOST_RESULT,
|
|
||||||
MAP_URL + APPROVAL: dict(lun=1),
|
|
||||||
MAP2_URL + APPROVAL: dict(lun=1),
|
|
||||||
ADD_PORT_URL: None},
|
|
||||||
PUT={VOLUME_URL + APPROVAL: VOLUME_RESULT},
|
|
||||||
DELETE={UNMAP_URL + APPROVAL: None,
|
|
||||||
VOLUME_URL + APPROVAL: None,
|
|
||||||
SNAPSHOT_URL + APPROVAL: None})
|
|
||||||
|
|
||||||
test_volume = mock.Mock(id=1, size=1)
|
test_volume = mock.Mock(id=1, size=1)
|
||||||
test_snapshot = mock.Mock(id=2, volume=test_volume)
|
test_snapshot = mock.Mock(id=2, volume=test_volume)
|
||||||
@ -97,6 +32,10 @@ test_clone = mock.Mock(id=3, size=1)
|
|||||||
test_connector = dict(wwpns=[TEST_WWN_1])
|
test_connector = dict(wwpns=[TEST_WWN_1])
|
||||||
|
|
||||||
|
|
||||||
|
class FakeInfinisdkException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InfiniboxDriverTestCase(test.TestCase):
|
class InfiniboxDriverTestCase(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(InfiniboxDriverTestCase, self).setUp()
|
super(InfiniboxDriverTestCase, self).setUp()
|
||||||
@ -116,29 +55,43 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
|
|
||||||
self.driver = infinidat.InfiniboxVolumeDriver(
|
self.driver = infinidat.InfiniboxVolumeDriver(
|
||||||
configuration=self.configuration)
|
configuration=self.configuration)
|
||||||
|
self._system = self._infinibox_mock()
|
||||||
|
infinisdk = self.patch("cinder.volume.drivers.infinidat.infinisdk")
|
||||||
|
capacity = self.patch("cinder.volume.drivers.infinidat.capacity")
|
||||||
|
capacity.byte = 1
|
||||||
|
capacity.GiB = units.Gi
|
||||||
|
infinisdk.core.exceptions.InfiniSDKException = FakeInfinisdkException
|
||||||
|
infinisdk.InfiniBox.return_value = self._system
|
||||||
self.driver.do_setup(None)
|
self.driver.do_setup(None)
|
||||||
self.driver._session = mock.Mock(spec=requests.Session)
|
|
||||||
self.driver._session.request.side_effect = self._request
|
|
||||||
self._responses = copy.deepcopy(GOOD_PATH_RESPONSES)
|
|
||||||
|
|
||||||
def _request(self, action, url, **kwargs):
|
def _infinibox_mock(self):
|
||||||
result = self._responses[action][url]
|
result = mock.Mock()
|
||||||
response = requests.Response()
|
self._mock_volume = mock.Mock()
|
||||||
if type(result) == int:
|
self._mock_volume.has_children.return_value = False
|
||||||
# tests set the response to an int of a bad status code if they
|
self._mock_volume.get_logical_units.return_value = []
|
||||||
# want the api call to fail
|
self._mock_volume.create_child.return_value = self._mock_volume
|
||||||
response.status_code = result
|
self._mock_host = mock.Mock()
|
||||||
response.raw = six.BytesIO(six.b(json.dumps(dict())))
|
self._mock_host.get_luns.return_value = []
|
||||||
else:
|
self._mock_host.map_volume().get_lun.return_value = 1
|
||||||
response.status_code = http_client.OK
|
self._mock_pool = mock.Mock()
|
||||||
response.raw = six.BytesIO(six.b(json.dumps(dict(result=result))))
|
self._mock_pool.get_free_physical_capacity.return_value = units.Gi
|
||||||
return response
|
self._mock_pool.get_physical_capacity.return_value = units.Gi
|
||||||
|
result.volumes.safe_get.return_value = self._mock_volume
|
||||||
|
result.volumes.create.return_value = self._mock_volume
|
||||||
|
result.pools.safe_get.return_value = self._mock_pool
|
||||||
|
result.hosts.safe_get.return_value = self._mock_host
|
||||||
|
result.hosts.create.return_value = self._mock_host
|
||||||
|
result.components.nodes.get_all.return_value = []
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _raise_infinisdk(self, *args, **kwargs):
|
||||||
|
raise FakeInfinisdkException()
|
||||||
|
|
||||||
def test_get_volume_stats_refreshes(self):
|
def test_get_volume_stats_refreshes(self):
|
||||||
result = self.driver.get_volume_stats()
|
result = self.driver.get_volume_stats()
|
||||||
self.assertEqual(1, result["free_capacity_gb"])
|
self.assertEqual(1, result["free_capacity_gb"])
|
||||||
# change the "free space" in the pool
|
# change the "free space" in the pool
|
||||||
self._responses["GET"][GET_POOL_URL][0]["free_physical_space"] = 0
|
self._mock_pool.get_free_physical_capacity.return_value = 0
|
||||||
# no refresh - free capacity should stay the same
|
# no refresh - free capacity should stay the same
|
||||||
result = self.driver.get_volume_stats(refresh=False)
|
result = self.driver.get_volume_stats(refresh=False)
|
||||||
self.assertEqual(1, result["free_capacity_gb"])
|
self.assertEqual(1, result["free_capacity_gb"])
|
||||||
@ -147,12 +100,12 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
self.assertEqual(0, result["free_capacity_gb"])
|
self.assertEqual(0, result["free_capacity_gb"])
|
||||||
|
|
||||||
def test_get_volume_stats_pool_not_found(self):
|
def test_get_volume_stats_pool_not_found(self):
|
||||||
self._responses["GET"][GET_POOL_URL] = []
|
self._system.pools.safe_get.return_value = None
|
||||||
self.assertRaises(exception.VolumeDriverException,
|
self.assertRaises(exception.VolumeDriverException,
|
||||||
self.driver.get_volume_stats)
|
self.driver.get_volume_stats)
|
||||||
|
|
||||||
def test_initialize_connection(self):
|
def test_initialize_connection(self):
|
||||||
self._responses["GET"][GET_HOST_URL] = [] # host doesn't exist yet
|
self._system.hosts.safe_get.return_value = None
|
||||||
result = self.driver.initialize_connection(test_volume, test_connector)
|
result = self.driver.initialize_connection(test_volume, test_connector)
|
||||||
self.assertEqual(1, result["data"]["target_lun"])
|
self.assertEqual(1, result["data"]["target_lun"])
|
||||||
|
|
||||||
@ -161,7 +114,10 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
self.assertEqual(1, result["data"]["target_lun"])
|
self.assertEqual(1, result["data"]["target_lun"])
|
||||||
|
|
||||||
def test_initialize_connection_mapping_exists(self):
|
def test_initialize_connection_mapping_exists(self):
|
||||||
self._responses["GET"][MAP_URL] = [{'lun': 888, 'volume_id': 1}]
|
mock_mapping = mock.Mock()
|
||||||
|
mock_mapping.get_volume.return_value = self._mock_volume
|
||||||
|
mock_mapping.get_lun.return_value = 888
|
||||||
|
self._mock_host.get_luns.return_value = [mock_mapping]
|
||||||
result = self.driver.initialize_connection(test_volume, test_connector)
|
result = self.driver.initialize_connection(test_volume, test_connector)
|
||||||
self.assertEqual(888, result["data"]["target_lun"])
|
self.assertEqual(888, result["data"]["target_lun"])
|
||||||
|
|
||||||
@ -171,20 +127,20 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
self.assertEqual(1, result["data"]["target_lun"])
|
self.assertEqual(1, result["data"]["target_lun"])
|
||||||
|
|
||||||
def test_initialize_connection_volume_doesnt_exist(self):
|
def test_initialize_connection_volume_doesnt_exist(self):
|
||||||
self._responses["GET"][GET_VOLUME_URL] = []
|
self._system.volumes.safe_get.return_value = None
|
||||||
self.assertRaises(exception.InvalidVolume,
|
self.assertRaises(exception.InvalidVolume,
|
||||||
self.driver.initialize_connection,
|
self.driver.initialize_connection,
|
||||||
test_volume, test_connector)
|
test_volume, test_connector)
|
||||||
|
|
||||||
def test_initialize_connection_create_fails(self):
|
def test_initialize_connection_create_fails(self):
|
||||||
self._responses["GET"][GET_HOST_URL] = [] # host doesn't exist yet
|
self._system.hosts.safe_get.return_value = None
|
||||||
self._responses["POST"][HOSTS_URL] = 500
|
self._system.hosts.create.side_effect = self._raise_infinisdk
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.initialize_connection,
|
self.driver.initialize_connection,
|
||||||
test_volume, test_connector)
|
test_volume, test_connector)
|
||||||
|
|
||||||
def test_initialize_connection_map_fails(self):
|
def test_initialize_connection_map_fails(self):
|
||||||
self._responses["POST"][MAP_URL + APPROVAL] = 500
|
self._mock_host.map_volume.side_effect = self._raise_infinisdk
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.initialize_connection,
|
self.driver.initialize_connection,
|
||||||
test_volume, test_connector)
|
test_volume, test_connector)
|
||||||
@ -193,13 +149,13 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
self.driver.terminate_connection(test_volume, test_connector)
|
self.driver.terminate_connection(test_volume, test_connector)
|
||||||
|
|
||||||
def test_terminate_connection_volume_doesnt_exist(self):
|
def test_terminate_connection_volume_doesnt_exist(self):
|
||||||
self._responses["GET"][GET_VOLUME_URL] = []
|
self._system.volumes.safe_get.return_value = None
|
||||||
self.assertRaises(exception.InvalidVolume,
|
self.assertRaises(exception.InvalidVolume,
|
||||||
self.driver.terminate_connection,
|
self.driver.terminate_connection,
|
||||||
test_volume, test_connector)
|
test_volume, test_connector)
|
||||||
|
|
||||||
def test_terminate_connection_api_fail(self):
|
def test_terminate_connection_api_fail(self):
|
||||||
self._responses["DELETE"][UNMAP_URL + APPROVAL] = 500
|
self._mock_host.unmap_volume.side_effect = self._raise_infinisdk
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.terminate_connection,
|
self.driver.terminate_connection,
|
||||||
test_volume, test_connector)
|
test_volume, test_connector)
|
||||||
@ -208,12 +164,12 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
self.driver.create_volume(test_volume)
|
self.driver.create_volume(test_volume)
|
||||||
|
|
||||||
def test_create_volume_pool_not_found(self):
|
def test_create_volume_pool_not_found(self):
|
||||||
self._responses["GET"][GET_POOL_URL] = []
|
self._system.pools.safe_get.return_value = None
|
||||||
self.assertRaises(exception.VolumeDriverException,
|
self.assertRaises(exception.VolumeDriverException,
|
||||||
self.driver.create_volume, test_volume)
|
self.driver.create_volume, test_volume)
|
||||||
|
|
||||||
def test_create_volume_api_fail(self):
|
def test_create_volume_api_fail(self):
|
||||||
self._responses["POST"][VOLUMES_URL] = 500
|
self._system.pools.safe_get.side_effect = self._raise_infinisdk
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.create_volume, test_volume)
|
self.driver.create_volume, test_volume)
|
||||||
|
|
||||||
@ -221,34 +177,20 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
self.driver.delete_volume(test_volume)
|
self.driver.delete_volume(test_volume)
|
||||||
|
|
||||||
def test_delete_volume_doesnt_exist(self):
|
def test_delete_volume_doesnt_exist(self):
|
||||||
self._responses["GET"][GET_VOLUME_URL] = []
|
self._system.volumes.safe_get.return_value = None
|
||||||
# should not raise an exception
|
# should not raise an exception
|
||||||
self.driver.delete_volume(test_volume)
|
self.driver.delete_volume(test_volume)
|
||||||
|
|
||||||
def test_delete_volume_doesnt_exist_on_delete(self):
|
|
||||||
self._responses["DELETE"][VOLUME_URL + APPROVAL] = (
|
|
||||||
http_client.NOT_FOUND)
|
|
||||||
# due to a possible race condition (get+delete is not atomic) the
|
|
||||||
# GET may return the volume but it may still be deleted before
|
|
||||||
# the DELETE request
|
|
||||||
# In this case we still should not raise an exception
|
|
||||||
self.driver.delete_volume(test_volume)
|
|
||||||
|
|
||||||
def test_delete_volume_with_children(self):
|
def test_delete_volume_with_children(self):
|
||||||
self._responses["GET"][GET_VOLUME_URL][0]['has_children'] = True
|
self._mock_volume.has_children.return_value = True
|
||||||
self.assertRaises(exception.VolumeIsBusy,
|
self.assertRaises(exception.VolumeIsBusy,
|
||||||
self.driver.delete_volume, test_volume)
|
self.driver.delete_volume, test_volume)
|
||||||
|
|
||||||
def test_delete_volume_api_fail(self):
|
|
||||||
self._responses["DELETE"][VOLUME_URL + APPROVAL] = 500
|
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
|
||||||
self.driver.delete_volume, test_volume)
|
|
||||||
|
|
||||||
def test_extend_volume(self):
|
def test_extend_volume(self):
|
||||||
self.driver.extend_volume(test_volume, 2)
|
self.driver.extend_volume(test_volume, 2)
|
||||||
|
|
||||||
def test_extend_volume_api_fail(self):
|
def test_extend_volume_api_fail(self):
|
||||||
self._responses["PUT"][VOLUME_URL + APPROVAL] = 500
|
self._mock_volume.resize.side_effect = self._raise_infinisdk
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.extend_volume, test_volume, 2)
|
self.driver.extend_volume, test_volume, 2)
|
||||||
|
|
||||||
@ -256,12 +198,12 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
self.driver.create_snapshot(test_snapshot)
|
self.driver.create_snapshot(test_snapshot)
|
||||||
|
|
||||||
def test_create_snapshot_volume_doesnt_exist(self):
|
def test_create_snapshot_volume_doesnt_exist(self):
|
||||||
self._responses["GET"][GET_VOLUME_URL] = []
|
self._system.volumes.safe_get.return_value = None
|
||||||
self.assertRaises(exception.InvalidVolume,
|
self.assertRaises(exception.InvalidVolume,
|
||||||
self.driver.create_snapshot, test_snapshot)
|
self.driver.create_snapshot, test_snapshot)
|
||||||
|
|
||||||
def test_create_snapshot_api_fail(self):
|
def test_create_snapshot_api_fail(self):
|
||||||
self._responses["POST"][VOLUMES_URL] = 500
|
self._mock_volume.create_snapshot.side_effect = self._raise_infinisdk
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.create_snapshot, test_snapshot)
|
self.driver.create_snapshot, test_snapshot)
|
||||||
|
|
||||||
@ -273,13 +215,13 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
self.driver.create_volume_from_snapshot(test_clone, test_snapshot)
|
self.driver.create_volume_from_snapshot(test_clone, test_snapshot)
|
||||||
|
|
||||||
def test_create_volume_from_snapshot_doesnt_exist(self):
|
def test_create_volume_from_snapshot_doesnt_exist(self):
|
||||||
self._responses["GET"][GET_SNAP_URL] = []
|
self._system.volumes.safe_get.return_value = None
|
||||||
self.assertRaises(exception.InvalidSnapshot,
|
self.assertRaises(exception.InvalidSnapshot,
|
||||||
self.driver.create_volume_from_snapshot,
|
self.driver.create_volume_from_snapshot,
|
||||||
test_clone, test_snapshot)
|
test_clone, test_snapshot)
|
||||||
|
|
||||||
def test_create_volume_from_snapshot_create_fails(self):
|
def test_create_volume_from_snapshot_create_fails(self):
|
||||||
self._responses["POST"][VOLUMES_URL] = 500
|
self._mock_volume.create_child.side_effect = self._raise_infinisdk
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.create_volume_from_snapshot,
|
self.driver.create_volume_from_snapshot,
|
||||||
test_clone, test_snapshot)
|
test_clone, test_snapshot)
|
||||||
@ -287,7 +229,7 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
@mock.patch("cinder.utils.brick_get_connector_properties",
|
@mock.patch("cinder.utils.brick_get_connector_properties",
|
||||||
return_value=test_connector)
|
return_value=test_connector)
|
||||||
def test_create_volume_from_snapshot_map_fails(self, *mocks):
|
def test_create_volume_from_snapshot_map_fails(self, *mocks):
|
||||||
self._responses["POST"][MAP_URL + APPROVAL] = 500
|
self._mock_host.map_volume.side_effect = self._raise_infinisdk
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.create_volume_from_snapshot,
|
self.driver.create_volume_from_snapshot,
|
||||||
test_clone, test_snapshot)
|
test_clone, test_snapshot)
|
||||||
@ -297,7 +239,7 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
@mock.patch("cinder.utils.brick_get_connector_properties",
|
@mock.patch("cinder.utils.brick_get_connector_properties",
|
||||||
return_value=test_connector)
|
return_value=test_connector)
|
||||||
def test_create_volume_from_snapshot_delete_clone_fails(self, *mocks):
|
def test_create_volume_from_snapshot_delete_clone_fails(self, *mocks):
|
||||||
self._responses["DELETE"][VOLUME_URL + APPROVAL] = 500
|
self._mock_volume.delete.side_effect = self._raise_infinisdk
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.create_volume_from_snapshot,
|
self.driver.create_volume_from_snapshot,
|
||||||
test_clone, test_snapshot)
|
test_clone, test_snapshot)
|
||||||
@ -306,21 +248,12 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
self.driver.delete_snapshot(test_snapshot)
|
self.driver.delete_snapshot(test_snapshot)
|
||||||
|
|
||||||
def test_delete_snapshot_doesnt_exist(self):
|
def test_delete_snapshot_doesnt_exist(self):
|
||||||
self._responses["GET"][GET_SNAP_URL] = []
|
self._system.volumes.safe_get.return_value = None
|
||||||
# should not raise an exception
|
# should not raise an exception
|
||||||
self.driver.delete_snapshot(test_snapshot)
|
self.driver.delete_snapshot(test_snapshot)
|
||||||
|
|
||||||
def test_delete_snapshot_doesnt_exist_on_delete(self):
|
|
||||||
self._responses["DELETE"][SNAPSHOT_URL + APPROVAL] = (
|
|
||||||
http_client.NOT_FOUND)
|
|
||||||
# due to a possible race condition (get+delete is not atomic) the
|
|
||||||
# GET may return the snapshot but it may still be deleted before
|
|
||||||
# the DELETE request
|
|
||||||
# In this case we still should not raise an exception
|
|
||||||
self.driver.delete_snapshot(test_snapshot)
|
|
||||||
|
|
||||||
def test_delete_snapshot_api_fail(self):
|
def test_delete_snapshot_api_fail(self):
|
||||||
self._responses["DELETE"][SNAPSHOT_URL + APPROVAL] = 500
|
self._mock_volume.safe_delete.side_effect = self._raise_infinisdk
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.delete_snapshot, test_snapshot)
|
self.driver.delete_snapshot, test_snapshot)
|
||||||
|
|
||||||
@ -332,14 +265,15 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
self.driver.create_cloned_volume(test_clone, test_volume)
|
self.driver.create_cloned_volume(test_clone, test_volume)
|
||||||
|
|
||||||
def test_create_cloned_volume_volume_already_mapped(self):
|
def test_create_cloned_volume_volume_already_mapped(self):
|
||||||
test_lun = [{'lun': 888, 'host_id': 10}]
|
mock_mapping = mock.Mock()
|
||||||
self._responses["GET"][VOLUME_MAPPING_URL] = test_lun
|
mock_mapping.get_volume.return_value = self._mock_volume
|
||||||
|
self._mock_volume.get_logical_units.return_value = [mock_mapping]
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.create_cloned_volume,
|
self.driver.create_cloned_volume,
|
||||||
test_clone, test_volume)
|
test_clone, test_volume)
|
||||||
|
|
||||||
def test_create_cloned_volume_create_fails(self):
|
def test_create_cloned_volume_create_fails(self):
|
||||||
self._responses["POST"][VOLUMES_URL] = 500
|
self._system.volumes.create.side_effect = self._raise_infinisdk
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.create_cloned_volume,
|
self.driver.create_cloned_volume,
|
||||||
test_clone, test_volume)
|
test_clone, test_volume)
|
||||||
@ -347,7 +281,7 @@ class InfiniboxDriverTestCase(test.TestCase):
|
|||||||
@mock.patch("cinder.utils.brick_get_connector_properties",
|
@mock.patch("cinder.utils.brick_get_connector_properties",
|
||||||
return_value=test_connector)
|
return_value=test_connector)
|
||||||
def test_create_cloned_volume_map_fails(self, *mocks):
|
def test_create_cloned_volume_map_fails(self, *mocks):
|
||||||
self._responses["POST"][MAP_URL + APPROVAL] = 500
|
self._mock_host.map_volume.side_effect = self._raise_infinisdk
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver.create_cloned_volume,
|
self.driver.create_cloned_volume,
|
||||||
test_clone, test_volume)
|
test_clone, test_volume)
|
||||||
|
@ -17,13 +17,12 @@ INFINIDAT InfiniBox Volume Driver
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
import functools
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import units
|
from oslo_utils import units
|
||||||
import requests
|
|
||||||
import six
|
|
||||||
|
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
@ -33,11 +32,19 @@ from cinder.volume.drivers.san import san
|
|||||||
from cinder.volume import utils as vol_utils
|
from cinder.volume import utils as vol_utils
|
||||||
from cinder.zonemanager import utils as fczm_utils
|
from cinder.zonemanager import utils as fczm_utils
|
||||||
|
|
||||||
|
try:
|
||||||
|
# capacity is a dependency of infinisdk, so if infinisdk is available
|
||||||
|
# then capacity should be available too
|
||||||
|
import capacity
|
||||||
|
import infinisdk
|
||||||
|
except ImportError:
|
||||||
|
capacity = None
|
||||||
|
infinisdk = None
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
VENDOR_NAME = 'INFINIDAT'
|
VENDOR_NAME = 'INFINIDAT'
|
||||||
DELETE_URI = 'volumes/%s?approved=true'
|
|
||||||
|
|
||||||
infinidat_opts = [
|
infinidat_opts = [
|
||||||
cfg.StrOpt('infinidat_pool_name',
|
cfg.StrOpt('infinidat_pool_name',
|
||||||
@ -48,9 +55,22 @@ CONF = cfg.CONF
|
|||||||
CONF.register_opts(infinidat_opts)
|
CONF.register_opts(infinidat_opts)
|
||||||
|
|
||||||
|
|
||||||
|
def infinisdk_to_cinder_exceptions(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except infinisdk.core.exceptions.InfiniSDKException as ex:
|
||||||
|
# string formatting of 'ex' includes http code and url
|
||||||
|
msg = _('Caught exception from infinisdk: %s') % ex
|
||||||
|
LOG.exception(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@interface.volumedriver
|
@interface.volumedriver
|
||||||
class InfiniboxVolumeDriver(san.SanDriver):
|
class InfiniboxVolumeDriver(san.SanDriver):
|
||||||
VERSION = '1.0'
|
VERSION = '1.1'
|
||||||
|
|
||||||
# ThirdPartySystems wiki page
|
# ThirdPartySystems wiki page
|
||||||
CI_WIKI_NAME = "INFINIDAT_Cinder_CI"
|
CI_WIKI_NAME = "INFINIDAT_Cinder_CI"
|
||||||
@ -62,53 +82,19 @@ class InfiniboxVolumeDriver(san.SanDriver):
|
|||||||
|
|
||||||
def do_setup(self, context):
|
def do_setup(self, context):
|
||||||
"""Driver initialization"""
|
"""Driver initialization"""
|
||||||
self._session = requests.Session()
|
if infinisdk is None:
|
||||||
self._session.auth = (self.configuration.san_login,
|
msg = _("Missing 'infinisdk' python module, ensure the library"
|
||||||
|
" is installed and available.")
|
||||||
|
raise exception.VolumeDriverException(message=msg)
|
||||||
|
auth = (self.configuration.san_login,
|
||||||
self.configuration.san_password)
|
self.configuration.san_password)
|
||||||
management_address = self.configuration.san_ip
|
management_address = self.configuration.san_ip
|
||||||
self._base_url = 'http://%s/api/rest/' % management_address
|
self._system = infinisdk.InfiniBox(management_address, auth=auth)
|
||||||
|
self._system.login()
|
||||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||||
self._backend_name = backend_name or self.__class__.__name__
|
self._backend_name = backend_name or self.__class__.__name__
|
||||||
self._volume_stats = None
|
self._volume_stats = None
|
||||||
LOG.debug('setup complete. base url: %s', self._base_url)
|
LOG.debug('setup complete')
|
||||||
|
|
||||||
def _request(self, action, uri, data=None):
|
|
||||||
LOG.debug('--> %(action)s %(uri)s %(data)r',
|
|
||||||
{'action': action, 'uri': uri, 'data': data})
|
|
||||||
response = self._session.request(action,
|
|
||||||
self._base_url + uri,
|
|
||||||
json=data)
|
|
||||||
LOG.debug('<-- %(status_code)s %(response_json)r',
|
|
||||||
{'status_code': response.status_code,
|
|
||||||
'response_json': response.json()})
|
|
||||||
try:
|
|
||||||
response.raise_for_status()
|
|
||||||
except requests.HTTPError as ex:
|
|
||||||
# text_type(ex) includes http code and url
|
|
||||||
msg = _('InfiniBox storage array returned %(exception)s\n'
|
|
||||||
'Data: %(data)s\n'
|
|
||||||
'Response: %(response_json)s') % {
|
|
||||||
'exception': six.text_type(ex),
|
|
||||||
'data': repr(data),
|
|
||||||
'response_json': repr(response.json())}
|
|
||||||
LOG.exception(msg)
|
|
||||||
if response.status_code == 404:
|
|
||||||
raise exception.NotFound()
|
|
||||||
else:
|
|
||||||
raise exception.VolumeBackendAPIException(data=msg)
|
|
||||||
return response.json()['result']
|
|
||||||
|
|
||||||
def _get(self, uri):
|
|
||||||
return self._request('GET', uri)
|
|
||||||
|
|
||||||
def _post(self, uri, data):
|
|
||||||
return self._request('POST', uri, data)
|
|
||||||
|
|
||||||
def _delete(self, uri):
|
|
||||||
return self._request('DELETE', uri)
|
|
||||||
|
|
||||||
def _put(self, uri, data):
|
|
||||||
return self._request('PUT', uri, data)
|
|
||||||
|
|
||||||
def _cleanup_wwpn(self, wwpn):
|
def _cleanup_wwpn(self, wwpn):
|
||||||
return wwpn.replace(':', '')
|
return wwpn.replace(':', '')
|
||||||
@ -124,87 +110,78 @@ class InfiniboxVolumeDriver(san.SanDriver):
|
|||||||
return 'openstack-host-%s' % wwn_for_name
|
return 'openstack-host-%s' % wwn_for_name
|
||||||
|
|
||||||
def _get_infinidat_volume_by_name(self, name):
|
def _get_infinidat_volume_by_name(self, name):
|
||||||
volumes = self._get('volumes?name=%s' % name)
|
volume = self._system.volumes.safe_get(name=name)
|
||||||
if len(volumes) != 1:
|
if volume is None:
|
||||||
msg = _('Volume "%s" not found') % name
|
msg = _('Volume "%s" not found') % name
|
||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
raise exception.InvalidVolume(reason=msg)
|
raise exception.InvalidVolume(reason=msg)
|
||||||
return volumes[0]
|
return volume
|
||||||
|
|
||||||
def _get_infinidat_snapshot_by_name(self, name):
|
def _get_infinidat_snapshot_by_name(self, name):
|
||||||
snapshots = self._get('volumes?name=%s' % name)
|
snapshot = self._system.volumes.safe_get(name=name)
|
||||||
if len(snapshots) != 1:
|
if snapshot is None:
|
||||||
msg = _('Snapshot "%s" not found') % name
|
msg = _('Snapshot "%s" not found') % name
|
||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
raise exception.InvalidSnapshot(reason=msg)
|
raise exception.InvalidSnapshot(reason=msg)
|
||||||
return snapshots[0]
|
return snapshot
|
||||||
|
|
||||||
def _get_infinidat_volume_id(self, cinder_volume):
|
def _get_infinidat_volume(self, cinder_volume):
|
||||||
volume_name = self._make_volume_name(cinder_volume)
|
volume_name = self._make_volume_name(cinder_volume)
|
||||||
return self._get_infinidat_volume_by_name(volume_name)['id']
|
return self._get_infinidat_volume_by_name(volume_name)
|
||||||
|
|
||||||
def _get_infinidat_snapshot_id(self, cinder_snapshot):
|
def _get_infinidat_snapshot(self, cinder_snapshot):
|
||||||
snap_name = self._make_snapshot_name(cinder_snapshot)
|
snap_name = self._make_snapshot_name(cinder_snapshot)
|
||||||
return self._get_infinidat_snapshot_by_name(snap_name)['id']
|
return self._get_infinidat_snapshot_by_name(snap_name)
|
||||||
|
|
||||||
def _get_infinidat_pool(self):
|
def _get_infinidat_pool(self):
|
||||||
pool_name = self.configuration.infinidat_pool_name
|
pool_name = self.configuration.infinidat_pool_name
|
||||||
pools = self._get('pools?name=%s' % pool_name)
|
pool = self._system.pools.safe_get(name=pool_name)
|
||||||
if len(pools) != 1:
|
if pool is None:
|
||||||
msg = _('Pool "%s" not found') % pool_name
|
msg = _('Pool "%s" not found') % pool_name
|
||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
raise exception.VolumeDriverException(message=msg)
|
raise exception.VolumeDriverException(message=msg)
|
||||||
return pools[0]
|
return pool
|
||||||
|
|
||||||
def _get_host(self, wwpn):
|
|
||||||
host_name = self._make_host_name(wwpn)
|
|
||||||
infinidat_hosts = self._get('hosts?name=%s' % host_name)
|
|
||||||
if len(infinidat_hosts) == 1:
|
|
||||||
return infinidat_hosts[0]
|
|
||||||
|
|
||||||
def _get_or_create_host(self, wwpn):
|
def _get_or_create_host(self, wwpn):
|
||||||
host_name = self._make_host_name(wwpn)
|
host_name = self._make_host_name(wwpn)
|
||||||
infinidat_host = self._get_host(wwpn)
|
infinidat_host = self._system.hosts.safe_get(name=host_name)
|
||||||
if infinidat_host is None:
|
if infinidat_host is None:
|
||||||
# create host
|
infinidat_host = self._system.hosts.create(name=host_name)
|
||||||
infinidat_host = self._post('hosts', dict(name=host_name))
|
infinidat_host.add_port(self._cleanup_wwpn(wwpn))
|
||||||
# add port to host
|
|
||||||
self._post('hosts/%s/ports' % infinidat_host['id'],
|
|
||||||
dict(type='FC', address=self._cleanup_wwpn(wwpn)))
|
|
||||||
return infinidat_host
|
return infinidat_host
|
||||||
|
|
||||||
def _get_mapping(self, host_id, volume_id):
|
def _get_mapping(self, host, volume):
|
||||||
existing_mapping = self._get("hosts/%s/luns" % host_id)
|
existing_mapping = host.get_luns()
|
||||||
for mapping in existing_mapping:
|
for mapping in existing_mapping:
|
||||||
if mapping['volume_id'] == volume_id:
|
if mapping.get_volume() == volume:
|
||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
def _get_or_create_mapping(self, host_id, volume_id):
|
def _get_or_create_mapping(self, host, volume):
|
||||||
mapping = self._get_mapping(host_id, volume_id)
|
mapping = self._get_mapping(host, volume)
|
||||||
if mapping:
|
if mapping:
|
||||||
return mapping
|
return mapping
|
||||||
# volume not mapped. map it
|
# volume not mapped. map it
|
||||||
uri = 'hosts/%s/luns?approved=true' % host_id
|
return host.map_volume(volume)
|
||||||
return self._post(uri, dict(volume_id=volume_id))
|
|
||||||
|
|
||||||
def _get_online_fc_ports(self):
|
def _get_online_fc_ports(self):
|
||||||
nodes = self._get('components/nodes?fields=fc_ports')
|
nodes = self._system.components.nodes.get_all()
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
for port in node['fc_ports']:
|
for port in node.get_fc_ports():
|
||||||
if (port['link_state'].lower() == 'up'
|
if (port.get_link_state().lower() == 'up' and
|
||||||
and port['state'] == 'OK'):
|
port.get_state() == 'OK'):
|
||||||
yield self._cleanup_wwpn(port['wwpn'])
|
yield str(port.get_wwpn())
|
||||||
|
|
||||||
@fczm_utils.add_fc_zone
|
@fczm_utils.add_fc_zone
|
||||||
|
@infinisdk_to_cinder_exceptions
|
||||||
def initialize_connection(self, volume, connector):
|
def initialize_connection(self, volume, connector):
|
||||||
"""Map an InfiniBox volume to the host"""
|
"""Map an InfiniBox volume to the host"""
|
||||||
volume_name = self._make_volume_name(volume)
|
volume_name = self._make_volume_name(volume)
|
||||||
infinidat_volume = self._get_infinidat_volume_by_name(volume_name)
|
infinidat_volume = self._get_infinidat_volume_by_name(volume_name)
|
||||||
for wwpn in connector['wwpns']:
|
for wwpn in connector['wwpns']:
|
||||||
infinidat_host = self._get_or_create_host(wwpn)
|
infinidat_host = self._get_or_create_host(wwpn)
|
||||||
mapping = self._get_or_create_mapping(infinidat_host['id'],
|
mapping = self._get_or_create_mapping(infinidat_host,
|
||||||
infinidat_volume['id'])
|
infinidat_volume)
|
||||||
lun = mapping['lun']
|
lun = mapping.get_lun()
|
||||||
|
|
||||||
# Create initiator-target mapping.
|
# Create initiator-target mapping.
|
||||||
target_wwpns = list(self._get_online_fc_ports())
|
target_wwpns = list(self._get_online_fc_ports())
|
||||||
@ -217,28 +194,25 @@ class InfiniboxVolumeDriver(san.SanDriver):
|
|||||||
initiator_target_map=init_target_map))
|
initiator_target_map=init_target_map))
|
||||||
|
|
||||||
@fczm_utils.remove_fc_zone
|
@fczm_utils.remove_fc_zone
|
||||||
|
@infinisdk_to_cinder_exceptions
|
||||||
def terminate_connection(self, volume, connector, **kwargs):
|
def terminate_connection(self, volume, connector, **kwargs):
|
||||||
"""Unmap an InfiniBox volume from the host"""
|
"""Unmap an InfiniBox volume from the host"""
|
||||||
volume_id = self._get_infinidat_volume_id(volume)
|
infinidat_volume = self._get_infinidat_volume(volume)
|
||||||
result_data = dict()
|
result_data = dict()
|
||||||
for wwpn in connector['wwpns']:
|
for wwpn in connector['wwpns']:
|
||||||
host_name = self._make_host_name(wwpn)
|
host_name = self._make_host_name(wwpn)
|
||||||
infinidat_hosts = self._get('hosts?name=%s' % host_name)
|
host = self._system.hosts.safe_get(name=host_name)
|
||||||
if len(infinidat_hosts) != 1:
|
if host is None:
|
||||||
# not found. ignore.
|
# not found. ignore.
|
||||||
continue
|
continue
|
||||||
host_id = infinidat_hosts[0]['id']
|
|
||||||
# unmap
|
# unmap
|
||||||
uri = ('hosts/%s/luns/volume_id/%s' % (host_id, volume_id) +
|
|
||||||
'?approved=true')
|
|
||||||
try:
|
try:
|
||||||
self._delete(uri)
|
host.unmap_volume(infinidat_volume)
|
||||||
except (exception.NotFound):
|
except KeyError:
|
||||||
continue # volume mapping not found
|
continue # volume mapping not found
|
||||||
# check if the host now doesn't have mappings, to delete host_entry
|
# check if the host now doesn't have mappings, to delete host_entry
|
||||||
# if needed
|
# if needed
|
||||||
infinidat_hosts = self._get('hosts?name=%s' % host_name)
|
if len(host.get_luns()) == 0:
|
||||||
if len(infinidat_hosts) == 1 and len(infinidat_hosts[0]['luns']) == 0:
|
|
||||||
# Create initiator-target mapping.
|
# Create initiator-target mapping.
|
||||||
target_wwpns = list(self._get_online_fc_ports())
|
target_wwpns = list(self._get_online_fc_ports())
|
||||||
target_wwpns, init_target_map = self._build_initiator_target_map(
|
target_wwpns, init_target_map = self._build_initiator_target_map(
|
||||||
@ -248,11 +222,16 @@ class InfiniboxVolumeDriver(san.SanDriver):
|
|||||||
return dict(driver_volume_type='fibre_channel',
|
return dict(driver_volume_type='fibre_channel',
|
||||||
data=result_data)
|
data=result_data)
|
||||||
|
|
||||||
|
@infinisdk_to_cinder_exceptions
|
||||||
def get_volume_stats(self, refresh=False):
|
def get_volume_stats(self, refresh=False):
|
||||||
if self._volume_stats is None or refresh:
|
if self._volume_stats is None or refresh:
|
||||||
pool = self._get_infinidat_pool()
|
pool = self._get_infinidat_pool()
|
||||||
free_capacity_gb = float(pool['free_physical_space']) / units.Gi
|
free_capacity_bytes = (pool.get_free_physical_capacity() /
|
||||||
total_capacity_gb = float(pool['physical_capacity']) / units.Gi
|
capacity.byte)
|
||||||
|
physical_capacity_bytes = (pool.get_physical_capacity() /
|
||||||
|
capacity.byte)
|
||||||
|
free_capacity_gb = float(free_capacity_bytes) / units.Gi
|
||||||
|
total_capacity_gb = float(physical_capacity_bytes) / units.Gi
|
||||||
self._volume_stats = dict(volume_backend_name=self._backend_name,
|
self._volume_stats = dict(volume_backend_name=self._backend_name,
|
||||||
vendor_name=VENDOR_NAME,
|
vendor_name=VENDOR_NAME,
|
||||||
driver_version=self.VERSION,
|
driver_version=self.VERSION,
|
||||||
@ -263,45 +242,46 @@ class InfiniboxVolumeDriver(san.SanDriver):
|
|||||||
return self._volume_stats
|
return self._volume_stats
|
||||||
|
|
||||||
def _create_volume(self, volume):
|
def _create_volume(self, volume):
|
||||||
# get pool id from name
|
|
||||||
pool = self._get_infinidat_pool()
|
pool = self._get_infinidat_pool()
|
||||||
# create volume
|
|
||||||
volume_name = self._make_volume_name(volume)
|
volume_name = self._make_volume_name(volume)
|
||||||
provtype = "THIN" if self.configuration.san_thin_provision else "THICK"
|
provtype = "THIN" if self.configuration.san_thin_provision else "THICK"
|
||||||
data = dict(pool_id=pool['id'],
|
size = volume.size * capacity.GiB
|
||||||
|
return self._system.volumes.create(name=volume_name,
|
||||||
|
pool=pool,
|
||||||
provtype=provtype,
|
provtype=provtype,
|
||||||
name=volume_name,
|
size=size)
|
||||||
size=volume.size * units.Gi)
|
|
||||||
return self._post('volumes', data)
|
|
||||||
|
|
||||||
|
@infinisdk_to_cinder_exceptions
|
||||||
def create_volume(self, volume):
|
def create_volume(self, volume):
|
||||||
"""Create a new volume on the backend."""
|
"""Create a new volume on the backend."""
|
||||||
# this is the same as _create_volume but without the return statement
|
# this is the same as _create_volume but without the return statement
|
||||||
self._create_volume(volume)
|
self._create_volume(volume)
|
||||||
|
|
||||||
|
@infinisdk_to_cinder_exceptions
|
||||||
def delete_volume(self, volume):
|
def delete_volume(self, volume):
|
||||||
"""Delete a volume from the backend."""
|
"""Delete a volume from the backend."""
|
||||||
try:
|
|
||||||
volume_name = self._make_volume_name(volume)
|
volume_name = self._make_volume_name(volume)
|
||||||
volume = self._get_infinidat_volume_by_name(volume_name)
|
try:
|
||||||
if volume['has_children']:
|
infinidat_volume = self._get_infinidat_volume_by_name(volume_name)
|
||||||
|
except exception.InvalidVolume:
|
||||||
|
return # volume not found
|
||||||
|
if infinidat_volume.has_children():
|
||||||
# can't delete a volume that has a live snapshot
|
# can't delete a volume that has a live snapshot
|
||||||
raise exception.VolumeIsBusy(volume_name=volume_name)
|
raise exception.VolumeIsBusy(volume_name=volume_name)
|
||||||
self._delete(DELETE_URI % volume['id'])
|
infinidat_volume.safe_delete()
|
||||||
except (exception.InvalidVolume, exception.NotFound):
|
|
||||||
return # volume not found
|
|
||||||
|
|
||||||
|
@infinisdk_to_cinder_exceptions
|
||||||
def extend_volume(self, volume, new_size):
|
def extend_volume(self, volume, new_size):
|
||||||
"""Extend the size of a volume."""
|
"""Extend the size of a volume."""
|
||||||
volume_id = self._get_infinidat_volume_id(volume)
|
volume = self._get_infinidat_volume(volume)
|
||||||
self._put('volumes/%s?approved=true' % volume_id,
|
volume.resize(new_size * capacity.GiB)
|
||||||
dict(size=new_size * units.Gi))
|
|
||||||
|
|
||||||
|
@infinisdk_to_cinder_exceptions
|
||||||
def create_snapshot(self, snapshot):
|
def create_snapshot(self, snapshot):
|
||||||
"""Creates a snapshot."""
|
"""Creates a snapshot."""
|
||||||
volume_id = self._get_infinidat_volume_id(snapshot.volume)
|
volume = self._get_infinidat_volume(snapshot.volume)
|
||||||
name = self._make_snapshot_name(snapshot)
|
name = self._make_snapshot_name(snapshot)
|
||||||
self._post('volumes', dict(parent_id=volume_id, name=name))
|
volume.create_snapshot(name=name)
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _device_connect_context(self, volume):
|
def _device_connect_context(self, volume):
|
||||||
@ -312,6 +292,7 @@ class InfiniboxVolumeDriver(san.SanDriver):
|
|||||||
finally:
|
finally:
|
||||||
self.terminate_connection(volume, connector)
|
self.terminate_connection(volume, connector)
|
||||||
|
|
||||||
|
@infinisdk_to_cinder_exceptions
|
||||||
def create_volume_from_snapshot(self, volume, snapshot):
|
def create_volume_from_snapshot(self, volume, snapshot):
|
||||||
"""Create volume from snapshot.
|
"""Create volume from snapshot.
|
||||||
|
|
||||||
@ -323,10 +304,9 @@ class InfiniboxVolumeDriver(san.SanDriver):
|
|||||||
- copy data from clone to volume
|
- copy data from clone to volume
|
||||||
- unmap volume and clone and delete the clone
|
- unmap volume and clone and delete the clone
|
||||||
"""
|
"""
|
||||||
snapshot_id = self._get_infinidat_snapshot_id(snapshot)
|
infinidat_snapshot = self._get_infinidat_snapshot(snapshot)
|
||||||
clone_name = self._make_volume_name(volume) + '-internal'
|
clone_name = self._make_volume_name(volume) + '-internal'
|
||||||
infinidat_clone = self._post('volumes', dict(parent_id=snapshot_id,
|
infinidat_clone = infinidat_snapshot.create_child(name=clone_name)
|
||||||
name=clone_name))
|
|
||||||
# we need a cinder-volume-like object to map the clone by name
|
# we need a cinder-volume-like object to map the clone by name
|
||||||
# (which is derived from the cinder id) but the clone is internal
|
# (which is derived from the cinder id) but the clone is internal
|
||||||
# so there is no such object. mock one
|
# so there is no such object. mock one
|
||||||
@ -344,26 +324,24 @@ class InfiniboxVolumeDriver(san.SanDriver):
|
|||||||
dd_block_size,
|
dd_block_size,
|
||||||
sparse=True)
|
sparse=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
self._delete(DELETE_URI % infinidat_volume['id'])
|
infinidat_volume.delete()
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
self._delete(DELETE_URI % infinidat_clone['id'])
|
infinidat_clone.delete()
|
||||||
|
|
||||||
|
@infinisdk_to_cinder_exceptions
|
||||||
def delete_snapshot(self, snapshot):
|
def delete_snapshot(self, snapshot):
|
||||||
"""Deletes a snapshot."""
|
"""Deletes a snapshot."""
|
||||||
try:
|
try:
|
||||||
snapshot_name = self._make_snapshot_name(snapshot)
|
snapshot = self._get_infinidat_snapshot(snapshot)
|
||||||
snapshot = self._get_infinidat_snapshot_by_name(snapshot_name)
|
except exception.InvalidSnapshot:
|
||||||
self._delete(DELETE_URI % snapshot['id'])
|
|
||||||
except (exception.InvalidSnapshot, exception.NotFound):
|
|
||||||
return # snapshot not found
|
return # snapshot not found
|
||||||
|
snapshot.safe_delete()
|
||||||
|
|
||||||
def _asssert_volume_not_mapped(self, volume):
|
def _asssert_volume_not_mapped(self, volume):
|
||||||
# copy is not atomic so we can't clone while the volume is mapped
|
# copy is not atomic so we can't clone while the volume is mapped
|
||||||
volume_name = self._make_volume_name(volume)
|
infinidat_volume = self._get_infinidat_volume(volume)
|
||||||
infinidat_volume = self._get_infinidat_volume_by_name(volume_name)
|
if len(infinidat_volume.get_logical_units()) == 0:
|
||||||
mappings = self._get("volumes/%s/luns" % infinidat_volume['id'])
|
|
||||||
if len(mappings) == 0:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# volume has mappings
|
# volume has mappings
|
||||||
@ -374,6 +352,7 @@ class InfiniboxVolumeDriver(san.SanDriver):
|
|||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
raise exception.VolumeBackendAPIException(data=msg)
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
@infinisdk_to_cinder_exceptions
|
||||||
def create_cloned_volume(self, volume, src_vref):
|
def create_cloned_volume(self, volume, src_vref):
|
||||||
"""Create a clone from source volume.
|
"""Create a clone from source volume.
|
||||||
|
|
||||||
@ -398,7 +377,7 @@ class InfiniboxVolumeDriver(san.SanDriver):
|
|||||||
dd_block_size,
|
dd_block_size,
|
||||||
sparse=True)
|
sparse=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
self._delete(DELETE_URI % infinidat_volume['id'])
|
infinidat_volume.delete()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _build_initiator_target_map(self, connector, all_target_wwns):
|
def _build_initiator_target_map(self, connector, all_target_wwns):
|
||||||
|
@ -36,3 +36,7 @@ storops>=0.4.8 # Apache-2.0
|
|||||||
|
|
||||||
# Violin
|
# Violin
|
||||||
vmemclient>=1.1.8 # Apache-2.0
|
vmemclient>=1.1.8 # Apache-2.0
|
||||||
|
|
||||||
|
# INFINIDAT
|
||||||
|
infinisdk # BSD-3
|
||||||
|
capacity # BSD
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
upgrade:
|
||||||
|
- INFINIDAT volume driver now requires the 'infinisdk' python module to be
|
||||||
|
installed.
|
Loading…
Reference in New Issue
Block a user