# License for the specific language governing permissions and limitations
# under the License.
-import json
-import urllib2
-
import mock
from oslo.utils import units
from oslo_concurrency import processutils
from cinder import exception
from cinder import test
+
+
+import sys
+sys.modules['purestorage'] = mock.Mock()
from cinder.volume.drivers import pure
DRIVER_PATH = "cinder.volume.drivers.pure"
}
+class FakePureStorageHTTPError(Exception):
+ def __init__(self, target=None, rest_version=None, code=None,
+ headers=None, text=None):
+ self.target = target
+ self.rest_version = rest_version
+ self.code = code
+ self.headers = headers
+ self.text = text
+
+
class PureISCSIDriverTestCase(test.TestCase):
def setUp(self):
self.config.pure_api_token = API_TOKEN
self.config.volume_backend_name = VOLUME_BACKEND_NAME
self.driver = pure.PureISCSIDriver(configuration=self.config)
- self.array = mock.create_autospec(pure.FlashArray)
+ self.array = mock.Mock()
self.driver._array = self.array
+ self.purestorage_module = pure.purestorage
+ self.purestorage_module.PureHTTPError = FakePureStorageHTTPError
- @mock.patch(ARRAY_OBJ, autospec=True)
@mock.patch(DRIVER_OBJ + "._choose_target_iscsi_port")
- def test_do_setup(self, mock_choose_target_iscsi_port, mock_array):
+ def test_do_setup(self, mock_choose_target_iscsi_port):
mock_choose_target_iscsi_port.return_value = ISCSI_PORTS[0]
- mock_array.return_value = self.array
+ self.purestorage_module.FlashArray.return_value = self.array
+ self.array.get_rest_version.return_value = \
+ self.driver.SUPPORTED_REST_API_VERSIONS[0]
self.driver.do_setup(None)
- mock_array.assert_called_with(TARGET, API_TOKEN)
+ self.purestorage_module.FlashArray.assert_called_with(
+ TARGET,
+ api_token=API_TOKEN
+ )
self.assertEqual(self.array, self.driver._array)
+ self.assertEqual(
+ self.driver.SUPPORTED_REST_API_VERSIONS,
+ self.purestorage_module.FlashArray.supported_rest_versions
+ )
mock_choose_target_iscsi_port.assert_called_with()
self.assertEqual(ISCSI_PORTS[0], self.driver._iscsi_port)
self.assert_error_propagates(
- [mock_array, mock_choose_target_iscsi_port],
- self.driver.do_setup, None)
+ [
+ self.purestorage_module.FlashArray,
+ mock_choose_target_iscsi_port
+ ],
+ self.driver.do_setup, None
+ )
def assert_error_propagates(self, mocks, func, *args, **kwargs):
"""Assert that errors from mocks propagate to func.
vol_name)
def test_delete_volume_already_deleted(self):
- self.array.list_volume_hosts.side_effect = exception.PureAPIException(
- code=400, reason="Volume does not exist")
+ self.array.list_volume_private_connections.side_effect = \
+ self.purestorage_module.PureHTTPError(
+ code=400,
+ text="Volume does not exist"
+ )
self.driver.delete_volume(VOLUME)
self.assertFalse(self.array.destroy_volume.called)
- self.array.list_volume_hosts.side_effect = None
- self.assert_error_propagates([self.array.destroy_volume],
- self.driver.delete_volume, VOLUME)
+
# Testing case where array.destroy_volume returns an exception
- # because volume already deleted
- self.array.destroy_volume.side_effect = exception.PureAPIException(
- code=400, reason="Volume does not exist")
+ # because volume has already been deleted
+ self.array.list_volume_private_connections.side_effect = None
+ self.array.list_volume_private_connections.return_value = {}
+ self.array.destroy_volume.side_effect = \
+ self.purestorage_module.PureHTTPError(
+ code=400,
+ text="Volume does not exist"
+ )
self.driver.delete_volume(VOLUME)
self.assertTrue(self.array.destroy_volume.called)
- self.array.destroy_volume.side_effect = None
- self.assert_error_propagates([self.array.destroy_volume],
- self.driver.delete_volume, VOLUME)
def test_delete_volume(self):
vol_name = VOLUME["name"] + "-cinder"
+ self.array.list_volume_private_connections.return_value = {}
self.driver.delete_volume(VOLUME)
expected = [mock.call.destroy_volume(vol_name)]
self.array.assert_has_calls(expected)
- self.array.destroy_volume.side_effect = exception.PureAPIException(
- code=400, reason="reason")
+ self.array.destroy_volume.side_effect = \
+ self.purestorage_module.PureHTTPError(code=400, text="reason")
self.driver.delete_snapshot(SNAPSHOT)
self.array.destroy_volume.side_effect = None
self.assert_error_propagates([self.array.destroy_volume],
vol_name = VOLUME["name"] + "-cinder"
host_name_a = "ha"
host_name_b = "hb"
- self.array.list_volume_hosts.return_value = [{
+ self.array.list_volume_private_connections.return_value = [{
"host": host_name_a,
"lun": 7,
"name": vol_name,
}]
self.driver.delete_volume(VOLUME)
- expected = [mock.call.list_volume_hosts(vol_name),
+ expected = [mock.call.list_volume_private_connections(vol_name),
mock.call.disconnect_host(host_name_a, vol_name),
mock.call.disconnect_host(host_name_b, vol_name),
mock.call.destroy_volume(vol_name)]
def test_create_snapshot(self):
vol_name = SRC_VOL["name"] + "-cinder"
self.driver.create_snapshot(SNAPSHOT)
- self.array.create_snapshot.assert_called_with(vol_name,
- SNAPSHOT["name"])
+ self.array.create_snapshot.assert_called_with(
+ vol_name,
+ suffix=SNAPSHOT["name"]
+ )
self.assert_error_propagates([self.array.create_snapshot],
self.driver.create_snapshot, SNAPSHOT)
self.driver.delete_snapshot(SNAPSHOT)
expected = [mock.call.destroy_volume(snap_name)]
self.array.assert_has_calls(expected)
- self.array.destroy_volume.side_effect = exception.PureAPIException(
- code=400, reason="reason")
+ self.array.destroy_volume.side_effect = \
+ self.purestorage_module.PureHTTPError(code=400, text="reason")
self.driver.delete_snapshot(SNAPSHOT)
self.array.destroy_volume.side_effect = None
self.assert_error_propagates([self.array.destroy_volume],
def test_connect_already_connected(self, mock_host):
mock_host.return_value = PURE_HOST
expected = {"host": PURE_HOST_NAME, "lun": 1}
- self.array.list_volume_hosts.return_value = \
+ self.array.list_volume_private_connections.return_value = \
[expected, {"host": "extra", "lun": 2}]
- self.array.connect_host.side_effect = exception.PureAPIException(
- code=400, reason="Connection already exists")
+ self.array.connect_host.side_effect = \
+ self.purestorage_module.PureHTTPError(
+ code=400,
+ text="Connection already exists"
+ )
actual = self.driver._connect(VOLUME, CONNECTOR)
self.assertEqual(expected, actual)
self.assertTrue(self.array.connect_host.called)
- self.assertTrue(self.array.list_volume_hosts)
+ self.assertTrue(self.array.list_volume_private_connections)
@mock.patch(DRIVER_OBJ + "._get_host", autospec=True)
def test_connect_already_connected_list_hosts_empty(self, mock_host):
mock_host.return_value = PURE_HOST
- self.array.list_volume_hosts.return_value = []
- self.array.connect_host.side_effect = exception.PureAPIException(
- code=400, reason="Connection already exists")
- self.assertRaises(exception.PureDriverException,
- lambda: self.driver._connect(VOLUME, CONNECTOR))
+ self.array.list_volume_private_connections.return_value = {}
+ self.array.connect_host.side_effect = \
+ self.purestorage_module.PureHTTPError(
+ code=400,
+ text="Connection already exists"
+ )
+ self.assertRaises(exception.PureDriverException, self.driver._connect,
+ VOLUME, CONNECTOR)
self.assertTrue(self.array.connect_host.called)
- self.assertTrue(self.array.list_volume_hosts)
+ self.assertTrue(self.array.list_volume_private_connections)
@mock.patch(DRIVER_OBJ + "._get_host", autospec=True)
def test_connect_already_connected_list_hosts_exception(self, mock_host):
mock_host.return_value = PURE_HOST
- self.array.list_volume_hosts.side_effect = \
- exception.PureAPIException(code=400, reason="")
- self.array.connect_host.side_effect = exception.PureAPIException(
- code=400, reason="Connection already exists")
- self.assertRaises(exception.PureAPIException,
- lambda: self.driver._connect(VOLUME, CONNECTOR))
+ self.array.list_volume_private_connections.side_effect = \
+ self.purestorage_module.PureHTTPError(code=400, text="")
+ self.array.connect_host.side_effect = \
+ self.purestorage_module.PureHTTPError(
+ code=400,
+ text="Connection already exists"
+ )
+ self.assertRaises(self.purestorage_module.PureHTTPError,
+ self.driver._connect, VOLUME, CONNECTOR)
self.assertTrue(self.array.connect_host.called)
- self.assertTrue(self.array.list_volume_hosts)
+ self.assertTrue(self.array.list_volume_private_connections)
def test_get_host(self):
good_host = PURE_HOST.copy()
self.array.delete_host.assert_called_with(PURE_HOST_NAME)
# Branch where connection is missing and the host is still deleted
self.array.reset_mock()
- self.array.disconnect_host.side_effect = exception.PureAPIException(
- code=400, reason="reason")
+ self.array.disconnect_host.side_effect = \
+ self.purestorage_module.PureHTTPError(code=400, text="reason")
self.driver.terminate_connection(VOLUME, CONNECTOR)
self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name)
self.array.list_host_connections.assert_called_with(PURE_HOST_NAME,
self.array.delete_host.assert_called_with(PURE_HOST_NAME)
# Branch where an unexpected exception occurs
self.array.reset_mock()
- self.array.disconnect_host.side_effect = exception.PureAPIException(
- code=500, reason="unexpected exception")
- self.assertRaises(exception.PureAPIException,
+ self.array.disconnect_host.side_effect = \
+ self.purestorage_module.PureHTTPError(
+ code=500,
+ text="Some other error"
+ )
+ self.assertRaises(self.purestorage_module.PureHTTPError,
self.driver.terminate_connection, VOLUME, CONNECTOR)
self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name)
self.assertFalse(self.array.list_host_connections.called)
def test_get_volume_stats(self):
self.assertEqual(self.driver.get_volume_stats(), {})
- self.array.get_array.return_value = SPACE_INFO
+ self.array.get.return_value = SPACE_INFO
result = {"volume_backend_name": VOLUME_BACKEND_NAME,
"vendor_name": "Pure Storage",
"driver_version": self.driver.VERSION,
self.driver.delete_consistencygroup(mock_context, mock_cgroup)
expected_name = pure._get_pgroup_name_from_id(mock_cgroup.id)
- self.array.delete_pgroup.assert_called_with(expected_name)
+ self.array.destroy_pgroup.assert_called_with(expected_name)
self.assertEqual(expected_volumes, volumes)
self.assertEqual(mock_cgroup['status'], model_update['status'])
mock_delete_volume.assert_called_with(self.driver, mock_volume)
- self.array.delete_pgroup.side_effect = exception.PureAPIException(
- code=400, reason="Protection group has been destroyed.")
+ self.array.destroy_pgroup.side_effect = \
+ self.purestorage_module.PureHTTPError(
+ code=400,
+ text="Protection group has been destroyed."
+ )
self.driver.delete_consistencygroup(mock_context, mock_cgroup)
- self.array.delete_pgroup.assert_called_with(expected_name)
+ self.array.destroy_pgroup.assert_called_with(expected_name)
mock_delete_volume.assert_called_with(self.driver, mock_volume)
- self.array.delete_pgroup.side_effect = exception.PureAPIException(
- code=400, reason="Protection group does not exist")
+ self.array.destroy_pgroup.side_effect = \
+ self.purestorage_module.PureHTTPError(
+ code=400,
+ text="Protection group does not exist"
+ )
self.driver.delete_consistencygroup(mock_context, mock_cgroup)
- self.array.delete_pgroup.assert_called_with(expected_name)
+ self.array.destroy_pgroup.assert_called_with(expected_name)
mock_delete_volume.assert_called_with(self.driver, mock_volume)
- self.array.delete_pgroup.side_effect = exception.PureAPIException(
- code=400, reason="Some other error")
- self.assertRaises(exception.PureAPIException,
+ self.array.destroy_pgroup.side_effect = \
+ self.purestorage_module.PureHTTPError(
+ code=400,
+ text="Some other error"
+ )
+ self.assertRaises(self.purestorage_module.PureHTTPError,
self.driver.delete_consistencygroup,
mock_context,
mock_volume)
- self.array.delete_pgroup.side_effect = exception.PureAPIException(
- code=500, reason="Another different error")
- self.assertRaises(exception.PureAPIException,
+ self.array.destroy_pgroup.side_effect = \
+ self.purestorage_module.PureHTTPError(
+ code=500,
+ text="Another different error"
+ )
+ self.assertRaises(self.purestorage_module.PureHTTPError,
self.driver.delete_consistencygroup,
mock_context,
mock_volume)
- self.array.delete_pgroup.side_effect = None
+ self.array.destroy_pgroup.side_effect = None
self.assert_error_propagates(
- [self.array.delete_pgroup],
+ [self.array.destroy_pgroup],
self.driver.delete_consistencygroup, mock_context, mock_cgroup)
def test_create_cgsnapshot(self):
pure._get_pgroup_name_from_id(mock_cgsnap.consistencygroup_id)
expected_snap_suffix = pure._get_pgroup_snap_suffix(mock_cgsnap)
self.array.create_pgroup_snapshot\
- .assert_called_with(expected_pgroup_name, expected_snap_suffix)
+ .assert_called_with(expected_pgroup_name,
+ suffix=expected_snap_suffix)
self.assertEqual({'status': 'available'}, model_update)
self.assertEqual(expected_snaps, snapshots)
self.assertEqual('available', mock_snap.status)
model_update, snapshots = \
self.driver.delete_cgsnapshot(mock_context, mock_cgsnap)
- self.array.delete_pgroup_snapshot.assert_called_with(snap_name)
+ self.array.destroy_pgroup.assert_called_with(snap_name)
self.assertEqual({'status': mock_cgsnap.status}, model_update)
self.assertEqual(expected_snaps, snapshots)
self.assertEqual('deleted', mock_snap.status)
- self.array.delete_pgroup_snapshot.side_effect = \
- exception.PureAPIException(
+ self.array.destroy_pgroup.side_effect = \
+ self.purestorage_module.PureHTTPError(
code=400,
- reason="Protection group snapshot has been destroyed."
+ text="Protection group snapshot has been destroyed."
)
self.driver.delete_cgsnapshot(mock_context, mock_cgsnap)
- self.array.delete_pgroup_snapshot.assert_called_with(snap_name)
+ self.array.destroy_pgroup.assert_called_with(snap_name)
- self.array.delete_pgroup_snapshot.side_effect = \
- exception.PureAPIException(
+ self.array.destroy_pgroup.side_effect = \
+ self.purestorage_module.PureHTTPError(
code=400,
- reason="Protection group snapshot does not exist"
+ text="Protection group snapshot does not exist"
)
self.driver.delete_cgsnapshot(mock_context, mock_cgsnap)
- self.array.delete_pgroup_snapshot.assert_called_with(snap_name)
+ self.array.destroy_pgroup.assert_called_with(snap_name)
- self.array.delete_pgroup_snapshot.side_effect = \
- exception.PureAPIException(
+ self.array.destroy_pgroup.side_effect = \
+ self.purestorage_module.PureHTTPError(
code=400,
- reason="Some other error"
+ text="Some other error"
)
- self.assertRaises(exception.PureAPIException,
+ self.assertRaises(self.purestorage_module.PureHTTPError,
self.driver.delete_cgsnapshot,
mock_context,
mock_cgsnap)
- self.array.delete_pgroup_snapshot.side_effect = \
- exception.PureAPIException(
+ self.array.destroy_pgroup.side_effect = \
+ self.purestorage_module.PureHTTPError(
code=500,
- reason="Another different error"
+ text="Another different error"
)
- self.assertRaises(exception.PureAPIException,
+ self.assertRaises(self.purestorage_module.PureHTTPError,
self.driver.delete_cgsnapshot,
mock_context,
mock_cgsnap)
- self.array.delete_pgroup_snapshot.side_effect = None
+ self.array.destroy_pgroup.side_effect = None
self.assert_error_propagates(
- [self.array.delete_pgroup_snapshot],
+ [self.array.destroy_pgroup],
self.driver.delete_cgsnapshot, mock_context, mock_cgsnap)
-
-
-class FlashArrayBaseTestCase(test.TestCase):
-
- def setUp(self):
- super(FlashArrayBaseTestCase, self).setUp()
- array = FakeFlashArray()
- array._target = TARGET
- array._rest_version = REST_VERSION
- array._root_url = "https://%s/api/%s/" % (TARGET, REST_VERSION)
- array._api_token = API_TOKEN
- self.array = array
-
- def assert_error_propagates(self, mocks, func, *args, **kwargs):
- """Assert that errors from mocks propagate to func.
-
- Fail if exceptions raised by mocks are not seen when calling
- func(*args, **kwargs). Ensure that we are really seeing exceptions
- from the mocks by failing if just running func(*args, **kargs) raises
- an exception itself.
- """
- func(*args, **kwargs)
- for mock_func in mocks:
- mock_func.side_effect = exception.PureAPIException(reason="reason")
- self.assertRaises(exception.PureAPIException,
- func, *args, **kwargs)
- mock_func.side_effect = None
-
-
-class FlashArrayInitTestCase(FlashArrayBaseTestCase):
-
- @mock.patch(ARRAY_OBJ + "._start_session", autospec=True)
- @mock.patch(ARRAY_OBJ + "._choose_rest_version", autospec=True)
- @mock.patch(DRIVER_PATH + ".urllib2.build_opener", autospec=True)
- def test_init(self, mock_build_opener, mock_choose, mock_start):
- opener = mock.Mock()
- mock_build_opener.return_value = opener
- mock_choose.return_value = REST_VERSION
- array = pure.FlashArray(TARGET, API_TOKEN)
- mock_choose.assert_called_with(array)
- mock_start.assert_called_with(array)
- self.assertEqual(array._target, TARGET)
- self.assertEqual(array._api_token, API_TOKEN)
- self.assertEqual(array._rest_version, REST_VERSION)
- self.assertIs(array._opener, opener)
- self.assert_error_propagates([mock_choose, mock_start],
- pure.FlashArray, TARGET, API_TOKEN)
-
-
-class FlashArrayHttpRequestTestCase(FlashArrayBaseTestCase):
-
- def setUp(self):
- super(FlashArrayHttpRequestTestCase, self).setUp()
- self.method = "POST"
- self.path = "path"
- self.path_template = "https://%s/api/%s/%s"
- self.full_path = self.path_template % (TARGET, REST_VERSION,
- self.path)
- self.headers = {"Content-Type": "application/json"}
- self.data = {"list": [1, 2, 3]}
- self.data_json = json.dumps(self.data)
- self.response_json = '[{"hello": "world"}, "!"]'
- self.result = json.loads(self.response_json)
- self.error_msg = "error-msg"
- self.response = mock.Mock(spec=["read", "readline", "info"])
- self.response.read.return_value = self.response_json
- self.response.read.side_effect = None
- self.response.info.return_value = self.headers
- self.response.info.side_effect = None
-
- def make_call(self, method=None, path=None, data=None):
- method = method if method else self.method
- path = path if path else self.full_path
- data = data if data else self.data_json
- return mock.call(FakeRequest(method, path, headers=self.headers), data)
-
- def test_http_request_success(self):
- self.array._opener.open.return_value = self.response
- real_result = self.array._http_request(
- self.method, self.path, self.data)
- self.assertEqual(self.result, real_result)
- self.assertEqual(self.array._opener.open.call_args_list,
- [self.make_call()])
-
- def test_http_request_401_error(self):
- self.array._opener.open.return_value = self.response
- error = urllib2.HTTPError(self.full_path, 401, self.error_msg,
- None, self.response)
- self.array._opener.open.side_effect = iter([error] +
- [self.response] * 2)
- real_result = self.array._http_request(
- self.method, self.path, self.data)
- self.assertEqual(self.result, real_result)
- expected = [self.make_call(),
- self.make_call("POST", self.path_template %
- (TARGET, REST_VERSION, "auth/session"),
- json.dumps({"api_token": API_TOKEN})),
- self.make_call()]
- self.assertEqual(self.array._opener.open.call_args_list, expected)
- self.array._opener.open.reset_mock()
- self.array._opener.open.side_effect = iter([error, error])
- self.assertRaises(exception.PureAPIException,
- self.array._http_request,
- self.method, self.path, self.data)
- self.array._opener.open.reset_mock()
- self.array._opener.open.side_effect = iter([error, self.response,
- error])
- self.assertRaises(exception.PureAPIException,
- self.array._http_request,
- self.method, self.path, self.data)
-
- @mock.patch(ARRAY_OBJ + "._choose_rest_version", autospec=True)
- def test_http_request_450_error(self, mock_choose):
- mock_choose.return_value = "1.1"
- error = urllib2.HTTPError(self.full_path, 450, self.error_msg,
- None, self.response)
- self.array._opener.open.side_effect = iter([error, self.response])
- real_result = self.array._http_request(
- self.method, self.path, self.data)
- self.assertEqual(self.result, real_result)
- expected = [self.make_call(),
- self.make_call(path=self.path_template %
- (TARGET, "1.1", self.path))]
- self.assertEqual(self.array._opener.open.call_args_list, expected)
- mock_choose.assert_called_with(self.array)
- self.array._opener.open.side_effect = error
- self.assertRaises(exception.PureAPIException,
- self.array._http_request,
- self.method, self.path, self.data)
- self.array._opener.open.reset_mock()
- mock_choose.reset_mock()
- self.array._opener.open.side_effect = error
- mock_choose.side_effect = exception.PureAPIException(reason="reason")
- self.assertRaises(exception.PureAPIException,
- self.array._http_request,
- self.method, self.path, self.data)
-
- def test_http_request_http_error(self):
- self.array._opener.open.return_value = self.response
- error = urllib2.HTTPError(self.full_path, 500, self.error_msg,
- None, self.response)
- self.array._opener.open.side_effect = error
- self.assertRaises(exception.PureAPIException,
- self.array._http_request,
- self.method, self.path, self.data)
- self.assertEqual(self.array._opener.open.call_args_list,
- [self.make_call()])
-
- def test_http_request_url_error(self):
- self.array._opener.open.return_value = self.response
- error = urllib2.URLError(self.error_msg)
- self.array._opener.open.side_effect = error
- # try/except used to ensure is instance of type but not subtype
- try:
- self.array._http_request(self.method, self.path, self.data)
- except exception.PureDriverException as err:
- self.assertFalse(isinstance(err, exception.PureAPIException))
- else:
- self.assertTrue(False, "expected failure, but passed")
- self.assertEqual(self.array._opener.open.call_args_list,
- [self.make_call()])
-
- def test_http_request_other_error(self):
- self.array._opener.open.return_value = self.response
- self.assert_error_propagates([self.array._opener.open],
- self.array._http_request,
- self.method, self.path, self.data)
-
- # Test with _http_requests rather than rest calls to ensure
- # root_url change happens properly
- def test_choose_rest_version(self):
- response_string = '{"version": ["0.1", "1.4", "1.3", "1.0"]}'
- self.response.read.return_value = response_string
- self.array._opener.open.return_value = self.response
- result = self.array._choose_rest_version()
- self.assertEqual(result, "1.3")
- self.array._opener.open.assert_called_with(FakeRequest(
- "GET", "https://%s/api/api_version" % TARGET,
- headers=self.headers), "null")
- self.array._opener.open.reset_mock()
- self.response.read.return_value = '{"version": ["0.1", "1.4"]}'
- self.assertRaises(exception.PureDriverException,
- self.array._choose_rest_version)
-
-
-@mock.patch(ARRAY_OBJ + "._http_request", autospec=True)
-class FlashArrayRESTTestCase(FlashArrayBaseTestCase):
-
- def setUp(self):
- super(FlashArrayRESTTestCase, self).setUp()
- self.kwargs = {"kwarg1": "val1", "kwarg2": "val2"}
- self.result = "expected_return"
-
- def test_choose_rest_version(self, mock_req):
- mock_req.return_value = {"version": ["0.1", "1.3", "1.1", "1.0"]}
- self.assert_error_propagates([mock_req],
- self.array._choose_rest_version)
-
- def test_start_session(self, mock_req):
- self.array._start_session()
- data = {"api_token": API_TOKEN}
- mock_req.assert_called_with(self.array, "POST", "auth/session",
- data, reestablish_session=False)
- self.assert_error_propagates([mock_req], self.array._start_session)
-
- def test_get_array(self, mock_req):
- mock_req.return_value = self.result
- result = self.array.get_array(**self.kwargs)
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(self.array, "GET", "array", self.kwargs)
- self.assert_error_propagates([mock_req], self.array.get_array,
- **self.kwargs)
-
- def test_create_volume(self, mock_req):
- mock_req.return_value = self.result
- result = self.array.create_volume("vol-name", "5G")
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(self.array, "POST", "volume/vol-name",
- {"size": "5G"})
- self.assert_error_propagates([mock_req], self.array.create_volume,
- "vol-name", "5G")
-
- def test_copy_volume(self, mock_req):
- mock_req.return_value = self.result
- result = self.array.copy_volume("src-name", "dest-name")
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(self.array, "POST", "volume/dest-name",
- {"source": "src-name"})
- self.assert_error_propagates([mock_req], self.array.copy_volume,
- "dest-name", "src-name")
-
- def test_create_snapshot(self, mock_req):
- mock_req.return_value = [self.result, "second-arg"]
- result = self.array.create_snapshot("vol-name", "suff")
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(
- self.array, "POST", "volume",
- {"source": ["vol-name"], "suffix": "suff", "snap": True})
- self.assert_error_propagates([mock_req], self.array.create_snapshot,
- "vol-name", "suff")
-
- def test_destroy_volume(self, mock_req):
- mock_req.return_value = self.result
- result = self.array.destroy_volume("vol-name")
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(self.array, "DELETE", "volume/vol-name")
- self.assert_error_propagates([mock_req], self.array.destroy_volume,
- "vol-name")
-
- def test_extend_volume(self, mock_req):
- mock_req.return_value = self.result
- result = self.array.extend_volume("vol-name", "5G")
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(self.array, "PUT", "volume/vol-name",
- {"size": "5G", "truncate": False})
- self.assert_error_propagates([mock_req], self.array.extend_volume,
- "vol-name", "5G")
-
- def test_list_hosts(self, mock_req):
- mock_req.return_value = self.result
- result = self.array.list_hosts(**self.kwargs)
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(self.array, "GET", "host", self.kwargs)
- self.assert_error_propagates([mock_req], self.array.list_hosts,
- **self.kwargs)
-
- def test_create_host(self, mock_req):
- mock_req.return_value = self.result
- host_name = "host1"
- params = {'iqnlist': ['iqn1']}
- result = self.array.create_host(host_name, iqnlist=['iqn1'])
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(self.array, "POST", "host/" + host_name,
- params)
- self.assert_error_propagates([mock_req], self.array.create_host,
- host_name, iqnlist=['iqn1'])
-
- def test_delete_host(self, mock_req):
- mock_req.return_value = self.result
- host_name = "host1"
- result = self.array.delete_host(host_name)
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(self.array, "DELETE", "host/" + host_name)
- self.assert_error_propagates([mock_req], self.array.delete_host,
- host_name)
-
- def test_connect_host(self, mock_req):
- mock_req.return_value = self.result
- result = self.array.connect_host("host-name", "vol-name",
- **self.kwargs)
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(self.array, "POST",
- "host/host-name/volume/vol-name",
- self.kwargs)
- self.assert_error_propagates([mock_req], self.array.connect_host,
- "host-name", "vol-name", **self.kwargs)
-
- def test_disconnect_host(self, mock_req):
- mock_req.return_value = self.result
- result = self.array.disconnect_host("host-name", "vol-name")
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(self.array, "DELETE",
- "host/host-name/volume/vol-name")
- self.assert_error_propagates([mock_req], self.array.disconnect_host,
- "host-name", "vol-name")
-
- def test_list_ports(self, mock_req):
- mock_req.return_value = self.result
- result = self.array.list_ports(**self.kwargs)
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(self.array, "GET", "port", self.kwargs)
- self.assert_error_propagates([mock_req], self.array.list_ports,
- **self.kwargs)
-
- def test_list_volume_hosts(self, mock_req):
- mock_req.return_value = self.result
- result = self.array.list_volume_hosts("vol-name")
- self.assertEqual(result, self.result)
- mock_req.assert_called_with(self.array, "GET", "volume/vol-name/host")
- self.assert_error_propagates([mock_req], self.array.list_volume_hosts,
- "vol-name")
-
- def test_create_pgroup(self, mock_req):
- mock_req.return_value = self.result
- pgroup_name = "cgroup_id"
- result = self.array.create_pgroup(pgroup_name)
- self.assertEqual(self.result, result)
- req_url = "pgroup/" + pgroup_name
- mock_req.assert_called_with(self.array, "POST", req_url)
- self.assert_error_propagates([mock_req], self.array.create_pgroup,
- pgroup_name)
-
- def test_delete_pgroup(self, mock_req):
- mock_req.return_value = self.result
- pgroup_name = "cgroup_id"
- result = self.array.delete_pgroup(pgroup_name)
- self.assertEqual(self.result, result)
- req_url = "pgroup/" + pgroup_name
- mock_req.assert_called_with(self.array, "DELETE", req_url)
- self.assert_error_propagates([mock_req], self.array.delete_pgroup,
- pgroup_name)
-
- def test_create_pgroup_snapshot(self, mock_req):
- mock_req.return_value = self.result
- pgroup_name = "cgroup_id"
- snap_suffix = "snap_suffix"
- result = self.array.create_pgroup_snapshot(pgroup_name, snap_suffix)
- self.assertEqual(self.result, result)
- expected_params = {
- "snap": True,
- "suffix": snap_suffix,
- "source": [pgroup_name]
- }
- mock_req.assert_called_with(self.array, "POST", "pgroup",
- expected_params)
- self.assert_error_propagates([mock_req],
- self.array.create_pgroup_snapshot,
- pgroup_name, snap_suffix)
-
- def test_delete_pgroup_snapshot(self, mock_req):
- mock_req.return_value = self.result
- snapshot_name = "snap1"
- result = self.array.delete_pgroup_snapshot(snapshot_name)
- self.assertEqual(self.result, result)
- req_url = "pgroup/" + snapshot_name
- mock_req.assert_called_with(self.array, "DELETE", req_url)
- self.assert_error_propagates([mock_req],
- self.array.delete_pgroup_snapshot,
- snapshot_name)
-
- def test_add_volume_to_pgroup(self, mock_req):
- mock_req.return_value = self.result
- pgroup_name = "cgroup_id"
- volume_name = "myvol-1"
- expected_params = {"addvollist": [volume_name]}
- result = self.array.add_volume_to_pgroup(pgroup_name, volume_name)
- self.assertEqual(self.result, result)
- req_url = "pgroup/" + pgroup_name
- mock_req.assert_called_with(self.array, "PUT", req_url,
- expected_params)
- self.assert_error_propagates([mock_req],
- self.array.add_volume_to_pgroup,
- pgroup_name, volume_name)
-
-
-class FakeFlashArray(pure.FlashArray):
-
- def __init__(self):
- self._opener = mock.Mock()
-
-
-class FakeRequest(urllib2.Request):
-
- def __init__(self, method, *args, **kwargs):
- urllib2.Request.__init__(self, *args, **kwargs)
- self.get_method = lambda: method
-
- def __eq__(self, other):
- if not isinstance(other, urllib2.Request):
- return False
- return (self.get_method() == other.get_method() and
- self.get_full_url() == other.get_full_url() and
- self.header_items() == other.header_items())
-
- def __ne__(self, other):
- return not (self == other)
This driver requires Purity version 3.4.0 or later.
"""
-import cookielib
-import json
import re
-import urllib2
import uuid
from oslo.config import cfg
from cinder import utils
from cinder.volume.drivers.san import san
+try:
+ import purestorage
+except ImportError:
+ purestorage = None
+
LOG = logging.getLogger(__name__)
PURE_OPTS = [
class PureISCSIDriver(san.SanISCSIDriver):
"""Performs volume management on Pure Storage FlashArray."""
- VERSION = "2.0.1"
+ VERSION = "2.0.3"
+
+ SUPPORTED_REST_API_VERSIONS = ['1.2', '1.3', '1.4']
def __init__(self, *args, **kwargs):
execute = kwargs.pop("execute", utils.execute)
def do_setup(self, context):
"""Performs driver initialization steps that could raise exceptions."""
- # Raises PureDriverException if unable to connect and PureAPIException
+ if purestorage is None:
+ msg = _("Missing 'purestorage' python module, ensure the library"
+ " is installed and available.")
+ raise exception.PureDriverException(msg)
+
+ # Raises PureDriverException if unable to connect and PureHTTPError
# if unable to authenticate.
- self._array = FlashArray(
+ purestorage.FlashArray.supported_rest_versions = \
+ self.SUPPORTED_REST_API_VERSIONS
+ self._array = purestorage.FlashArray(
self.configuration.san_ip,
- self.configuration.pure_api_token)
+ api_token=self.configuration.pure_api_token)
self._iscsi_port = self._choose_target_iscsi_port()
def check_for_setup_error(self):
LOG.debug("Enter PureISCSIDriver.delete_volume.")
vol_name = _get_vol_name(volume)
try:
- connected_hosts = self._array.list_volume_hosts(vol_name)
+ connected_hosts = \
+ self._array.list_volume_private_connections(vol_name)
for host_info in connected_hosts:
host_name = host_info["host"]
self._disconnect_host(host_name, vol_name)
self._array.destroy_volume(vol_name)
- except exception.PureAPIException as err:
+ except purestorage.PureHTTPError as err:
with excutils.save_and_reraise_exception() as ctxt:
- if err.kwargs["code"] == 400 and \
- ERR_MSG_NOT_EXIST in err.msg:
+ if err.code == 400 and \
+ ERR_MSG_NOT_EXIST in err.text:
# Happens if the volume does not exist.
ctxt.reraise = False
LOG.warn(_LW("Volume deletion failed with message: %s"),
- err.msg)
+ err.text)
LOG.debug("Leave PureISCSIDriver.delete_volume.")
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
LOG.debug("Enter PureISCSIDriver.create_snapshot.")
vol_name, snap_suff = _get_snap_name(snapshot).split(".")
- self._array.create_snapshot(vol_name, snap_suff)
+ self._array.create_snapshot(vol_name, suffix=snap_suff)
LOG.debug("Leave PureISCSIDriver.create_snapshot.")
def delete_snapshot(self, snapshot):
snap_name = _get_snap_name(snapshot)
try:
self._array.destroy_volume(snap_name)
- except exception.PureAPIException as err:
+ except purestorage.PureHTTPError as err:
with excutils.save_and_reraise_exception() as ctxt:
- if err.kwargs["code"] == 400:
+ if err.code == 400:
# Happens if the snapshot does not exist.
ctxt.reraise = False
LOG.error(_LE("Snapshot deletion failed with message:"
- " %s"), err.msg)
+ " %s"), err.text)
LOG.debug("Leave PureISCSIDriver.delete_snapshot.")
def initialize_connection(self, volume, connector):
try:
connection = self._array.connect_host(host_name, vol_name)
- except exception.PureAPIException as err:
+ except purestorage.PureHTTPError as err:
with excutils.save_and_reraise_exception() as ctxt:
- if (err.kwargs["code"] == 400 and
- "Connection already exists" in err.msg):
+ if (err.code == 400 and
+ "Connection already exists" in err.text):
# Happens if the volume is already connected to the host.
ctxt.reraise = False
LOG.warn(_LW("Volume connection already exists with "
- "message: %s") % err.msg)
+ "message: %s"), err.text)
# Get the info for the existing connection
- connected_hosts = self._array.list_volume_hosts(vol_name)
+ connected_hosts = \
+ self._array.list_volume_private_connections(vol_name)
for host_info in connected_hosts:
if host_info["host"] == host_name:
connection = host_info
LOG.debug("Enter PureISCSIDriver._disconnect_host.")
try:
self._array.disconnect_host(host_name, vol_name)
- except exception.PureAPIException as err:
+ except purestorage.PureHTTPError as err:
with excutils.save_and_reraise_exception() as ctxt:
- if err.kwargs["code"] == 400:
+ if err.code == 400:
# Happens if the host and volume are not connected.
ctxt.reraise = False
LOG.error(_LE("Disconnection failed with message: "
- "%(msg)s."), {"msg": err.msg})
+ "%(msg)s."), {"msg": err.text})
if (GENERATED_NAME.match(host_name) and
not self._array.list_host_connections(host_name,
private=True)):
def _update_stats(self):
"""Set self._stats with relevant information."""
- info = self._array.get_array(space=True)
+ info = self._array.get(space=True)
total = float(info["capacity"]) / units.Gi
free = float(info["capacity"] - info["total"]) / units.Gi
data = {"volume_backend_name": self._backend_name,
def _add_volume_to_consistency_group(self, consistencygroup_id, vol_name):
pgroup_name = _get_pgroup_name_from_id(consistencygroup_id)
- self._array.add_volume_to_pgroup(pgroup_name, vol_name)
+ self._array.set_pgroup(pgroup_name, addvollist=[vol_name])
def create_consistencygroup(self, context, group):
"""Creates a consistencygroup."""
LOG.debug("Enter PureISCSIDriver.delete_consistencygroup")
try:
- self._array.delete_pgroup(_get_pgroup_name_from_id(group.id))
- except exception.PureAPIException as err:
+ self._array.destroy_pgroup(_get_pgroup_name_from_id(group.id))
+ except purestorage.PureHTTPError as err:
with excutils.save_and_reraise_exception() as ctxt:
- if (err.kwargs["code"] == 400 and
- (ERR_MSG_PENDING_ERADICATION in err.msg or
- ERR_MSG_NOT_EXIST in err.msg)):
+ if (err.code == 400 and
+ (ERR_MSG_PENDING_ERADICATION in err.text or
+ ERR_MSG_NOT_EXIST in err.text)):
# Treat these as a "success" case since we are trying
# to delete them anyway.
ctxt.reraise = False
LOG.warning(_LW("Unable to delete Protection Group: %s"),
- err.msg)
+ err.text)
volumes = self.db.volume_get_all_by_group(context, group.id)
pgroup_name = _get_pgroup_name_from_id(cgsnapshot.consistencygroup_id)
pgsnap_suffix = _get_pgroup_snap_suffix(cgsnapshot)
- self._array.create_pgroup_snapshot(pgroup_name, pgsnap_suffix)
+ self._array.create_pgroup_snapshot(pgroup_name, suffix=pgsnap_suffix)
snapshots = self.db.snapshot_get_all_for_cgsnapshot(
context, cgsnapshot.id)
pgsnap_name = _get_pgroup_snap_name(cgsnapshot)
try:
- self._array.delete_pgroup_snapshot(pgsnap_name)
- except exception.PureAPIException as err:
+ # FlashArray.destroy_pgroup is also used for deleting
+ # pgroup snapshots. The underlying REST API is identical.
+ self._array.destroy_pgroup(pgsnap_name)
+ except purestorage.PureHTTPError as err:
with excutils.save_and_reraise_exception() as ctxt:
- if (err.kwargs["code"] == 400 and
- (ERR_MSG_PENDING_ERADICATION in err.msg or
- ERR_MSG_NOT_EXIST in err.msg)):
+ if (err.code == 400 and
+ (ERR_MSG_PENDING_ERADICATION in err.text or
+ ERR_MSG_NOT_EXIST in err.text)):
# Treat these as a "success" case since we are trying
# to delete them anyway.
ctxt.reraise = False
LOG.warning(_LW("Unable to delete Protection Group "
- "Snapshot: %s"), err.msg)
+ "Snapshot: %s"), err.text)
snapshots = self.db.snapshot_get_all_for_cgsnapshot(
context, cgsnapshot.id)
LOG.debug("Leave PureISCSIDriver.delete_cgsnapshot")
return model_update, snapshots
-
-
-class FlashArray(object):
- """Wrapper for Pure Storage REST API."""
- SUPPORTED_REST_API_VERSIONS = ["1.3", "1.2"]
-
- def __init__(self, target, api_token):
- cookie_handler = urllib2.HTTPCookieProcessor(cookielib.CookieJar())
- self._opener = urllib2.build_opener(cookie_handler)
- self._target = target
- self._rest_version = self._choose_rest_version()
- self._root_url = "https://%s/api/%s/" % (target, self._rest_version)
- self._api_token = api_token
- self._start_session()
-
- def _http_request(self, method, path, data=None, reestablish_session=True):
- """Perform HTTP request for REST API."""
- req = urllib2.Request(self._root_url + path,
- headers={"Content-Type": "application/json"})
- req.get_method = lambda: method
- body = json.dumps(data)
- try:
- # Raises urllib2.HTTPError if response code != 200
- response = self._opener.open(req, body)
- except urllib2.HTTPError as err:
- if (reestablish_session and err.code == 401):
- self._start_session()
- return self._http_request(method, path, data,
- reestablish_session=False)
- elif err.code == 450:
- # Purity REST API version is bad
- new_version = self._choose_rest_version()
- if new_version == self._rest_version:
- raise exception.PureAPIException(
- code=err.code,
- reason=(_("Unable to find usable REST API version. "
- "Response from Pure Storage REST API: %s") %
- err.read()))
- self._rest_version = new_version
- self._root_url = "https://%s/api/%s/" % (self._target,
- self._rest_version)
- return self._http_request(method, path, data)
- else:
- raise exception.PureAPIException(
- code=err.code,
- reason=_("exception:%s") % err.read())
- except urllib2.URLError as err:
- # Error outside scope of HTTP status codes,
- # e.g., unable to resolve domain name
- raise exception.PureDriverException(
- reason=_("Unable to connect to %r. Check san_ip.") %
- self._target)
- else:
- content = response.read()
- if "application/json" in response.info().get('Content-Type'):
- return json.loads(content)
- raise exception.PureAPIException(
- reason=(_("Response not in JSON: %s") % content))
-
- def _choose_rest_version(self):
- """Return a REST API version."""
- self._root_url = "https://%s/api/" % self._target
- data = self._http_request("GET", "api_version")
- available_versions = data["version"]
- available_versions.sort(reverse=True)
- for version in available_versions:
- if version in FlashArray.SUPPORTED_REST_API_VERSIONS:
- return version
- raise exception.PureDriverException(
- reason=_("All REST API versions supported by this version of "
- "the Pure Storage iSCSI driver are unavailable on "
- "array."))
-
- def _start_session(self):
- """Start a REST API session."""
- self._http_request("POST", "auth/session",
- {"api_token": self._api_token},
- reestablish_session=False)
-
- def get_array(self, **kwargs):
- """Return a dictionary containing information about the array."""
- return self._http_request("GET", "array", kwargs)
-
- def create_volume(self, name, size):
- """Create a volume and return a dictionary describing it."""
- return self._http_request("POST", "volume/%s" % name,
- {"size": size})
-
- def copy_volume(self, source, dest):
- """Clone a volume and return a dictionary describing the new volume."""
- return self._http_request("POST", "volume/%s" % dest,
- {"source": source})
-
- def create_snapshot(self, volume, suffix):
- """Create a snapshot and return a dictionary describing it."""
- data = {"source": [volume], "suffix": suffix, "snap": True}
- return self._http_request("POST", "volume", data)[0]
-
- def destroy_volume(self, volume):
- """Destroy an existing volume or snapshot."""
- return self._http_request("DELETE", "volume/%s" % volume)
-
- def extend_volume(self, volume, size):
- """Extend a volume to a new, larger size."""
- return self._http_request("PUT", "volume/%s" % volume,
- {"size": size, "truncate": False})
-
- def list_hosts(self, **kwargs):
- """Return a list of dictionaries describing each host."""
- return self._http_request("GET", "host", kwargs)
-
- def list_host_connections(self, host, **kwargs):
- """Return a list of dictionaries describing connected volumes."""
- return self._http_request("GET", "host/%s/volume" % host, kwargs)
-
- def create_host(self, host, **kwargs):
- """Create a host."""
- return self._http_request("POST", "host/%s" % host, kwargs)
-
- def delete_host(self, host):
- """Delete a host."""
- return self._http_request("DELETE", "host/%s" % host)
-
- def connect_host(self, host, volume, **kwargs):
- """Create a connection between a host and a volume."""
- return self._http_request("POST",
- "host/%s/volume/%s" % (host, volume),
- kwargs)
-
- def disconnect_host(self, host, volume):
- """Delete a connection between a host and a volume."""
- return self._http_request("DELETE",
- "host/%s/volume/%s" % (host, volume))
-
- def set_host(self, host, **kwargs):
- """Set an attribute of a host."""
- return self._http_request("PUT", "host/%s" % host, kwargs)
-
- def list_ports(self, **kwargs):
- """Return a list of dictionaries describing ports."""
- return self._http_request("GET", "port", kwargs)
-
- def list_volume_hosts(self, volume):
- """Return a list of dictionaries describing connected hosts."""
- return self._http_request("GET", "volume/%s/host" % volume)
-
- def create_pgroup(self, name):
- return self._http_request("POST", "pgroup/%s" % name)
-
- def delete_pgroup(self, name):
- return self._http_request("DELETE", "pgroup/%s" % name)
-
- def create_pgroup_snapshot(self, pgroup_name, pgsnapshot_suffix):
- params = {
- "snap": True,
- "suffix": pgsnapshot_suffix,
- "source": [pgroup_name]
- }
- return self._http_request("POST", "pgroup", params)
-
- def delete_pgroup_snapshot(self, name):
- return self._http_request("DELETE", "pgroup/%s" % name)
-
- def add_volume_to_pgroup(self, pgroup_name, volume_name):
- return self._http_request("PUT", "pgroup/%s" % pgroup_name,
- {"addvollist": [volume_name]})