]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Switch the PureISCSIDriver over to using the purestorage pypi module.
authorPatrick East <patrick.east@purestorage.com>
Wed, 14 Jan 2015 23:07:16 +0000 (15:07 -0800)
committerPatrick East <patrick.east@purestorage.com>
Thu, 15 Jan 2015 22:20:22 +0000 (14:20 -0800)
All management api's will now go through the purestorage module, the
interface is extremely similar to the original FlashArray object
included in the cinder.volume.drivers.pure module. This allows for
updates to the REST API and python code that uses it to not disrupt the
cinder driver.

Implements: blueprint pure-iscsi-purestorage-lib
Change-Id: Ifcfb1c2337e5837f2121929c4788137d8bd1cf42

cinder/exception.py
cinder/tests/test_pure.py
cinder/volume/drivers/pure.py

index 67a657facaf479f6c53a1ce3d7527c3656e2dfa8..6015765c10a41fad605a14c562bba8b40807b866 100644 (file)
@@ -689,10 +689,6 @@ class PureDriverException(VolumeDriverException):
     message = _("Pure Storage Cinder driver failure: %(reason)s")
 
 
-class PureAPIException(VolumeBackendAPIException):
-    message = _("Bad response from Pure Storage REST API: %(reason)s")
-
-
 # Zadara
 class ZadaraException(VolumeDriverException):
     message = _('Zadara Cinder Driver exception.')
index 46d26dd31cc4311eb4c59627f9713b2015f309be..2a80dd77d03cfd08e957705c6ee1c5d679bd8ac2 100644 (file)
 #    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"
@@ -102,6 +103,16 @@ SPACE_INFO = {"capacity": TOTAL_SPACE * units.Gi,
               }
 
 
+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):
@@ -111,22 +122,36 @@ class PureISCSIDriverTestCase(test.TestCase):
         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.
@@ -262,30 +287,34 @@ class PureISCSIDriverTestCase(test.TestCase):
                                 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],
@@ -295,7 +324,7 @@ class PureISCSIDriverTestCase(test.TestCase):
         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,
@@ -308,7 +337,7 @@ class PureISCSIDriverTestCase(test.TestCase):
         }]
 
         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)]
@@ -317,8 +346,10 @@ class PureISCSIDriverTestCase(test.TestCase):
     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)
 
@@ -327,8 +358,8 @@ class PureISCSIDriverTestCase(test.TestCase):
         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],
@@ -420,37 +451,46 @@ class PureISCSIDriverTestCase(test.TestCase):
     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()
@@ -503,8 +543,8 @@ class PureISCSIDriverTestCase(test.TestCase):
         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,
@@ -512,9 +552,12 @@ class PureISCSIDriverTestCase(test.TestCase):
         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)
@@ -522,7 +565,7 @@ class PureISCSIDriverTestCase(test.TestCase):
 
     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,
@@ -619,40 +662,52 @@ class PureISCSIDriverTestCase(test.TestCase):
             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):
@@ -674,7 +729,8 @@ class PureISCSIDriverTestCase(test.TestCase):
             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)
@@ -700,455 +756,49 @@ class PureISCSIDriverTestCase(test.TestCase):
         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)
index 37710c4deca97f6e6fd69e09f1d95afdf08f5b71..6ba3e789c51970900430b45ae3de9ac59d69daaa 100644 (file)
@@ -18,10 +18,7 @@ Volume driver for Pure Storage FlashArray storage system.
 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
@@ -35,6 +32,11 @@ from cinder.openstack.common import log as logging
 from cinder import utils
 from cinder.volume.drivers.san import san
 
+try:
+    import purestorage
+except ImportError:
+    purestorage = None
+
 LOG = logging.getLogger(__name__)
 
 PURE_OPTS = [
@@ -96,7 +98,9 @@ def _generate_purity_host_name(name):
 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)
@@ -109,11 +113,18 @@ class PureISCSIDriver(san.SanISCSIDriver):
 
     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):
@@ -181,26 +192,27 @@ class PureISCSIDriver(san.SanISCSIDriver):
         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):
@@ -209,13 +221,13 @@ class PureISCSIDriver(san.SanISCSIDriver):
         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):
@@ -292,16 +304,17 @@ class PureISCSIDriver(san.SanISCSIDriver):
 
         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
@@ -336,13 +349,13 @@ class PureISCSIDriver(san.SanISCSIDriver):
         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)):
@@ -366,7 +379,7 @@ class PureISCSIDriver(san.SanISCSIDriver):
 
     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,
@@ -390,7 +403,7 @@ class PureISCSIDriver(san.SanISCSIDriver):
 
     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."""
@@ -408,17 +421,17 @@ class PureISCSIDriver(san.SanISCSIDriver):
         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)
 
@@ -437,7 +450,7 @@ class PureISCSIDriver(san.SanISCSIDriver):
 
         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)
@@ -457,17 +470,19 @@ class PureISCSIDriver(san.SanISCSIDriver):
         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)
@@ -479,169 +494,3 @@ class PureISCSIDriver(san.SanISCSIDriver):
 
         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]})