]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Refactor PureISCSIDriver into base and iSCSI classes.
authorPatrick East <patrick.east@purestorage.com>
Tue, 14 Apr 2015 20:45:22 +0000 (13:45 -0700)
committerPatrick East <patrick.east@purestorage.com>
Tue, 2 Jun 2015 15:08:24 +0000 (08:08 -0700)
This adds in a new class called PureBaseVolumeDriver that contains
all of the generic shared methods that utilize the management REST API.

The PureISCSIDriver now inherits from the base driver and implements
the functionality required for iSCSI hosts/initiators.

As part of this we’ve cleaned up some of the logging where it would
previously have hard coded the drivers class name in it, and moved some
static non-class helper methods into the base class.

These changes will ease future work to add additional drivers or split
them into separate files.

Implements: blueprint pure-base-driver
Change-Id: Iaf3979e5cccc150d09ca00daa3d9c3471f9795a0

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

index 1208e887017dfc783f3968915b69613112185dc5..72c7fc122c19a544c90a80207e3a96b57f36eea8 100644 (file)
@@ -37,7 +37,8 @@ from cinder.volume.drivers import pure
 patch_retry.stop()
 
 DRIVER_PATH = "cinder.volume.drivers.pure"
-DRIVER_OBJ = DRIVER_PATH + ".PureISCSIDriver"
+BASE_DRIVER_OBJ = DRIVER_PATH + ".PureBaseVolumeDriver"
+ISCSI_DRIVER_OBJ = DRIVER_PATH + ".PureISCSIDriver"
 ARRAY_OBJ = DRIVER_PATH + ".FlashArray"
 
 TARGET = "pure-target"
@@ -46,45 +47,49 @@ VOLUME_BACKEND_NAME = "Pure_iSCSI"
 PORT_NAMES = ["ct0.eth2", "ct0.eth3", "ct1.eth2", "ct1.eth3"]
 ISCSI_IPS = ["10.0.0." + str(i + 1) for i in range(len(PORT_NAMES))]
 HOSTNAME = "computenode1"
-PURE_HOST_NAME = pure._generate_purity_host_name(HOSTNAME)
-PURE_HOST = {"name": PURE_HOST_NAME,
-             "hgroup": None,
-             "iqn": [],
-             "wwn": [],
-             }
+PURE_HOST_NAME = pure.PureBaseVolumeDriver._generate_purity_host_name(HOSTNAME)
+PURE_HOST = {
+    "name": PURE_HOST_NAME,
+    "hgroup": None,
+    "iqn": [],
+    "wwn": [],
+}
 REST_VERSION = "1.2"
 VOLUME_ID = "abcdabcd-1234-abcd-1234-abcdeffedcba"
-VOLUME = {"name": "volume-" + VOLUME_ID,
-          "id": VOLUME_ID,
-          "display_name": "fake_volume",
-          "size": 2,
-          "host": "irrelevant",
-          "volume_type": None,
-          "volume_type_id": None,
-          "consistencygroup_id": None
-          }
+VOLUME = {
+    "name": "volume-" + VOLUME_ID,
+    "id": VOLUME_ID,
+    "display_name": "fake_volume",
+    "size": 2,
+    "host": "irrelevant",
+    "volume_type": None,
+    "volume_type_id": None,
+    "consistencygroup_id": None,
+}
 VOLUME_WITH_CGROUP = VOLUME.copy()
 VOLUME_WITH_CGROUP['consistencygroup_id'] = \
     "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
 SRC_VOL_ID = "dc7a294d-5964-4379-a15f-ce5554734efc"
-SRC_VOL = {"name": "volume-" + SRC_VOL_ID,
-           "id": SRC_VOL_ID,
-           "display_name": 'fake_src',
-           "size": 2,
-           "host": "irrelevant",
-           "volume_type": None,
-           "volume_type_id": None,
-           "consistencygroup_id": None
-           }
+SRC_VOL = {
+    "name": "volume-" + SRC_VOL_ID,
+    "id": SRC_VOL_ID,
+    "display_name": 'fake_src',
+    "size": 2,
+    "host": "irrelevant",
+    "volume_type": None,
+    "volume_type_id": None,
+    "consistencygroup_id": None,
+}
 SNAPSHOT_ID = "04fe2f9a-d0c4-4564-a30d-693cc3657b47"
-SNAPSHOT = {"name": "snapshot-" + SNAPSHOT_ID,
-            "id": SNAPSHOT_ID,
-            "volume_id": SRC_VOL_ID,
-            "volume_name": "volume-" + SRC_VOL_ID,
-            "volume_size": 2,
-            "display_name": "fake_snapshot",
-            "cgsnapshot_id": None
-            }
+SNAPSHOT = {
+    "name": "snapshot-" + SNAPSHOT_ID,
+    "id": SNAPSHOT_ID,
+    "volume_id": SRC_VOL_ID,
+    "volume_name": "volume-" + SRC_VOL_ID,
+    "volume_size": 2,
+    "display_name": "fake_snapshot",
+    "cgsnapshot_id": None,
+}
 SNAPSHOT_WITH_CGROUP = SNAPSHOT.copy()
 SNAPSHOT_WITH_CGROUP['cgsnapshot_id'] = \
     "4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
@@ -97,35 +102,41 @@ ISCSI_PORTS = [{"name": name,
                 "portal": ip + ":" + TARGET_PORT,
                 "wwn": None,
                 } for name, ip in zip(PORT_NAMES, ISCSI_IPS)]
-NON_ISCSI_PORT = {"name": "ct0.fc1",
-                  "iqn": None,
-                  "portal": None,
-                  "wwn": "5001500150015081",
-                  }
+NON_ISCSI_PORT = {
+    "name": "ct0.fc1",
+    "iqn": None,
+    "portal": None,
+    "wwn": "5001500150015081",
+}
 PORTS_WITH = ISCSI_PORTS + [NON_ISCSI_PORT]
 PORTS_WITHOUT = [NON_ISCSI_PORT]
-VOLUME_CONNECTIONS = [{"host": "h1", "name": VOLUME["name"] + "-cinder"},
-                      {"host": "h2", "name": VOLUME["name"] + "-cinder"},
-                      ]
+VOLUME_CONNECTIONS = [
+    {"host": "h1", "name": VOLUME["name"] + "-cinder"},
+    {"host": "h2", "name": VOLUME["name"] + "-cinder"},
+]
 TOTAL_CAPACITY = 50.0
 USED_SPACE = 32.1
 PROVISIONED_CAPACITY = 70.0
 DEFAULT_OVER_SUBSCRIPTION = 20
-SPACE_INFO = {"capacity": TOTAL_CAPACITY * units.Gi,
-              "total": USED_SPACE * units.Gi
-              }
-SPACE_INFO_EMPTY = {"capacity": TOTAL_CAPACITY * units.Gi,
-                    "total": 0
-                    }
-
-CONNECTION_INFO = {"driver_volume_type": "iscsi",
-                   "data": {"target_iqn": TARGET_IQN,
-                            "target_portal": ISCSI_IPS[0] + ":" + TARGET_PORT,
-                            "target_lun": 1,
-                            "target_discovered": True,
-                            "access_mode": "rw",
-                            },
-                   }
+SPACE_INFO = {
+    "capacity": TOTAL_CAPACITY * units.Gi,
+    "total": USED_SPACE * units.Gi,
+}
+SPACE_INFO_EMPTY = {
+    "capacity": TOTAL_CAPACITY * units.Gi,
+    "total": 0,
+}
+
+CONNECTION_INFO = {
+    "driver_volume_type": "iscsi",
+    "data": {
+        "target_iqn": TARGET_IQN,
+        "target_portal": ISCSI_IPS[0] + ":" + TARGET_PORT,
+        "target_lun": 1,
+        "target_discovered": True,
+        "access_mode": "rw",
+    },
+}
 
 
 class FakePureStorageHTTPError(Exception):
@@ -138,47 +149,17 @@ class FakePureStorageHTTPError(Exception):
         self.text = text
 
 
-class PureISCSIDriverTestCase(test.TestCase):
-
+class PureDriverTestCase(test.TestCase):
     def setUp(self):
-        super(PureISCSIDriverTestCase, self).setUp()
+        super(PureDriverTestCase, self).setUp()
         self.mock_config = mock.Mock()
         self.mock_config.san_ip = TARGET
         self.mock_config.pure_api_token = API_TOKEN
         self.mock_config.volume_backend_name = VOLUME_BACKEND_NAME
-        self.mock_config.use_chap_auth = False
-        self.driver = pure.PureISCSIDriver(configuration=self.mock_config)
         self.array = mock.Mock()
-        self.driver._array = self.array
         self.purestorage_module = pure.purestorage
         self.purestorage_module.PureHTTPError = FakePureStorageHTTPError
 
-    @mock.patch(DRIVER_OBJ + "._choose_target_iscsi_port")
-    def test_do_setup(self, mock_choose_target_iscsi_port):
-        mock_choose_target_iscsi_port.return_value = ISCSI_PORTS[0]
-        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)
-        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(
-            [
-                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.
 
@@ -195,17 +176,25 @@ class PureISCSIDriverTestCase(test.TestCase):
                               func, *args, **kwargs)
             mock_func.side_effect = None
 
+
+class PureBaseVolumeDriverTestCase(PureDriverTestCase):
+
+    def setUp(self):
+        super(PureBaseVolumeDriverTestCase, self).setUp()
+        self.driver = pure.PureBaseVolumeDriver(configuration=self.mock_config)
+        self.driver._array = self.array
+
     def test_generate_purity_host_name(self):
-        generate = pure._generate_purity_host_name
-        result = generate("really-long-string-thats-a-bit-too-long")
+        result = self.driver._generate_purity_host_name(
+            "really-long-string-thats-a-bit-too-long")
         self.assertTrue(result.startswith("really-long-string-that-"))
         self.assertTrue(result.endswith("-cinder"))
-        self.assertEqual(len(result), 63)
+        self.assertEqual(63, len(result))
         self.assertTrue(pure.GENERATED_NAME.match(result))
-        result = generate("!@#$%^-invalid&*")
+        result = self.driver._generate_purity_host_name("!@#$%^-invalid&*")
         self.assertTrue(result.startswith("invalid---"))
         self.assertTrue(result.endswith("-cinder"))
-        self.assertEqual(len(result), 49)
+        self.assertEqual(49, len(result))
         self.assertTrue(pure.GENERATED_NAME.match(result))
 
     def test_create_volume(self):
@@ -215,7 +204,7 @@ class PureISCSIDriverTestCase(test.TestCase):
         self.assert_error_propagates([self.array.create_volume],
                                      self.driver.create_volume, VOLUME)
 
-    @mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group",
+    @mock.patch(BASE_DRIVER_OBJ + "._add_volume_to_consistency_group",
                 autospec=True)
     def test_create_volume_with_cgroup(self, mock_add_to_cgroup):
         vol_name = VOLUME_WITH_CGROUP["name"] + "-cinder"
@@ -251,10 +240,11 @@ class PureISCSIDriverTestCase(test.TestCase):
             self.driver.create_volume_from_snapshot, VOLUME, SNAPSHOT)
         SNAPSHOT["volume_size"] = 2  # reset size
 
-    @mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group",
+    @mock.patch(BASE_DRIVER_OBJ + "._add_volume_to_consistency_group",
                 autospec=True)
-    @mock.patch(DRIVER_OBJ + "._extend_if_needed", autospec=True)
-    @mock.patch(DRIVER_PATH + "._get_pgroup_vol_snap_name", autospec=True)
+    @mock.patch(BASE_DRIVER_OBJ + "._extend_if_needed", autospec=True)
+    @mock.patch(BASE_DRIVER_OBJ + "._get_pgroup_vol_snap_name",
+                spec=pure.PureBaseVolumeDriver._get_pgroup_vol_snap_name)
     def test_create_volume_from_cgsnapshot(self, mock_get_snap_name,
                                            mock_extend_if_needed,
                                            mock_add_to_cgroup):
@@ -300,7 +290,7 @@ class PureISCSIDriverTestCase(test.TestCase):
             self.driver.create_cloned_volume, VOLUME, SRC_VOL)
         SRC_VOL["size"] = 2  # reset size
 
-    @mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group",
+    @mock.patch(BASE_DRIVER_OBJ + "._add_volume_to_consistency_group",
                 autospec=True)
     def test_create_cloned_volume_with_cgroup(self, mock_add_to_cgroup):
         vol_name = VOLUME_WITH_CGROUP["name"] + "-cinder"
@@ -354,12 +344,12 @@ class PureISCSIDriverTestCase(test.TestCase):
             "host": host_name_a,
             "lun": 7,
             "name": vol_name,
-            "size": 3221225472
+            "size": 3221225472,
         }, {
             "host": host_name_b,
             "lun": 2,
             "name": vol_name,
-            "size": 3221225472
+            "size": 3221225472,
         }]
 
         self.driver.delete_volume(VOLUME)
@@ -391,214 +381,7 @@ class PureISCSIDriverTestCase(test.TestCase):
         self.assert_error_propagates([self.array.destroy_volume],
                                      self.driver.delete_snapshot, SNAPSHOT)
 
-    @mock.patch(DRIVER_OBJ + "._connect")
-    @mock.patch(DRIVER_OBJ + "._get_target_iscsi_port")
-    def test_initialize_connection(self, mock_get_iscsi_port, mock_connection):
-        mock_get_iscsi_port.return_value = ISCSI_PORTS[0]
-        mock_connection.return_value = {"vol": VOLUME["name"] + "-cinder",
-                                        "lun": 1,
-                                        }
-        result = CONNECTION_INFO
-        real_result = self.driver.initialize_connection(VOLUME, CONNECTOR)
-        self.assertDictMatch(result, real_result)
-        mock_get_iscsi_port.assert_called_with()
-        mock_connection.assert_called_with(VOLUME, CONNECTOR, None)
-        self.assert_error_propagates([mock_get_iscsi_port, mock_connection],
-                                     self.driver.initialize_connection,
-                                     VOLUME, CONNECTOR)
-
-    @mock.patch(DRIVER_OBJ + "._connect")
-    @mock.patch(DRIVER_OBJ + "._get_target_iscsi_port")
-    def test_initialize_connection_with_auth(self, mock_get_iscsi_port,
-                                             mock_connection):
-        auth_type = "CHAP"
-        chap_username = CONNECTOR["host"]
-        chap_password = "password"
-        mock_get_iscsi_port.return_value = ISCSI_PORTS[0]
-        initiator_update = [{"key": pure.CHAP_SECRET_KEY,
-                            "value": chap_password}]
-        mock_connection.return_value = {
-            "vol": VOLUME["name"] + "-cinder",
-            "lun": 1,
-            "auth_username": chap_username,
-            "auth_password": chap_password,
-        }
-        result = CONNECTION_INFO.copy()
-        result["data"]["auth_method"] = auth_type
-        result["data"]["auth_username"] = chap_username
-        result["data"]["auth_password"] = chap_password
-
-        self.mock_config.use_chap_auth = True
-
-        # Branch where no credentials were generated
-        real_result = self.driver.initialize_connection(VOLUME,
-                                                        CONNECTOR)
-        mock_connection.assert_called_with(VOLUME, CONNECTOR, None)
-        self.assertDictMatch(result, real_result)
-
-        # Branch where new credentials were generated
-        mock_connection.return_value["initiator_update"] = initiator_update
-        result["initiator_update"] = initiator_update
-        real_result = self.driver.initialize_connection(VOLUME,
-                                                        CONNECTOR)
-        mock_connection.assert_called_with(VOLUME, CONNECTOR, None)
-        self.assertDictMatch(result, real_result)
-
-        self.assert_error_propagates([mock_get_iscsi_port, mock_connection],
-                                     self.driver.initialize_connection,
-                                     VOLUME, CONNECTOR)
-
-    @mock.patch(DRIVER_OBJ + "._choose_target_iscsi_port")
-    @mock.patch(DRIVER_OBJ + "._run_iscsiadm_bare")
-    def test_get_target_iscsi_port(self, mock_iscsiadm, mock_choose_port):
-        self.driver._iscsi_port = ISCSI_PORTS[1]
-        self.assertEqual(self.driver._get_target_iscsi_port(), ISCSI_PORTS[1])
-        mock_iscsiadm.assert_called_with(["-m", "discovery",
-                                          "-t", "sendtargets",
-                                          "-p", ISCSI_PORTS[1]["portal"]])
-        self.assertFalse(mock_choose_port.called)
-        mock_iscsiadm.side_effect = [processutils.ProcessExecutionError, None]
-        mock_choose_port.return_value = ISCSI_PORTS[2]
-        self.assertEqual(self.driver._get_target_iscsi_port(), ISCSI_PORTS[2])
-        mock_choose_port.assert_called_with()
-        mock_iscsiadm.side_effect = processutils.ProcessExecutionError
-        self.assert_error_propagates([mock_choose_port],
-                                     self.driver._get_target_iscsi_port)
-
-    @mock.patch(DRIVER_OBJ + "._run_iscsiadm_bare")
-    def test_choose_target_iscsi_port(self, mock_iscsiadm):
-        self.array.list_ports.return_value = PORTS_WITHOUT
-        self.assertRaises(exception.PureDriverException,
-                          self.driver._choose_target_iscsi_port)
-        self.array.list_ports.return_value = PORTS_WITH
-        self.assertEqual(ISCSI_PORTS[0],
-                         self.driver._choose_target_iscsi_port())
-        self.assert_error_propagates([mock_iscsiadm, self.array.list_ports],
-                                     self.driver._choose_target_iscsi_port)
-
-    @mock.patch(DRIVER_PATH + "._generate_chap_secret", autospec=True)
-    @mock.patch(DRIVER_OBJ + "._get_host", autospec=True)
-    @mock.patch(DRIVER_PATH + "._generate_purity_host_name", autospec=True)
-    def test_connect(self, mock_generate, mock_host, mock_gen_secret):
-        vol_name = VOLUME["name"] + "-cinder"
-        result = {"vol": vol_name, "lun": 1}
-
-        # Branch where host already exists
-        mock_host.return_value = PURE_HOST
-        self.array.connect_host.return_value = {"vol": vol_name, "lun": 1}
-        real_result = self.driver._connect(VOLUME, CONNECTOR, None)
-        self.assertEqual(result, real_result)
-        mock_host.assert_called_with(self.driver, CONNECTOR)
-        self.assertFalse(mock_generate.called)
-        self.assertFalse(self.array.create_host.called)
-        self.array.connect_host.assert_called_with(PURE_HOST_NAME, vol_name)
-
-        # Branch where new host is created
-        mock_host.return_value = None
-        mock_generate.return_value = PURE_HOST_NAME
-        real_result = self.driver._connect(VOLUME, CONNECTOR, None)
-        mock_host.assert_called_with(self.driver, CONNECTOR)
-        mock_generate.assert_called_with(HOSTNAME)
-        self.array.create_host.assert_called_with(PURE_HOST_NAME,
-                                                  iqnlist=[INITIATOR_IQN])
-        self.assertEqual(result, real_result)
-
-        mock_generate.reset_mock()
-        self.array.reset_mock()
-        self.assert_error_propagates(
-            [mock_host, mock_generate, self.array.connect_host,
-             self.array.create_host],
-            self.driver._connect, VOLUME, CONNECTOR, None)
-
-        self.mock_config.use_chap_auth = True
-        chap_user = CONNECTOR["host"]
-        chap_password = "sOmEseCr3t"
-
-        # Branch where chap is used and credentials already exist
-        initiator_data = [{"key": pure.CHAP_SECRET_KEY,
-                           "value": chap_password}]
-        self.driver._connect(VOLUME, CONNECTOR, initiator_data)
-        result["auth_username"] = chap_user
-        result["auth_password"] = chap_password
-        self.assertDictMatch(result, real_result)
-        self.array.set_host.assert_called_with(PURE_HOST_NAME,
-                                               host_user=chap_user,
-                                               host_password=chap_password)
-
-        # Branch where chap is used and credentials are generated
-        mock_gen_secret.return_value = chap_password
-        self.driver._connect(VOLUME, CONNECTOR, None)
-        result["auth_username"] = chap_user
-        result["auth_password"] = chap_password
-        result["initiator_update"] = {
-            "set_values": {
-                pure.CHAP_SECRET_KEY: chap_password
-            }
-        }
-        self.assertDictMatch(result, real_result)
-        self.array.set_host.assert_called_with(PURE_HOST_NAME,
-                                               host_user=chap_user,
-                                               host_password=chap_password)
-
-    @mock.patch(DRIVER_OBJ + "._get_host", autospec=True)
-    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_private_connections.return_value = \
-            [expected, {"host": "extra", "lun": 2}]
-        self.array.connect_host.side_effect = \
-            self.purestorage_module.PureHTTPError(
-                code=400,
-                text="Connection already exists"
-            )
-        actual = self.driver._connect(VOLUME, CONNECTOR, None)
-        self.assertEqual(expected, actual)
-        self.assertTrue(self.array.connect_host.called)
-        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_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, None)
-        self.assertTrue(self.array.connect_host.called)
-        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_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, None)
-        self.assertTrue(self.array.connect_host.called)
-        self.assertTrue(self.array.list_volume_private_connections)
-
-    def test_get_host(self):
-        good_host = PURE_HOST.copy()
-        good_host.update(iqn=["another-wrong-iqn", INITIATOR_IQN])
-        bad_host = {"name": "bad-host", "iqn": ["wrong-iqn"]}
-        self.array.list_hosts.return_value = [bad_host]
-        real_result = self.driver._get_host(CONNECTOR)
-        self.assertIs(real_result, None)
-        self.array.list_hosts.return_value.append(good_host)
-        real_result = self.driver._get_host(CONNECTOR)
-        self.assertEqual(real_result, good_host)
-        self.assert_error_propagates([self.array.list_hosts],
-                                     self.driver._get_host, CONNECTOR)
-
-    @mock.patch(DRIVER_OBJ + "._get_host", autospec=True)
+    @mock.patch(BASE_DRIVER_OBJ + "._get_host", autospec=True)
     def test_terminate_connection(self, mock_host):
         vol_name = VOLUME["name"] + "-cinder"
         mock_host.return_value = {"name": "some-host"}
@@ -656,82 +439,85 @@ class PureISCSIDriverTestCase(test.TestCase):
         self.assertFalse(self.array.list_host_connections.called)
         self.assertFalse(self.array.delete_host.called)
 
-    @mock.patch(DRIVER_OBJ + ".get_filter_function", autospec=True)
-    @mock.patch(DRIVER_OBJ + "._get_provisioned_space", autospec=True)
+    @mock.patch(BASE_DRIVER_OBJ + ".get_filter_function", autospec=True)
+    @mock.patch(BASE_DRIVER_OBJ + "._get_provisioned_space", autospec=True)
     def test_get_volume_stats(self, mock_space, mock_filter):
         filter_function = "capabilities.total_volumes < 10"
         mock_space.return_value = (PROVISIONED_CAPACITY * units.Gi, 100)
         mock_filter.return_value = filter_function
-        self.assertEqual(self.driver.get_volume_stats(), {})
+        self.assertEqual({}, self.driver.get_volume_stats())
         self.array.get.return_value = SPACE_INFO
-        result = {"volume_backend_name": VOLUME_BACKEND_NAME,
-                  "vendor_name": "Pure Storage",
-                  "driver_version": self.driver.VERSION,
-                  "storage_protocol": "iSCSI",
-                  "total_capacity_gb": TOTAL_CAPACITY,
-                  "free_capacity_gb": TOTAL_CAPACITY - USED_SPACE,
-                  "reserved_percentage": 0,
-                  "consistencygroup_support": True,
-                  "thin_provisioning_support": True,
-                  "provisioned_capacity": PROVISIONED_CAPACITY,
-                  "max_over_subscription_ratio": (PROVISIONED_CAPACITY /
-                                                  USED_SPACE),
-                  "total_volumes": 100,
-                  "filter_function": filter_function
-                  }
+        result = {
+            "volume_backend_name": VOLUME_BACKEND_NAME,
+            "vendor_name": "Pure Storage",
+            "driver_version": self.driver.VERSION,
+            "storage_protocol": None,
+            "total_capacity_gb": TOTAL_CAPACITY,
+            "free_capacity_gb": TOTAL_CAPACITY - USED_SPACE,
+            "reserved_percentage": 0,
+            "consistencygroup_support": True,
+            "thin_provisioning_support": True,
+            "provisioned_capacity": PROVISIONED_CAPACITY,
+            "max_over_subscription_ratio": (PROVISIONED_CAPACITY /
+                                            USED_SPACE),
+            "total_volumes": 100,
+            "filter_function": filter_function,
+        }
         real_result = self.driver.get_volume_stats(refresh=True)
         self.assertDictMatch(result, real_result)
         self.assertDictMatch(result, self.driver._stats)
 
-    @mock.patch(DRIVER_OBJ + ".get_filter_function", autospec=True)
-    @mock.patch(DRIVER_OBJ + "._get_provisioned_space", autospec=True)
+    @mock.patch(BASE_DRIVER_OBJ + ".get_filter_function", autospec=True)
+    @mock.patch(BASE_DRIVER_OBJ + "._get_provisioned_space", autospec=True)
     def test_get_volume_stats_empty_array(self, mock_space, mock_filter):
         filter_function = "capabilities.total_volumes < 10"
         mock_space.return_value = (PROVISIONED_CAPACITY * units.Gi, 100)
         mock_filter.return_value = filter_function
-        self.assertEqual(self.driver.get_volume_stats(), {})
+        self.assertEqual({}, self.driver.get_volume_stats())
         self.array.get.return_value = SPACE_INFO_EMPTY
-        result = {"volume_backend_name": VOLUME_BACKEND_NAME,
-                  "vendor_name": "Pure Storage",
-                  "driver_version": self.driver.VERSION,
-                  "storage_protocol": "iSCSI",
-                  "total_capacity_gb": TOTAL_CAPACITY,
-                  "free_capacity_gb": TOTAL_CAPACITY,
-                  "reserved_percentage": 0,
-                  "consistencygroup_support": True,
-                  "thin_provisioning_support": True,
-                  "provisioned_capacity": PROVISIONED_CAPACITY,
-                  "max_over_subscription_ratio": DEFAULT_OVER_SUBSCRIPTION,
-                  "total_volumes": 100,
-                  "filter_function": filter_function
-                  }
+        result = {
+            "volume_backend_name": VOLUME_BACKEND_NAME,
+            "vendor_name": "Pure Storage",
+            "driver_version": self.driver.VERSION,
+            "storage_protocol": None,
+            "total_capacity_gb": TOTAL_CAPACITY,
+            "free_capacity_gb": TOTAL_CAPACITY,
+            "reserved_percentage": 0,
+            "consistencygroup_support": True,
+            "thin_provisioning_support": True,
+            "provisioned_capacity": PROVISIONED_CAPACITY,
+            "max_over_subscription_ratio": DEFAULT_OVER_SUBSCRIPTION,
+            "total_volumes": 100,
+            "filter_function": filter_function,
+        }
         real_result = self.driver.get_volume_stats(refresh=True)
         self.assertDictMatch(result, real_result)
         self.assertDictMatch(result, self.driver._stats)
 
-    @mock.patch(DRIVER_OBJ + ".get_filter_function", autospec=True)
-    @mock.patch(DRIVER_OBJ + "._get_provisioned_space", autospec=True)
+    @mock.patch(BASE_DRIVER_OBJ + ".get_filter_function", autospec=True)
+    @mock.patch(BASE_DRIVER_OBJ + "._get_provisioned_space", autospec=True)
     def test_get_volume_stats_nothing_provisioned(self, mock_space,
                                                   mock_filter):
         filter_function = "capabilities.total_volumes < 10"
         mock_space.return_value = (0, 0)
         mock_filter.return_value = filter_function
-        self.assertEqual(self.driver.get_volume_stats(), {})
+        self.assertEqual({}, self.driver.get_volume_stats())
         self.array.get.return_value = SPACE_INFO
-        result = {"volume_backend_name": VOLUME_BACKEND_NAME,
-                  "vendor_name": "Pure Storage",
-                  "driver_version": self.driver.VERSION,
-                  "storage_protocol": "iSCSI",
-                  "total_capacity_gb": TOTAL_CAPACITY,
-                  "free_capacity_gb": TOTAL_CAPACITY - USED_SPACE,
-                  "reserved_percentage": 0,
-                  "consistencygroup_support": True,
-                  "thin_provisioning_support": True,
-                  "provisioned_capacity": 0,
-                  "max_over_subscription_ratio": DEFAULT_OVER_SUBSCRIPTION,
-                  "total_volumes": 0,
-                  "filter_function": filter_function
-                  }
+        result = {
+            "volume_backend_name": VOLUME_BACKEND_NAME,
+            "vendor_name": "Pure Storage",
+            "driver_version": self.driver.VERSION,
+            "storage_protocol": None,
+            "total_capacity_gb": TOTAL_CAPACITY,
+            "free_capacity_gb": TOTAL_CAPACITY - USED_SPACE,
+            "reserved_percentage": 0,
+            "consistencygroup_support": True,
+            "thin_provisioning_support": True,
+            "provisioned_capacity": 0,
+            "max_over_subscription_ratio": DEFAULT_OVER_SUBSCRIPTION,
+            "total_volumes": 0,
+            "filter_function": filter_function,
+        }
         real_result = self.driver.get_volume_stats(refresh=True)
         self.assertDictMatch(result, real_result)
         self.assertDictMatch(result, self.driver._stats)
@@ -746,14 +532,14 @@ class PureISCSIDriverTestCase(test.TestCase):
     def test_get_pgroup_name_from_id(self):
         id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
         expected_name = "consisgroup-%s-cinder" % id
-        actual_name = pure._get_pgroup_name_from_id(id)
+        actual_name = self.driver._get_pgroup_name_from_id(id)
         self.assertEqual(expected_name, actual_name)
 
     def test_get_pgroup_snap_suffix(self):
         cgsnap = mock.Mock()
         cgsnap.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
         expected_suffix = "cgsnapshot-%s-cinder" % cgsnap.id
-        actual_suffix = pure._get_pgroup_snap_suffix(cgsnap)
+        actual_suffix = self.driver._get_pgroup_snap_suffix(cgsnap)
         self.assertEqual(expected_suffix, actual_suffix)
 
     def test_get_pgroup_snap_name(self):
@@ -766,7 +552,7 @@ class PureISCSIDriverTestCase(test.TestCase):
         expected_name = "consisgroup-%(cg)s-cinder.cgsnapshot-%(snap)s-cinder"\
                         % {"cg": cg_id, "snap": cgsnap_id}
 
-        actual_name = pure._get_pgroup_snap_name(mock_cgsnap)
+        actual_name = self.driver._get_pgroup_snap_name(mock_cgsnap)
 
         self.assertEqual(expected_name, actual_name)
 
@@ -782,11 +568,13 @@ class PureISCSIDriverTestCase(test.TestCase):
         mock_snap.volume_name = volume_name
 
         expected_name = "consisgroup-%(cg)s-cinder.cgsnapshot-%(snap)s-cinder"\
-                        ".%(vol)s-cinder" % {"cg": cg_id,
-                                             "snap": cgsnap_id,
-                                             "vol": volume_name}
+                        ".%(vol)s-cinder" % {
+                            "cg": cg_id,
+                            "snap": cgsnap_id,
+                            "vol": volume_name,
+                        }
 
-        actual_name = pure._get_pgroup_vol_snap_name(mock_snap)
+        actual_name = self.driver._get_pgroup_vol_snap_name(mock_snap)
 
         self.assertEqual(expected_name, actual_name)
 
@@ -796,7 +584,7 @@ class PureISCSIDriverTestCase(test.TestCase):
 
         model_update = self.driver.create_consistencygroup(None, mock_cgroup)
 
-        expected_name = pure._get_pgroup_name_from_id(mock_cgroup.id)
+        expected_name = self.driver._get_pgroup_name_from_id(mock_cgroup.id)
         self.array.create_pgroup.assert_called_with(expected_name)
         self.assertEqual({'status': 'available'}, model_update)
 
@@ -804,8 +592,8 @@ class PureISCSIDriverTestCase(test.TestCase):
             [self.array.create_pgroup],
             self.driver.create_consistencygroup, None, mock_cgroup)
 
-    @mock.patch(DRIVER_OBJ + ".create_volume_from_snapshot")
-    @mock.patch(DRIVER_OBJ + ".create_consistencygroup")
+    @mock.patch(BASE_DRIVER_OBJ + ".create_volume_from_snapshot")
+    @mock.patch(BASE_DRIVER_OBJ + ".create_consistencygroup")
     def test_create_consistencygroup_from_src(self, mock_create_cg,
                                               mock_create_vol):
         mock_context = mock.Mock()
@@ -844,7 +632,7 @@ class PureISCSIDriverTestCase(test.TestCase):
                           mock.Mock(),  # group
                           [mock.Mock()])  # volumes
 
-    @mock.patch(DRIVER_OBJ + ".delete_volume", autospec=True)
+    @mock.patch(BASE_DRIVER_OBJ + ".delete_volume", autospec=True)
     def test_delete_consistencygroup(self, mock_delete_volume):
         mock_cgroup = mock.MagicMock()
         mock_cgroup.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
@@ -858,7 +646,7 @@ class PureISCSIDriverTestCase(test.TestCase):
         model_update, volumes = \
             self.driver.delete_consistencygroup(mock_context, mock_cgroup)
 
-        expected_name = pure._get_pgroup_name_from_id(mock_cgroup.id)
+        expected_name = self.driver._get_pgroup_name_from_id(mock_cgroup.id)
         self.array.destroy_pgroup.assert_called_with(expected_name)
         self.assertEqual(expected_volumes, volumes)
         self.assertEqual(mock_cgroup['status'], model_update['status'])
@@ -924,7 +712,7 @@ class PureISCSIDriverTestCase(test.TestCase):
         expected_addvollist = [vol['name'] + '-cinder' for vol in add_vols]
         remove_vols = [
             {'name': 'vol4'},
-            {'name': 'vol5'}
+            {'name': 'vol5'},
         ]
         expected_remvollist = [vol['name'] + '-cinder' for vol in remove_vols]
         self.driver.update_consistencygroup(mock.Mock(), mock_group,
@@ -940,7 +728,7 @@ class PureISCSIDriverTestCase(test.TestCase):
         expected_addvollist = []
         remove_vols = [
             {'name': 'vol4'},
-            {'name': 'vol5'}
+            {'name': 'vol5'},
         ]
         expected_remvollist = [vol['name'] + '-cinder' for vol in remove_vols]
         self.driver.update_consistencygroup(mock.Mock(), mock_group,
@@ -993,9 +781,9 @@ class PureISCSIDriverTestCase(test.TestCase):
         model_update, snapshots = \
             self.driver.create_cgsnapshot(mock_context, mock_cgsnap)
 
-        expected_pgroup_name = \
-            pure._get_pgroup_name_from_id(mock_cgsnap.consistencygroup_id)
-        expected_snap_suffix = pure._get_pgroup_snap_suffix(mock_cgsnap)
+        cg_id = mock_cgsnap.consistencygroup_id
+        expected_pgroup_name = self.driver._get_pgroup_name_from_id(cg_id)
+        expected_snap_suffix = self.driver._get_pgroup_snap_suffix(mock_cgsnap)
         self.array.create_pgroup_snapshot\
             .assert_called_with(expected_pgroup_name,
                                 suffix=expected_snap_suffix)
@@ -1007,7 +795,8 @@ class PureISCSIDriverTestCase(test.TestCase):
             [self.array.create_pgroup_snapshot],
             self.driver.create_cgsnapshot, mock_context, mock_cgsnap)
 
-    @mock.patch(DRIVER_PATH + "._get_pgroup_snap_name", autospec=True)
+    @mock.patch(BASE_DRIVER_OBJ + "._get_pgroup_snap_name",
+                spec=pure.PureBaseVolumeDriver._get_pgroup_snap_name)
     def test_delete_cgsnapshot(self, mock_get_snap_name):
         snap_name = "consisgroup-4a2f7e3a-312a-40c5-96a8-536b8a0f" \
                     "e074-cinder.4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
@@ -1180,3 +969,246 @@ class PureISCSIDriverTestCase(test.TestCase):
 
         self.array.rename_volume.assert_called_with(vol_name,
                                                     unmanaged_vol_name)
+
+
+class PureISCSIDriverTestCase(PureDriverTestCase):
+
+    def setUp(self):
+        super(PureISCSIDriverTestCase, self).setUp()
+        self.mock_config.use_chap_auth = False
+        self.driver = pure.PureISCSIDriver(configuration=self.mock_config)
+        self.driver._array = self.array
+
+    @mock.patch(ISCSI_DRIVER_OBJ + "._choose_target_iscsi_port")
+    def test_do_setup(self, mock_choose_target_iscsi_port):
+        mock_choose_target_iscsi_port.return_value = ISCSI_PORTS[0]
+        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)
+        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(
+            [
+                self.purestorage_module.FlashArray,
+                mock_choose_target_iscsi_port
+            ],
+            self.driver.do_setup, None
+        )
+
+    def test_get_host(self):
+        good_host = PURE_HOST.copy()
+        good_host.update(iqn=["another-wrong-iqn", INITIATOR_IQN])
+        bad_host = {"name": "bad-host", "iqn": ["wrong-iqn"]}
+        self.array.list_hosts.return_value = [bad_host]
+        real_result = self.driver._get_host(CONNECTOR)
+        self.assertIs(real_result, None)
+        self.array.list_hosts.return_value.append(good_host)
+        real_result = self.driver._get_host(CONNECTOR)
+        self.assertEqual(good_host, real_result)
+        self.assert_error_propagates([self.array.list_hosts],
+                                     self.driver._get_host, CONNECTOR)
+
+    @mock.patch(ISCSI_DRIVER_OBJ + "._connect")
+    @mock.patch(ISCSI_DRIVER_OBJ + "._get_target_iscsi_port")
+    def test_initialize_connection(self, mock_get_iscsi_port, mock_connection):
+        mock_get_iscsi_port.return_value = ISCSI_PORTS[0]
+        mock_connection.return_value = {
+            "vol": VOLUME["name"] + "-cinder",
+            "lun": 1,
+        }
+        result = CONNECTION_INFO
+        real_result = self.driver.initialize_connection(VOLUME, CONNECTOR)
+        self.assertDictMatch(result, real_result)
+        mock_get_iscsi_port.assert_called_with()
+        mock_connection.assert_called_with(VOLUME, CONNECTOR, None)
+        self.assert_error_propagates([mock_get_iscsi_port, mock_connection],
+                                     self.driver.initialize_connection,
+                                     VOLUME, CONNECTOR)
+
+    @mock.patch(ISCSI_DRIVER_OBJ + "._connect")
+    @mock.patch(ISCSI_DRIVER_OBJ + "._get_target_iscsi_port")
+    def test_initialize_connection_with_auth(self, mock_get_iscsi_port,
+                                             mock_connection):
+        auth_type = "CHAP"
+        chap_username = CONNECTOR["host"]
+        chap_password = "password"
+        mock_get_iscsi_port.return_value = ISCSI_PORTS[0]
+        initiator_update = [{"key": pure.CHAP_SECRET_KEY,
+                            "value": chap_password}]
+        mock_connection.return_value = {
+            "vol": VOLUME["name"] + "-cinder",
+            "lun": 1,
+            "auth_username": chap_username,
+            "auth_password": chap_password,
+        }
+        result = CONNECTION_INFO.copy()
+        result["data"]["auth_method"] = auth_type
+        result["data"]["auth_username"] = chap_username
+        result["data"]["auth_password"] = chap_password
+
+        self.mock_config.use_chap_auth = True
+
+        # Branch where no credentials were generated
+        real_result = self.driver.initialize_connection(VOLUME,
+                                                        CONNECTOR)
+        mock_connection.assert_called_with(VOLUME, CONNECTOR, None)
+        self.assertDictMatch(result, real_result)
+
+        # Branch where new credentials were generated
+        mock_connection.return_value["initiator_update"] = initiator_update
+        result["initiator_update"] = initiator_update
+        real_result = self.driver.initialize_connection(VOLUME,
+                                                        CONNECTOR)
+        mock_connection.assert_called_with(VOLUME, CONNECTOR, None)
+        self.assertDictMatch(result, real_result)
+
+        self.assert_error_propagates([mock_get_iscsi_port, mock_connection],
+                                     self.driver.initialize_connection,
+                                     VOLUME, CONNECTOR)
+
+    @mock.patch(ISCSI_DRIVER_OBJ + "._choose_target_iscsi_port")
+    @mock.patch(ISCSI_DRIVER_OBJ + "._run_iscsiadm_bare")
+    def test_get_target_iscsi_port(self, mock_iscsiadm, mock_choose_port):
+        self.driver._iscsi_port = ISCSI_PORTS[1]
+        self.assertEqual(ISCSI_PORTS[1], self.driver._get_target_iscsi_port())
+        mock_iscsiadm.assert_called_with(["-m", "discovery",
+                                          "-t", "sendtargets",
+                                          "-p", ISCSI_PORTS[1]["portal"]])
+        self.assertFalse(mock_choose_port.called)
+        mock_iscsiadm.side_effect = [processutils.ProcessExecutionError, None]
+        mock_choose_port.return_value = ISCSI_PORTS[2]
+        self.assertEqual(ISCSI_PORTS[2], self.driver._get_target_iscsi_port())
+        mock_choose_port.assert_called_with()
+        mock_iscsiadm.side_effect = processutils.ProcessExecutionError
+        self.assert_error_propagates([mock_choose_port],
+                                     self.driver._get_target_iscsi_port)
+
+    @mock.patch(ISCSI_DRIVER_OBJ + "._run_iscsiadm_bare")
+    def test_choose_target_iscsi_port(self, mock_iscsiadm):
+        self.array.list_ports.return_value = PORTS_WITHOUT
+        self.assertRaises(exception.PureDriverException,
+                          self.driver._choose_target_iscsi_port)
+        self.array.list_ports.return_value = PORTS_WITH
+        self.assertEqual(ISCSI_PORTS[0],
+                         self.driver._choose_target_iscsi_port())
+        self.assert_error_propagates([mock_iscsiadm, self.array.list_ports],
+                                     self.driver._choose_target_iscsi_port)
+
+    @mock.patch("cinder.volume.utils.generate_password", autospec=True)
+    @mock.patch(ISCSI_DRIVER_OBJ + "._get_host", autospec=True)
+    @mock.patch(ISCSI_DRIVER_OBJ + "._generate_purity_host_name", spec=True)
+    def test_connect(self, mock_generate, mock_host, mock_gen_secret):
+        vol_name = VOLUME["name"] + "-cinder"
+        result = {"vol": vol_name, "lun": 1}
+
+        # Branch where host already exists
+        mock_host.return_value = PURE_HOST
+        self.array.connect_host.return_value = {"vol": vol_name, "lun": 1}
+        real_result = self.driver._connect(VOLUME, CONNECTOR, None)
+        self.assertEqual(result, real_result)
+        mock_host.assert_called_with(self.driver, CONNECTOR)
+        self.assertFalse(mock_generate.called)
+        self.assertFalse(self.array.create_host.called)
+        self.array.connect_host.assert_called_with(PURE_HOST_NAME, vol_name)
+
+        # Branch where new host is created
+        mock_host.return_value = None
+        mock_generate.return_value = PURE_HOST_NAME
+        real_result = self.driver._connect(VOLUME, CONNECTOR, None)
+        mock_host.assert_called_with(self.driver, CONNECTOR)
+        mock_generate.assert_called_with(HOSTNAME)
+        self.array.create_host.assert_called_with(PURE_HOST_NAME,
+                                                  iqnlist=[INITIATOR_IQN])
+        self.assertEqual(result, real_result)
+
+        mock_generate.reset_mock()
+        self.array.reset_mock()
+        self.assert_error_propagates(
+            [mock_host, mock_generate, self.array.connect_host,
+             self.array.create_host],
+            self.driver._connect, VOLUME, CONNECTOR, None)
+
+        self.mock_config.use_chap_auth = True
+        chap_user = CONNECTOR["host"]
+        chap_password = "sOmEseCr3t"
+
+        # Branch where chap is used and credentials already exist
+        initiator_data = [{"key": pure.CHAP_SECRET_KEY,
+                           "value": chap_password}]
+        self.driver._connect(VOLUME, CONNECTOR, initiator_data)
+        result["auth_username"] = chap_user
+        result["auth_password"] = chap_password
+        self.assertDictMatch(result, real_result)
+        self.array.set_host.assert_called_with(PURE_HOST_NAME,
+                                               host_user=chap_user,
+                                               host_password=chap_password)
+
+        # Branch where chap is used and credentials are generated
+        mock_gen_secret.return_value = chap_password
+        self.driver._connect(VOLUME, CONNECTOR, None)
+        result["auth_username"] = chap_user
+        result["auth_password"] = chap_password
+        result["initiator_update"] = {
+            "set_values": {
+                pure.CHAP_SECRET_KEY: chap_password
+            },
+        }
+        self.assertDictMatch(result, real_result)
+        self.array.set_host.assert_called_with(PURE_HOST_NAME,
+                                               host_user=chap_user,
+                                               host_password=chap_password)
+
+    @mock.patch(ISCSI_DRIVER_OBJ + "._get_host", autospec=True)
+    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_private_connections.return_value = \
+            [expected, {"host": "extra", "lun": 2}]
+        self.array.connect_host.side_effect = \
+            self.purestorage_module.PureHTTPError(
+                code=400,
+                text="Connection already exists"
+            )
+        actual = self.driver._connect(VOLUME, CONNECTOR, None)
+        self.assertEqual(expected, actual)
+        self.assertTrue(self.array.connect_host.called)
+        self.assertTrue(self.array.list_volume_private_connections)
+
+    @mock.patch(ISCSI_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_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, None)
+        self.assertTrue(self.array.connect_host.called)
+        self.assertTrue(self.array.list_volume_private_connections)
+
+    @mock.patch(ISCSI_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_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, None)
+        self.assertTrue(self.array.connect_host.called)
+        self.assertTrue(self.array.list_volume_private_connections)
index 5a0d7af245e76418eae40ffe59776bf69ab1d557..59411afa5df2e2fa31d2e8ec713d6a9820133edc 100644 (file)
@@ -59,64 +59,31 @@ ERR_MSG_NOT_EXIST = "does not exist"
 ERR_MSG_PENDING_ERADICATION = "has been destroyed"
 
 
-def _get_vol_name(volume):
-    """Return the name of the volume Purity will use."""
-    return volume["name"] + "-cinder"
+def log_debug_trace(f):
+    def wrapper(*args, **kwargs):
+        cls_name = args[0].__class__.__name__
+        method_name = "%(cls_name)s.%(method)s" % {"cls_name": cls_name,
+                                                   "method": f.func_name}
+        LOG.debug("Enter " + method_name)
+        result = f(*args, **kwargs)
+        LOG.debug("Leave " + method_name)
+        return result
 
+    return wrapper
 
-def _get_snap_name(snapshot):
-    """Return the name of the snapshot that Purity will use."""
-    return "%s-cinder.%s" % (snapshot["volume_name"], snapshot["name"])
 
-
-def _get_pgroup_name_from_id(id):
-    return "consisgroup-%s-cinder" % id
-
-
-def _get_pgroup_snap_suffix(cgsnapshot):
-    return "cgsnapshot-%s-cinder" % cgsnapshot.id
-
-
-def _get_pgroup_snap_name(cgsnapshot):
-    """Return the name of the pgroup snapshot that Purity will use"""
-    return "%s.%s" % (_get_pgroup_name_from_id(cgsnapshot.consistencygroup_id),
-                      _get_pgroup_snap_suffix(cgsnapshot))
-
-
-def _get_pgroup_vol_snap_name(snapshot):
-    """Return the name of the snapshot that Purity will use for a volume."""
-    cg_name = _get_pgroup_name_from_id(snapshot.cgsnapshot.consistencygroup_id)
-    cgsnapshot_id = _get_pgroup_snap_suffix(snapshot.cgsnapshot)
-    volume_name = snapshot.volume_name
-    return "%s.%s.%s-cinder" % (cg_name, cgsnapshot_id, volume_name)
-
-
-def _generate_purity_host_name(name):
-    """Return a valid Purity host name based on the name passed in."""
-    if len(name) > 23:
-        name = name[0:23]
-    name = INVALID_CHARACTERS.sub("-", name)
-    name = name.lstrip("-")
-    return "{name}-{uuid}-cinder".format(name=name, uuid=uuid.uuid4().hex)
-
-
-def _generate_chap_secret():
-    return volume_utils.generate_password()
-
-
-class PureISCSIDriver(san.SanISCSIDriver):
+class PureBaseVolumeDriver(san.SanDriver):
     """Performs volume management on Pure Storage FlashArray."""
 
-    VERSION = "2.0.6"
-
     SUPPORTED_REST_API_VERSIONS = ['1.2', '1.3', '1.4']
 
     def __init__(self, *args, **kwargs):
         execute = kwargs.pop("execute", utils.execute)
-        super(PureISCSIDriver, self).__init__(execute=execute, *args, **kwargs)
+        super(PureBaseVolumeDriver, self).__init__(execute=execute, *args,
+                                                   **kwargs)
         self.configuration.append_config_values(PURE_OPTS)
         self._array = None
-        self._iscsi_port = None
+        self._storage_protocol = None
         self._backend_name = (self.configuration.volume_backend_name or
                               self.__class__.__name__)
 
@@ -134,17 +101,16 @@ class PureISCSIDriver(san.SanISCSIDriver):
         self._array = purestorage.FlashArray(
             self.configuration.san_ip,
             api_token=self.configuration.pure_api_token)
-        self._iscsi_port = self._choose_target_iscsi_port()
 
     def check_for_setup_error(self):
         # Avoid inheriting check_for_setup_error from SanDriver, which checks
         # for san_password or san_private_key, not relevant to our driver.
         pass
 
+    @log_debug_trace
     def create_volume(self, volume):
         """Creates a volume."""
-        LOG.debug("Enter PureISCSIDriver.create_volume.")
-        vol_name = _get_vol_name(volume)
+        vol_name = self._get_vol_name(volume)
         vol_size = volume["size"] * units.Gi
         self._array.create_volume(vol_name, vol_size)
 
@@ -153,16 +119,15 @@ class PureISCSIDriver(san.SanISCSIDriver):
                 volume['consistencygroup_id'],
                 vol_name
             )
-        LOG.debug("Leave PureISCSIDriver.create_volume.")
 
+    @log_debug_trace
     def create_volume_from_snapshot(self, volume, snapshot):
         """Creates a volume from a snapshot."""
-        LOG.debug("Enter PureISCSIDriver.create_volume_from_snapshot.")
-        vol_name = _get_vol_name(volume)
+        vol_name = self._get_vol_name(volume)
         if snapshot['cgsnapshot_id']:
-            snap_name = _get_pgroup_vol_snap_name(snapshot)
+            snap_name = self._get_pgroup_vol_snap_name(snapshot)
         else:
-            snap_name = _get_snap_name(snapshot)
+            snap_name = self._get_snap_name(snapshot)
 
         self._array.copy_volume(snap_name, vol_name)
         self._extend_if_needed(vol_name, snapshot["volume_size"],
@@ -172,13 +137,12 @@ class PureISCSIDriver(san.SanISCSIDriver):
                 volume['consistencygroup_id'],
                 vol_name
             )
-        LOG.debug("Leave PureISCSIDriver.create_volume_from_snapshot.")
 
+    @log_debug_trace
     def create_cloned_volume(self, volume, src_vref):
         """Creates a clone of the specified volume."""
-        LOG.debug("Enter PureISCSIDriver.create_cloned_volume.")
-        vol_name = _get_vol_name(volume)
-        src_name = _get_vol_name(src_vref)
+        vol_name = self._get_vol_name(volume)
+        src_name = self._get_vol_name(src_vref)
         self._array.copy_volume(src_name, vol_name)
         self._extend_if_needed(vol_name, src_vref["size"], volume["size"])
 
@@ -188,18 +152,16 @@ class PureISCSIDriver(san.SanISCSIDriver):
                 vol_name
             )
 
-        LOG.debug("Leave PureISCSIDriver.create_cloned_volume.")
-
     def _extend_if_needed(self, vol_name, src_size, vol_size):
         """Extend the volume from size src_size to size vol_size."""
         if vol_size > src_size:
             vol_size = vol_size * units.Gi
             self._array.extend_volume(vol_name, vol_size)
 
+    @log_debug_trace
     def delete_volume(self, volume):
         """Disconnect all hosts and delete the volume"""
-        LOG.debug("Enter PureISCSIDriver.delete_volume.")
-        vol_name = _get_vol_name(volume)
+        vol_name = self._get_vol_name(volume)
         try:
             connected_hosts = \
                 self._array.list_volume_private_connections(vol_name)
@@ -215,19 +177,17 @@ class PureISCSIDriver(san.SanISCSIDriver):
                     ctxt.reraise = False
                     LOG.warning(_LW("Volume deletion failed with message: %s"),
                                 err.text)
-        LOG.debug("Leave PureISCSIDriver.delete_volume.")
 
+    @log_debug_trace
     def create_snapshot(self, snapshot):
         """Creates a snapshot."""
-        LOG.debug("Enter PureISCSIDriver.create_snapshot.")
-        vol_name, snap_suff = _get_snap_name(snapshot).split(".")
+        vol_name, snap_suff = self._get_snap_name(snapshot).split(".")
         self._array.create_snapshot(vol_name, suffix=snap_suff)
-        LOG.debug("Leave PureISCSIDriver.create_snapshot.")
 
+    @log_debug_trace
     def delete_snapshot(self, snapshot):
         """Deletes a snapshot."""
-        LOG.debug("Enter PureISCSIDriver.delete_snapshot.")
-        snap_name = _get_snap_name(snapshot)
+        snap_name = self._get_snap_name(snapshot)
         try:
             self._array.destroy_volume(snap_name)
         except purestorage.PureHTTPError as err:
@@ -237,7 +197,6 @@ class PureISCSIDriver(san.SanISCSIDriver):
                     ctxt.reraise = False
                     LOG.error(_LE("Snapshot deletion failed with message:"
                                   " %s"), err.text)
-        LOG.debug("Leave PureISCSIDriver.delete_snapshot.")
 
     def ensure_export(self, context, volume):
         pass
@@ -245,190 +204,26 @@ class PureISCSIDriver(san.SanISCSIDriver):
     def create_export(self, context, volume):
         pass
 
-    def initialize_connection(self, volume, connector, initiator_data=None):
-        """Allow connection to connector and return connection info."""
-        LOG.debug("Enter PureISCSIDriver.initialize_connection.")
-        target_port = self._get_target_iscsi_port()
-        connection = self._connect(volume, connector, initiator_data)
-        properties = {
-            "driver_volume_type": "iscsi",
-            "data": {
-                "target_iqn": target_port["iqn"],
-                "target_portal": target_port["portal"],
-                "target_lun": connection["lun"],
-                "target_discovered": True,
-                "access_mode": "rw",
-            },
-        }
-
-        if self.configuration.use_chap_auth:
-            properties["data"]["auth_method"] = "CHAP"
-            properties["data"]["auth_username"] = connection["auth_username"]
-            properties["data"]["auth_password"] = connection["auth_password"]
-
-        initiator_update = connection.get("initiator_update", False)
-        if initiator_update:
-            properties["initiator_update"] = initiator_update
-
-        LOG.debug("Leave PureISCSIDriver.initialize_connection.")
-        return properties
-
-    def _get_target_iscsi_port(self):
-        """Return dictionary describing iSCSI-enabled port on target array."""
-        try:
-            self._run_iscsiadm_bare(["-m", "discovery", "-t", "sendtargets",
-                                     "-p", self._iscsi_port["portal"]])
-        except processutils.ProcessExecutionError as err:
-            LOG.warning(_LW("iSCSI discovery of port %(port_name)s at "
-                            "%(port_portal)s failed with error: %(err_msg)s"),
-                        {"port_name": self._iscsi_port["name"],
-                         "port_portal": self._iscsi_port["portal"],
-                         "err_msg": err.stderr})
-            self._iscsi_port = self._choose_target_iscsi_port()
-        return self._iscsi_port
-
-    @utils.retry(exception.PureDriverException, retries=3)
-    def _choose_target_iscsi_port(self):
-        """Find a reachable iSCSI-enabled port on target array."""
-        ports = self._array.list_ports()
-        iscsi_ports = [port for port in ports if port["iqn"]]
-        for port in iscsi_ports:
-            try:
-                self._run_iscsiadm_bare(["-m", "discovery",
-                                         "-t", "sendtargets",
-                                         "-p", port["portal"]])
-            except processutils.ProcessExecutionError as err:
-                LOG.debug(("iSCSI discovery of port %(port_name)s at "
-                           "%(port_portal)s failed with error: %(err_msg)s"),
-                          {"port_name": port["name"],
-                           "port_portal": port["portal"],
-                           "err_msg": err.stderr})
-            else:
-                LOG.info(_LI("Using port %(name)s on the array at %(portal)s "
-                             "for iSCSI connectivity."),
-                         {"name": port["name"], "portal": port["portal"]})
-                return port
-        raise exception.PureDriverException(
-            reason=_("No reachable iSCSI-enabled ports on target array."))
-
-    def _get_chap_credentials(self, host, data):
-        initiator_updates = None
-        username = host
-        password = None
-        if data:
-            for d in data:
-                if d["key"] == CHAP_SECRET_KEY:
-                    password = d["value"]
-                    break
-        if not password:
-            password = _generate_chap_secret()
-            initiator_updates = {
-                "set_values": {
-                    CHAP_SECRET_KEY: password
-                }
-            }
-        return username, password, initiator_updates
-
-    @utils.synchronized('PureISCSIDriver._connect', external=True)
-    def _connect(self, volume, connector, initiator_data):
-        """Connect the host and volume; return dict describing connection."""
-        connection = None
-        iqn = connector["initiator"]
-
-        if self.configuration.use_chap_auth:
-            (chap_username, chap_password, initiator_update) = \
-                self._get_chap_credentials(connector['host'], initiator_data)
-
-        vol_name = _get_vol_name(volume)
-        host = self._get_host(connector)
-
-        if host:
-            host_name = host["name"]
-            LOG.info(_LI("Re-using existing purity host %(host_name)r"),
-                     {"host_name": host_name})
-            if self.configuration.use_chap_auth:
-                if not GENERATED_NAME.match(host_name):
-                    LOG.error(_LE("Purity host %(host_name)s is not managed "
-                                  "by Cinder and can't have CHAP credentials "
-                                  "modified. Remove IQN %(iqn)s from the host "
-                                  "to resolve this issue."),
-                              {"host_name": host_name,
-                               "iqn": connector["initiator"]})
-                    raise exception.PureDriverException(
-                        reason=_("Unable to re-use a host that is not "
-                                 "managed by Cinder with use_chap_auth=True,"))
-                elif chap_username is None or chap_password is None:
-                    LOG.error(_LE("Purity host %(host_name)s is managed by "
-                                  "Cinder but CHAP credentials could not be "
-                                  "retrieved from the Cinder database."),
-                              {"host_name": host_name})
-                    raise exception.PureDriverException(
-                        reason=_("Unable to re-use host with unknown CHAP "
-                                 "credentials configured."))
-        else:
-            host_name = _generate_purity_host_name(connector["host"])
-            LOG.info(_LI("Creating host object %(host_name)r with IQN:"
-                         " %(iqn)s."), {"host_name": host_name, "iqn": iqn})
-            self._array.create_host(host_name, iqnlist=[iqn])
-
-            if self.configuration.use_chap_auth:
-                self._array.set_host(host_name,
-                                     host_user=chap_username,
-                                     host_password=chap_password)
-
-        try:
-            connection = self._array.connect_host(host_name, vol_name)
-        except purestorage.PureHTTPError as err:
-            with excutils.save_and_reraise_exception() as ctxt:
-                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.warning(_LW("Volume connection already exists with "
-                                    "message: %s"), err.text)
-                    # Get the info for the existing connection
-                    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
-                            break
-        if not connection:
-            raise exception.PureDriverException(
-                reason=_("Unable to connect or find connection to host"))
-
-        if self.configuration.use_chap_auth:
-            connection["auth_username"] = chap_username
-            connection["auth_password"] = chap_password
-
-            if initiator_update:
-                connection["initiator_update"] = initiator_update
-
-        return connection
-
     def _get_host(self, connector):
-        """Return dict describing existing Purity host object or None."""
-        hosts = self._array.list_hosts()
-        for host in hosts:
-            if connector["initiator"] in host["iqn"]:
-                return host
-        return None
+        """Get a Purity Host that corresponds to the host in the connector.
+
+        This implementation is specific to the host type (iSCSI, FC, etc).
+        """
+        raise NotImplementedError
 
+    @log_debug_trace
     def terminate_connection(self, volume, connector, **kwargs):
         """Terminate connection."""
-        LOG.debug("Enter PureISCSIDriver.terminate_connection.")
-        vol_name = _get_vol_name(volume)
+        vol_name = self._get_vol_name(volume)
         host = self._get_host(connector)
         if host:
             host_name = host["name"]
             self._disconnect_host(host_name, vol_name)
         else:
-            LOG.error(_LE("Unable to find host object in Purity with IQN: "
-                          "%(iqn)s."), {"iqn": connector["initiator"]})
-        LOG.debug("Leave PureISCSIDriver.terminate_connection.")
+            LOG.error(_LE("Unable to disconnect host from volume."))
 
+    @log_debug_trace
     def _disconnect_host(self, host_name, vol_name):
-        LOG.debug("Enter PureISCSIDriver._disconnect_host.")
         try:
             self._array.disconnect_host(host_name, vol_name)
         except purestorage.PureHTTPError as err:
@@ -444,19 +239,17 @@ class PureISCSIDriver(san.SanISCSIDriver):
             LOG.info(_LI("Deleting unneeded host %(host_name)r."),
                      {"host_name": host_name})
             self._array.delete_host(host_name)
-        LOG.debug("Leave PureISCSIDriver._disconnect_host.")
 
+    @log_debug_trace
     def get_volume_stats(self, refresh=False):
         """Return the current state of the volume service.
 
         If 'refresh' is True, run the update first.
         """
 
-        LOG.debug("Enter PureISCSIDriver.get_volume_stats.")
         if refresh:
             LOG.debug("Updating volume stats.")
             self._update_stats()
-        LOG.debug("Leave PureISCSIDriver.get_volume_stats.")
         return self._stats
 
     def _update_stats(self):
@@ -476,20 +269,21 @@ class PureISCSIDriver(san.SanISCSIDriver):
             thin_provisioning = 20
         else:
             thin_provisioning = provisioned_space / used_space
-        data = {"volume_backend_name": self._backend_name,
-                "vendor_name": "Pure Storage",
-                "driver_version": self.VERSION,
-                "storage_protocol": "iSCSI",
-                "total_capacity_gb": total_capacity,
-                "free_capacity_gb": free_space,
-                "reserved_percentage": 0,
-                "consistencygroup_support": True,
-                "thin_provisioning_support": True,
-                "provisioned_capacity": provisioned_space,
-                "max_over_subscription_ratio": thin_provisioning,
-                "total_volumes": total_vols,
-                "filter_function": self.get_filter_function()
-                }
+        data = {
+            "volume_backend_name": self._backend_name,
+            "vendor_name": "Pure Storage",
+            "driver_version": self.VERSION,
+            "storage_protocol": self._storage_protocol,
+            "total_capacity_gb": total_capacity,
+            "free_capacity_gb": free_space,
+            "reserved_percentage": 0,
+            "consistencygroup_support": True,
+            "thin_provisioning_support": True,
+            "provisioned_capacity": provisioned_space,
+            "max_over_subscription_ratio": thin_provisioning,
+            "total_volumes": total_vols,
+            "filter_function": self.get_filter_function(),
+        }
         self._stats = data
 
     def _get_provisioned_space(self):
@@ -497,32 +291,29 @@ class PureISCSIDriver(san.SanISCSIDriver):
         volumes = self._array.list_volumes(pending=True)
         return sum(item["size"] for item in volumes), len(volumes)
 
+    @log_debug_trace
     def extend_volume(self, volume, new_size):
         """Extend volume to new_size."""
-        LOG.debug("Enter PureISCSIDriver.extend_volume.")
-        vol_name = _get_vol_name(volume)
+        vol_name = self._get_vol_name(volume)
         new_size = new_size * units.Gi
         self._array.extend_volume(vol_name, new_size)
-        LOG.debug("Leave PureISCSIDriver.extend_volume.")
 
     def _add_volume_to_consistency_group(self, consistencygroup_id, vol_name):
-        pgroup_name = _get_pgroup_name_from_id(consistencygroup_id)
+        pgroup_name = self._get_pgroup_name_from_id(consistencygroup_id)
         self._array.set_pgroup(pgroup_name, addvollist=[vol_name])
 
+    @log_debug_trace
     def create_consistencygroup(self, context, group):
         """Creates a consistencygroup."""
-        LOG.debug("Enter PureISCSIDriver.create_consistencygroup")
 
-        self._array.create_pgroup(_get_pgroup_name_from_id(group.id))
+        self._array.create_pgroup(self._get_pgroup_name_from_id(group.id))
 
         model_update = {'status': 'available'}
-
-        LOG.debug("Leave PureISCSIDriver.create_consistencygroup")
         return model_update
 
+    @log_debug_trace
     def create_consistencygroup_from_src(self, context, group, volumes,
                                          cgsnapshot=None, snapshots=None):
-        LOG.debug("Enter PureISCSIDriver.create_consistencygroup_from_src")
 
         if cgsnapshot and snapshots:
             self.create_consistencygroup(context, group)
@@ -531,17 +322,16 @@ class PureISCSIDriver(san.SanISCSIDriver):
         else:
             msg = _("create_consistencygroup_from_src only supports a"
                     " cgsnapshot source, other sources cannot be used.")
-            raise exception.InvalidInput(msg)
+            raise exception.InvalidInput(reason=msg)
 
-        LOG.debug("Leave PureISCSIDriver.create_consistencygroup_from_src")
         return None, None
 
+    @log_debug_trace
     def delete_consistencygroup(self, context, group):
         """Deletes a consistency group."""
-        LOG.debug("Enter PureISCSIDriver.delete_consistencygroup")
 
         try:
-            self._array.destroy_pgroup(_get_pgroup_name_from_id(group.id))
+            self._array.destroy_pgroup(self._get_pgroup_name_from_id(group.id))
         except purestorage.PureHTTPError as err:
             with excutils.save_and_reraise_exception() as ctxt:
                 if (err.code == 400 and
@@ -561,36 +351,35 @@ class PureISCSIDriver(san.SanISCSIDriver):
 
         model_update = {'status': group['status']}
 
-        LOG.debug("Leave PureISCSIDriver.delete_consistencygroup")
         return model_update, volumes
 
+    @log_debug_trace
     def update_consistencygroup(self, context, group,
                                 add_volumes=None, remove_volumes=None):
-        LOG.debug("Enter PureISCSIDriver.update_consistencygroup")
 
-        pgroup_name = _get_pgroup_name_from_id(group.id)
+        pgroup_name = self._get_pgroup_name_from_id(group.id)
         if add_volumes:
-            addvollist = [_get_vol_name(volume) for volume in add_volumes]
+            addvollist = [self._get_vol_name(vol) for vol in add_volumes]
         else:
             addvollist = []
 
         if remove_volumes:
-            remvollist = [_get_vol_name(volume) for volume in remove_volumes]
+            remvollist = [self._get_vol_name(vol) for vol in remove_volumes]
         else:
             remvollist = []
 
         self._array.set_pgroup(pgroup_name, addvollist=addvollist,
                                remvollist=remvollist)
 
-        LOG.debug("Leave PureISCSIDriver.update_consistencygroup")
         return None, None, None
 
+    @log_debug_trace
     def create_cgsnapshot(self, context, cgsnapshot):
         """Creates a cgsnapshot."""
-        LOG.debug("Enter PureISCSIDriver.create_cgsnapshot")
 
-        pgroup_name = _get_pgroup_name_from_id(cgsnapshot.consistencygroup_id)
-        pgsnap_suffix = _get_pgroup_snap_suffix(cgsnapshot)
+        cg_id = cgsnapshot.consistencygroup_id
+        pgroup_name = self._get_pgroup_name_from_id(cg_id)
+        pgsnap_suffix = self._get_pgroup_snap_suffix(cgsnapshot)
         self._array.create_pgroup_snapshot(pgroup_name, suffix=pgsnap_suffix)
 
         snapshots = self.db.snapshot_get_all_for_cgsnapshot(
@@ -601,14 +390,13 @@ class PureISCSIDriver(san.SanISCSIDriver):
 
         model_update = {'status': 'available'}
 
-        LOG.debug("Leave PureISCSIDriver.create_cgsnapshot")
         return model_update, snapshots
 
+    @log_debug_trace
     def delete_cgsnapshot(self, context, cgsnapshot):
         """Deletes a cgsnapshot."""
-        LOG.debug("Enter PureISCSIDriver.delete_cgsnapshot")
 
-        pgsnap_name = _get_pgroup_snap_name(cgsnapshot)
+        pgsnap_name = self._get_pgroup_snap_name(cgsnapshot)
 
         try:
             # FlashArray.destroy_pgroup is also used for deleting
@@ -633,7 +421,6 @@ class PureISCSIDriver(san.SanISCSIDriver):
 
         model_update = {'status': cgsnapshot.status}
 
-        LOG.debug("Leave PureISCSIDriver.delete_cgsnapshot")
         return model_update, snapshots
 
     def _validate_manage_existing_ref(self, existing_ref):
@@ -645,7 +432,7 @@ class PureISCSIDriver(san.SanISCSIDriver):
         if "name" not in existing_ref or not existing_ref["name"]:
             raise exception.ManageExistingInvalidReference(
                 existing_ref=existing_ref,
-                reason=_("PureISCSIDriver manage_existing requires a 'name'"
+                reason=_("manage_existing requires a 'name'"
                          " key to identify an existing volume."))
 
         ref_vol_name = existing_ref['name']
@@ -666,12 +453,12 @@ class PureISCSIDriver(san.SanISCSIDriver):
             existing_ref=existing_ref,
             reason=_("Unable to find volume with name=%s") % ref_vol_name)
 
+    @log_debug_trace
     def manage_existing(self, volume, existing_ref):
         """Brings an existing backend storage object under Cinder management.
 
         We expect a volume name in the existing_ref that matches one in Purity.
         """
-        LOG.debug("Enter PureISCSIDriver.manage_existing.")
 
         self._validate_manage_existing_ref(existing_ref)
 
@@ -686,26 +473,25 @@ class PureISCSIDriver(san.SanISCSIDriver):
                          "volume connected to hosts. Please disconnect the "
                          "volume from existing hosts before importing."))
 
-        new_vol_name = _get_vol_name(volume)
+        new_vol_name = self._get_vol_name(volume)
         LOG.info(_LI("Renaming existing volume %(ref_name)s to %(new_name)s"),
                  {"ref_name": ref_vol_name, "new_name": new_vol_name})
         self._array.rename_volume(ref_vol_name, new_vol_name)
-        LOG.debug("Leave PureISCSIDriver.manage_existing.")
         return None
 
+    @log_debug_trace
     def manage_existing_get_size(self, volume, existing_ref):
         """Return size of volume to be managed by manage_existing.
 
         We expect a volume name in the existing_ref that matches one in Purity.
         """
-        LOG.debug("Enter PureISCSIDriver.manage_existing_get_size.")
 
         volume_info = self._validate_manage_existing_ref(existing_ref)
         size = math.ceil(float(volume_info["size"]) / units.Gi)
 
-        LOG.debug("Leave PureISCSIDriver.manage_existing_get_size.")
         return size
 
+    @log_debug_trace
     def unmanage(self, volume):
         """Removes the specified volume from Cinder management.
 
@@ -713,7 +499,7 @@ class PureISCSIDriver(san.SanISCSIDriver):
 
         The volume will be renamed with "-unmanaged" as a suffix
         """
-        vol_name = _get_vol_name(volume)
+        vol_name = self._get_vol_name(volume)
         unmanaged_vol_name = vol_name + "-unmanaged"
         LOG.info(_LI("Renaming existing volume %(ref_name)s to %(new_name)s"),
                  {"ref_name": vol_name, "new_name": unmanaged_vol_name})
@@ -726,3 +512,239 @@ class PureISCSIDriver(san.SanISCSIDriver):
                     ctxt.reraise = False
                     LOG.warning(_LW("Volume unmanage was unable to rename "
                                     "the volume, error message: %s"), err.text)
+
+    @staticmethod
+    def _get_vol_name(volume):
+        """Return the name of the volume Purity will use."""
+        return volume["name"] + "-cinder"
+
+    @staticmethod
+    def _get_snap_name(snapshot):
+        """Return the name of the snapshot that Purity will use."""
+        return "%s-cinder.%s" % (snapshot["volume_name"], snapshot["name"])
+
+    @staticmethod
+    def _get_pgroup_name_from_id(id):
+        return "consisgroup-%s-cinder" % id
+
+    @staticmethod
+    def _get_pgroup_snap_suffix(cgsnapshot):
+        return "cgsnapshot-%s-cinder" % cgsnapshot.id
+
+    @classmethod
+    def _get_pgroup_snap_name(cls, cgsnapshot):
+        """Return the name of the pgroup snapshot that Purity will use"""
+        cg_id = cgsnapshot.consistencygroup_id
+        return "%s.%s" % (cls._get_pgroup_name_from_id(cg_id),
+                          cls._get_pgroup_snap_suffix(cgsnapshot))
+
+    @classmethod
+    def _get_pgroup_vol_snap_name(cls, snapshot):
+        """Return the name of the snapshot that Purity will use."""
+        cg_id = snapshot.cgsnapshot.consistencygroup_id
+        cg_name = cls._get_pgroup_name_from_id(cg_id)
+        cgsnapshot_id = cls._get_pgroup_snap_suffix(snapshot.cgsnapshot)
+        volume_name = snapshot.volume_name
+        return "%s.%s.%s-cinder" % (cg_name, cgsnapshot_id, volume_name)
+
+    @staticmethod
+    def _generate_purity_host_name(name):
+        """Return a valid Purity host name based on the name passed in."""
+        if len(name) > 23:
+            name = name[0:23]
+        name = INVALID_CHARACTERS.sub("-", name)
+        name = name.lstrip("-")
+        return "{name}-{uuid}-cinder".format(name=name, uuid=uuid.uuid4().hex)
+
+    def _connect_host_to_vol(self, host_name, vol_name):
+        connection = None
+        try:
+            connection = self._array.connect_host(host_name, vol_name)
+        except purestorage.PureHTTPError as err:
+            with excutils.save_and_reraise_exception() as ctxt:
+                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.warning(_LW("Volume connection already exists with "
+                                    "message: %s"), err.text)
+                    # Get the info for the existing connection
+                    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
+                            break
+        if not connection:
+            raise exception.PureDriverException(
+                reason=_("Unable to connect or find connection to host"))
+
+        return connection
+
+
+class PureISCSIDriver(PureBaseVolumeDriver, san.SanISCSIDriver):
+
+    VERSION = "3.0.0"
+
+    def __init__(self, *args, **kwargs):
+        execute = kwargs.pop("execute", utils.execute)
+        super(PureISCSIDriver, self).__init__(execute=execute, *args, **kwargs)
+        self._storage_protocol = "iSCSI"
+        self._iscsi_port = None
+
+    def do_setup(self, context):
+        super(PureISCSIDriver, self).do_setup(context)
+        self._iscsi_port = self._choose_target_iscsi_port()
+
+    def _get_host(self, connector):
+        """Return dict describing existing Purity host object or None."""
+        hosts = self._array.list_hosts()
+        for host in hosts:
+            if connector["initiator"] in host["iqn"]:
+                return host
+        return None
+
+    @log_debug_trace
+    def initialize_connection(self, volume, connector, initiator_data=None):
+        """Allow connection to connector and return connection info."""
+        target_port = self._get_target_iscsi_port()
+        connection = self._connect(volume, connector, initiator_data)
+        properties = {
+            "driver_volume_type": "iscsi",
+            "data": {
+                "target_iqn": target_port["iqn"],
+                "target_portal": target_port["portal"],
+                "target_lun": connection["lun"],
+                "target_discovered": True,
+                "access_mode": "rw",
+            },
+        }
+
+        if self.configuration.use_chap_auth:
+            properties["data"]["auth_method"] = "CHAP"
+            properties["data"]["auth_username"] = connection["auth_username"]
+            properties["data"]["auth_password"] = connection["auth_password"]
+
+        initiator_update = connection.get("initiator_update", False)
+        if initiator_update:
+            properties["initiator_update"] = initiator_update
+
+        return properties
+
+    def _get_target_iscsi_port(self):
+        """Return dictionary describing iSCSI-enabled port on target array."""
+        try:
+            self._run_iscsiadm_bare(["-m", "discovery", "-t", "sendtargets",
+                                     "-p", self._iscsi_port["portal"]])
+        except processutils.ProcessExecutionError as err:
+            LOG.warning(_LW("iSCSI discovery of port %(port_name)s at "
+                            "%(port_portal)s failed with error: %(err_msg)s"),
+                        {"port_name": self._iscsi_port["name"],
+                         "port_portal": self._iscsi_port["portal"],
+                         "err_msg": err.stderr})
+            self._iscsi_port = self._choose_target_iscsi_port()
+        return self._iscsi_port
+
+    @utils.retry(exception.PureDriverException, retries=3)
+    def _choose_target_iscsi_port(self):
+        """Find a reachable iSCSI-enabled port on target array."""
+        ports = self._array.list_ports()
+        iscsi_ports = [port for port in ports if port["iqn"]]
+        for port in iscsi_ports:
+            try:
+                self._run_iscsiadm_bare(["-m", "discovery",
+                                         "-t", "sendtargets",
+                                         "-p", port["portal"]])
+            except processutils.ProcessExecutionError as err:
+                LOG.debug(("iSCSI discovery of port %(port_name)s at "
+                           "%(port_portal)s failed with error: %(err_msg)s"),
+                          {"port_name": port["name"],
+                           "port_portal": port["portal"],
+                           "err_msg": err.stderr})
+            else:
+                LOG.info(_LI("Using port %(name)s on the array at %(portal)s "
+                             "for iSCSI connectivity."),
+                         {"name": port["name"], "portal": port["portal"]})
+                return port
+        raise exception.PureDriverException(
+            reason=_("No reachable iSCSI-enabled ports on target array."))
+
+    @staticmethod
+    def _generate_chap_secret():
+        return volume_utils.generate_password()
+
+    @classmethod
+    def _get_chap_credentials(cls, host, data):
+        initiator_updates = None
+        username = host
+        password = None
+        if data:
+            for d in data:
+                if d["key"] == CHAP_SECRET_KEY:
+                    password = d["value"]
+                    break
+        if not password:
+            password = cls._generate_chap_secret()
+            initiator_updates = {
+                "set_values": {
+                    CHAP_SECRET_KEY: password
+                }
+            }
+        return username, password, initiator_updates
+
+    @utils.synchronized('PureISCSIDriver._connect', external=True)
+    def _connect(self, volume, connector, initiator_data):
+        """Connect the host and volume; return dict describing connection."""
+        iqn = connector["initiator"]
+
+        if self.configuration.use_chap_auth:
+            (chap_username, chap_password, initiator_update) = \
+                self._get_chap_credentials(connector['host'], initiator_data)
+
+        vol_name = self._get_vol_name(volume)
+        host = self._get_host(connector)
+
+        if host:
+            host_name = host["name"]
+            LOG.info(_LI("Re-using existing purity host %(host_name)r"),
+                     {"host_name": host_name})
+            if self.configuration.use_chap_auth:
+                if not GENERATED_NAME.match(host_name):
+                    LOG.error(_LE("Purity host %(host_name)s is not managed "
+                                  "by Cinder and can't have CHAP credentials "
+                                  "modified. Remove IQN %(iqn)s from the host "
+                                  "to resolve this issue."),
+                              {"host_name": host_name,
+                               "iqn": connector["initiator"]})
+                    raise exception.PureDriverException(
+                        reason=_("Unable to re-use a host that is not "
+                                 "managed by Cinder with use_chap_auth=True,"))
+                elif chap_username is None or chap_password is None:
+                    LOG.error(_LE("Purity host %(host_name)s is managed by "
+                                  "Cinder but CHAP credentials could not be "
+                                  "retrieved from the Cinder database."),
+                              {"host_name": host_name})
+                    raise exception.PureDriverException(
+                        reason=_("Unable to re-use host with unknown CHAP "
+                                 "credentials configured."))
+        else:
+            host_name = self._generate_purity_host_name(connector["host"])
+            LOG.info(_LI("Creating host object %(host_name)r with IQN:"
+                         " %(iqn)s."), {"host_name": host_name, "iqn": iqn})
+            self._array.create_host(host_name, iqnlist=[iqn])
+
+            if self.configuration.use_chap_auth:
+                self._array.set_host(host_name,
+                                     host_user=chap_username,
+                                     host_password=chap_password)
+
+        connection = self._connect_host_to_vol(host_name, vol_name)
+
+        if self.configuration.use_chap_auth:
+            connection["auth_username"] = chap_username
+            connection["auth_password"] = chap_password
+
+            if initiator_update:
+                connection["initiator_update"] = initiator_update
+
+        return connection