]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add Pure Storage FibreChannel driver
authorDaniel Wilson <daniel.wilson@purestorage.com>
Tue, 12 May 2015 17:47:35 +0000 (10:47 -0700)
committerDaniel Wilson <daniel.wilson@purestorage.com>
Thu, 4 Jun 2015 22:06:23 +0000 (15:06 -0700)
    This commit adds a FibreChannel driver for Pure Storage. The driver shares
    the majority of functionality with base driver and only has the code
    necessary to support connection to FibreChannel.

Implements: blueprint pure-fc-driver
Change-Id: Ia4ce76d6a2ec754951c13d3620f9f47d6920058b

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

index 72c7fc122c19a544c90a80207e3a96b57f36eea8..49101c3de70c00cf9ee87c66d8b81848adb10ae5 100644 (file)
@@ -39,13 +39,16 @@ patch_retry.stop()
 DRIVER_PATH = "cinder.volume.drivers.pure"
 BASE_DRIVER_OBJ = DRIVER_PATH + ".PureBaseVolumeDriver"
 ISCSI_DRIVER_OBJ = DRIVER_PATH + ".PureISCSIDriver"
+FC_DRIVER_OBJ = DRIVER_PATH + ".PureFCDriver"
 ARRAY_OBJ = DRIVER_PATH + ".FlashArray"
 
 TARGET = "pure-target"
 API_TOKEN = "12345678-abcd-1234-abcd-1234567890ab"
 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))]
+ISCSI_PORT_NAMES = ["ct0.eth2", "ct0.eth3", "ct1.eth2", "ct1.eth3"]
+FC_PORT_NAMES = ["ct0.fc2", "ct0.fc3", "ct1.fc2", "ct1.fc3"]
+ISCSI_IPS = ["10.0.0." + str(i + 1) for i in range(len(ISCSI_PORT_NAMES))]
+FC_WWNS = ["21000024ff59fe9" + str(i + 1) for i in range(len(FC_PORT_NAMES))]
 HOSTNAME = "computenode1"
 PURE_HOST_NAME = pure.PureBaseVolumeDriver._generate_purity_host_name(HOSTNAME)
 PURE_HOST = {
@@ -94,14 +97,36 @@ SNAPSHOT_WITH_CGROUP = SNAPSHOT.copy()
 SNAPSHOT_WITH_CGROUP['cgsnapshot_id'] = \
     "4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
 INITIATOR_IQN = "iqn.1993-08.org.debian:01:222"
-CONNECTOR = {"initiator": INITIATOR_IQN, "host": HOSTNAME}
+INITIATOR_WWN = "5001500150015081"
+ISCSI_CONNECTOR = {"initiator": INITIATOR_IQN, "host": HOSTNAME}
+FC_CONNECTOR = {"wwpns": {INITIATOR_WWN}, "host": HOSTNAME}
 TARGET_IQN = "iqn.2010-06.com.purestorage:flasharray.12345abc"
+TARGET_WWN = "21000024ff59fe94"
 TARGET_PORT = "3260"
+INITIATOR_TARGET_MAP =\
+    {
+        '5001500150015081': ['21000024ff59fe93',
+                             '21000024ff59fe92',
+                             '21000024ff59fe91',
+                             '21000024ff59fe94'],
+    }
+DEVICE_MAPPING =\
+    {
+        "fabric": {'initiator_port_wwn_list': {INITIATOR_WWN},
+                   'target_port_wwn_list': FC_WWNS
+                   },
+    }
+
 ISCSI_PORTS = [{"name": name,
                 "iqn": TARGET_IQN,
                 "portal": ip + ":" + TARGET_PORT,
                 "wwn": None,
-                } for name, ip in zip(PORT_NAMES, ISCSI_IPS)]
+                } for name, ip in zip(ISCSI_PORT_NAMES, ISCSI_IPS)]
+FC_PORTS = [{"name": name,
+             "iqn": None,
+             "portal": None,
+             "wwn": wwn,
+             } for name, wwn in zip(FC_PORT_NAMES, FC_WWNS)]
 NON_ISCSI_PORT = {
     "name": "ct0.fc1",
     "iqn": None,
@@ -127,7 +152,7 @@ SPACE_INFO_EMPTY = {
     "total": 0,
 }
 
-CONNECTION_INFO = {
+ISCSI_CONNECTION_INFO = {
     "driver_volume_type": "iscsi",
     "data": {
         "target_iqn": TARGET_IQN,
@@ -137,6 +162,16 @@ CONNECTION_INFO = {
         "access_mode": "rw",
     },
 }
+FC_CONNECTION_INFO = {
+    "driver_volume_type": "fibre_channel",
+    "data": {
+        "target_wwn": FC_WWNS,
+        "target_lun": 1,
+        "target_discovered": True,
+        "access_mode": "rw",
+        "initiator_target_map": INITIATOR_TARGET_MAP,
+    },
+}
 
 
 class FakePureStorageHTTPError(Exception):
@@ -386,7 +421,7 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase):
         vol_name = VOLUME["name"] + "-cinder"
         mock_host.return_value = {"name": "some-host"}
         # Branch with manually created host
-        self.driver.terminate_connection(VOLUME, CONNECTOR)
+        self.driver.terminate_connection(VOLUME, ISCSI_CONNECTOR)
         self.array.disconnect_host.assert_called_with("some-host", vol_name)
         self.assertFalse(self.array.list_host_connections.called)
         self.assertFalse(self.array.delete_host.called)
@@ -395,7 +430,7 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase):
         self.array.list_host_connections.return_value = []
         mock_host.return_value = PURE_HOST.copy()
         mock_host.return_value.update(hgroup="some-group")
-        self.driver.terminate_connection(VOLUME, CONNECTOR)
+        self.driver.terminate_connection(VOLUME, ISCSI_CONNECTOR)
         self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name)
         self.assertTrue(self.array.list_host_connections.called)
         self.assertTrue(self.array.delete_host.called)
@@ -404,7 +439,7 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase):
         self.array.list_host_connections.return_value = [
             {"lun": 2, "name": PURE_HOST_NAME, "vol": "some-vol"}]
         mock_host.return_value = PURE_HOST
-        self.driver.terminate_connection(VOLUME, CONNECTOR)
+        self.driver.terminate_connection(VOLUME, ISCSI_CONNECTOR)
         self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name)
         self.array.list_host_connections.assert_called_with(PURE_HOST_NAME,
                                                             private=True)
@@ -412,7 +447,7 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase):
         # Branch where host gets deleted
         self.array.reset_mock()
         self.array.list_host_connections.return_value = []
-        self.driver.terminate_connection(VOLUME, CONNECTOR)
+        self.driver.terminate_connection(VOLUME, ISCSI_CONNECTOR)
         self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name)
         self.array.list_host_connections.assert_called_with(PURE_HOST_NAME,
                                                             private=True)
@@ -421,7 +456,7 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase):
         self.array.reset_mock()
         self.array.disconnect_host.side_effect = \
             self.purestorage_module.PureHTTPError(code=400, text="reason")
-        self.driver.terminate_connection(VOLUME, CONNECTOR)
+        self.driver.terminate_connection(VOLUME, ISCSI_CONNECTOR)
         self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name)
         self.array.list_host_connections.assert_called_with(PURE_HOST_NAME,
                                                             private=True)
@@ -434,7 +469,9 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase):
                 text="Some other error"
             )
         self.assertRaises(self.purestorage_module.PureHTTPError,
-                          self.driver.terminate_connection, VOLUME, CONNECTOR)
+                          self.driver.terminate_connection,
+                          VOLUME,
+                          ISCSI_CONNECTOR)
         self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name)
         self.assertFalse(self.array.list_host_connections.called)
         self.assertFalse(self.array.delete_host.called)
@@ -1010,13 +1047,13 @@ class PureISCSIDriverTestCase(PureDriverTestCase):
         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)
+        real_result = self.driver._get_host(ISCSI_CONNECTOR)
+        self.assertIs(None, real_result)
         self.array.list_hosts.return_value.append(good_host)
-        real_result = self.driver._get_host(CONNECTOR)
+        real_result = self.driver._get_host(ISCSI_CONNECTOR)
         self.assertEqual(good_host, real_result)
         self.assert_error_propagates([self.array.list_hosts],
-                                     self.driver._get_host, CONNECTOR)
+                                     self.driver._get_host, ISCSI_CONNECTOR)
 
     @mock.patch(ISCSI_DRIVER_OBJ + "._connect")
     @mock.patch(ISCSI_DRIVER_OBJ + "._get_target_iscsi_port")
@@ -1026,21 +1063,22 @@ class PureISCSIDriverTestCase(PureDriverTestCase):
             "vol": VOLUME["name"] + "-cinder",
             "lun": 1,
         }
-        result = CONNECTION_INFO
-        real_result = self.driver.initialize_connection(VOLUME, CONNECTOR)
+        result = ISCSI_CONNECTION_INFO
+        real_result = self.driver.initialize_connection(VOLUME,
+                                                        ISCSI_CONNECTOR)
         self.assertDictMatch(result, real_result)
         mock_get_iscsi_port.assert_called_with()
-        mock_connection.assert_called_with(VOLUME, CONNECTOR, None)
+        mock_connection.assert_called_with(VOLUME, ISCSI_CONNECTOR, None)
         self.assert_error_propagates([mock_get_iscsi_port, mock_connection],
                                      self.driver.initialize_connection,
-                                     VOLUME, CONNECTOR)
+                                     VOLUME, ISCSI_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_username = ISCSI_CONNECTOR["host"]
         chap_password = "password"
         mock_get_iscsi_port.return_value = ISCSI_PORTS[0]
         initiator_update = [{"key": pure.CHAP_SECRET_KEY,
@@ -1051,7 +1089,7 @@ class PureISCSIDriverTestCase(PureDriverTestCase):
             "auth_username": chap_username,
             "auth_password": chap_password,
         }
-        result = CONNECTION_INFO.copy()
+        result = ISCSI_CONNECTION_INFO.copy()
         result["data"]["auth_method"] = auth_type
         result["data"]["auth_username"] = chap_username
         result["data"]["auth_password"] = chap_password
@@ -1060,21 +1098,21 @@ class PureISCSIDriverTestCase(PureDriverTestCase):
 
         # Branch where no credentials were generated
         real_result = self.driver.initialize_connection(VOLUME,
-                                                        CONNECTOR)
-        mock_connection.assert_called_with(VOLUME, CONNECTOR, None)
+                                                        ISCSI_CONNECTOR)
+        mock_connection.assert_called_with(VOLUME, ISCSI_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)
+                                                        ISCSI_CONNECTOR)
+        mock_connection.assert_called_with(VOLUME, ISCSI_CONNECTOR, None)
         self.assertDictMatch(result, real_result)
 
         self.assert_error_propagates([mock_get_iscsi_port, mock_connection],
                                      self.driver.initialize_connection,
-                                     VOLUME, CONNECTOR)
+                                     VOLUME, ISCSI_CONNECTOR)
 
     @mock.patch(ISCSI_DRIVER_OBJ + "._choose_target_iscsi_port")
     @mock.patch(ISCSI_DRIVER_OBJ + "._run_iscsiadm_bare")
@@ -1114,9 +1152,9 @@ class PureISCSIDriverTestCase(PureDriverTestCase):
         # 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)
+        real_result = self.driver._connect(VOLUME, ISCSI_CONNECTOR, None)
         self.assertEqual(result, real_result)
-        mock_host.assert_called_with(self.driver, CONNECTOR)
+        mock_host.assert_called_with(self.driver, ISCSI_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)
@@ -1124,8 +1162,8 @@ class PureISCSIDriverTestCase(PureDriverTestCase):
         # 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)
+        real_result = self.driver._connect(VOLUME, ISCSI_CONNECTOR, None)
+        mock_host.assert_called_with(self.driver, ISCSI_CONNECTOR)
         mock_generate.assert_called_with(HOSTNAME)
         self.array.create_host.assert_called_with(PURE_HOST_NAME,
                                                   iqnlist=[INITIATOR_IQN])
@@ -1136,16 +1174,16 @@ class PureISCSIDriverTestCase(PureDriverTestCase):
         self.assert_error_propagates(
             [mock_host, mock_generate, self.array.connect_host,
              self.array.create_host],
-            self.driver._connect, VOLUME, CONNECTOR, None)
+            self.driver._connect, VOLUME, ISCSI_CONNECTOR, None)
 
         self.mock_config.use_chap_auth = True
-        chap_user = CONNECTOR["host"]
+        chap_user = ISCSI_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)
+        self.driver._connect(VOLUME, ISCSI_CONNECTOR, initiator_data)
         result["auth_username"] = chap_user
         result["auth_password"] = chap_password
         self.assertDictMatch(result, real_result)
@@ -1155,7 +1193,7 @@ class PureISCSIDriverTestCase(PureDriverTestCase):
 
         # Branch where chap is used and credentials are generated
         mock_gen_secret.return_value = chap_password
-        self.driver._connect(VOLUME, CONNECTOR, None)
+        self.driver._connect(VOLUME, ISCSI_CONNECTOR, None)
         result["auth_username"] = chap_user
         result["auth_password"] = chap_password
         result["initiator_update"] = {
@@ -1179,7 +1217,7 @@ class PureISCSIDriverTestCase(PureDriverTestCase):
                 code=400,
                 text="Connection already exists"
             )
-        actual = self.driver._connect(VOLUME, CONNECTOR, None)
+        actual = self.driver._connect(VOLUME, ISCSI_CONNECTOR, None)
         self.assertEqual(expected, actual)
         self.assertTrue(self.array.connect_host.called)
         self.assertTrue(self.array.list_volume_private_connections)
@@ -1194,7 +1232,7 @@ class PureISCSIDriverTestCase(PureDriverTestCase):
                 text="Connection already exists"
             )
         self.assertRaises(exception.PureDriverException, self.driver._connect,
-                          VOLUME, CONNECTOR, None)
+                          VOLUME, ISCSI_CONNECTOR, None)
         self.assertTrue(self.array.connect_host.called)
         self.assertTrue(self.array.list_volume_private_connections)
 
@@ -1209,6 +1247,133 @@ class PureISCSIDriverTestCase(PureDriverTestCase):
                 text="Connection already exists"
             )
         self.assertRaises(self.purestorage_module.PureHTTPError,
-                          self.driver._connect, VOLUME, CONNECTOR, None)
+                          self.driver._connect, VOLUME, ISCSI_CONNECTOR, None)
+        self.assertTrue(self.array.connect_host.called)
+        self.assertTrue(self.array.list_volume_private_connections)
+
+
+class PureFCDriverTestCase(PureDriverTestCase):
+
+    def setUp(self):
+        super(PureFCDriverTestCase, self).setUp()
+        self.driver = pure.PureFCDriver(configuration=self.mock_config)
+        self.driver._array = self.array
+        self.driver._lookup_service = mock.Mock()
+
+    def test_do_setup(self):
+        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
+        )
+
+    def test_get_host(self):
+        good_host = PURE_HOST.copy()
+        good_host.update(wwn=["another-wrong-wwn", INITIATOR_WWN])
+        bad_host = {"name": "bad-host", "wwn": ["wrong-wwn"]}
+        self.array.list_hosts.return_value = [bad_host]
+        actual_result = self.driver._get_host(FC_CONNECTOR)
+        self.assertIs(None, actual_result)
+        self.array.list_hosts.return_value.append(good_host)
+        actual_result = self.driver._get_host(FC_CONNECTOR)
+        self.assertEqual(good_host, actual_result)
+        self.assert_error_propagates([self.array.list_hosts],
+                                     self.driver._get_host, FC_CONNECTOR)
+
+    @mock.patch(FC_DRIVER_OBJ + "._connect")
+    def test_initialize_connection(self, mock_connection):
+        lookup_service = self.driver._lookup_service
+        (lookup_service.get_device_mapping_from_network.
+         return_value) = DEVICE_MAPPING
+        mock_connection.return_value = {"vol": VOLUME["name"] + "-cinder",
+                                        "lun": 1,
+                                        }
+        self.array.list_ports.return_value = FC_PORTS
+        actual_result = self.driver.initialize_connection(VOLUME, FC_CONNECTOR)
+        self.assertDictMatch(FC_CONNECTION_INFO, actual_result)
+
+    @mock.patch(FC_DRIVER_OBJ + "._get_host", autospec=True)
+    @mock.patch(FC_DRIVER_OBJ + "._generate_purity_host_name", spec=True)
+    def test_connect(self, mock_generate, mock_host):
+        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, FC_CONNECTOR, None)
+        self.assertEqual(result, real_result)
+        mock_host.assert_called_with(self.driver, FC_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, FC_CONNECTOR, None)
+        mock_host.assert_called_with(self.driver, FC_CONNECTOR)
+        mock_generate.assert_called_with(HOSTNAME)
+        self.array.create_host.assert_called_with(PURE_HOST_NAME,
+                                                  wwnlist={INITIATOR_WWN})
+        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, FC_CONNECTOR, None)
+
+    @mock.patch(FC_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, FC_CONNECTOR, None)
+        self.assertEqual(expected, actual)
+        self.assertTrue(self.array.connect_host.called)
+        self.assertTrue(self.array.list_volume_private_connections)
+
+    @mock.patch(FC_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, FC_CONNECTOR, None)
+        self.assertTrue(self.array.connect_host.called)
+        self.assertTrue(self.array.list_volume_private_connections)
+
+    @mock.patch(FC_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, FC_CONNECTOR, None)
         self.assertTrue(self.array.connect_host.called)
         self.assertTrue(self.array.list_volume_private_connections)
index 59411afa5df2e2fa31d2e8ec713d6a9820133edc..9babd0cba789b61328d54480093912aee0510576 100644 (file)
@@ -31,8 +31,10 @@ from oslo_utils import units
 from cinder import exception
 from cinder.i18n import _, _LE, _LI, _LW
 from cinder import utils
+from cinder.volume import driver
 from cinder.volume.drivers.san import san
 from cinder.volume import utils as volume_utils
+from cinder.zonemanager import utils as fczm_utils
 
 try:
     import purestorage
@@ -211,19 +213,26 @@ class PureBaseVolumeDriver(san.SanDriver):
         """
         raise NotImplementedError
 
-    @log_debug_trace
-    def terminate_connection(self, volume, connector, **kwargs):
-        """Terminate connection."""
+    def _disconnect(self, volume, connector, **kwargs):
         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)
+            result = self._disconnect_host(host_name, vol_name)
         else:
             LOG.error(_LE("Unable to disconnect host from volume."))
+            result = False
+
+        return result
+
+    @log_debug_trace
+    def terminate_connection(self, volume, connector, **kwargs):
+        """Terminate connection."""
+        self._disconnect(volume, connector, **kwargs)
 
     @log_debug_trace
     def _disconnect_host(self, host_name, vol_name):
+        """Return value indicates if host was deleted on array or not"""
         try:
             self._array.disconnect_host(host_name, vol_name)
         except purestorage.PureHTTPError as err:
@@ -239,6 +248,9 @@ class PureBaseVolumeDriver(san.SanDriver):
             LOG.info(_LI("Deleting unneeded host %(host_name)r."),
                      {"host_name": host_name})
             self._array.delete_host(host_name)
+            return True
+
+        return False
 
     @log_debug_trace
     def get_volume_stats(self, refresh=False):
@@ -748,3 +760,113 @@ class PureISCSIDriver(PureBaseVolumeDriver, san.SanISCSIDriver):
                 connection["initiator_update"] = initiator_update
 
         return connection
+
+
+class PureFCDriver(PureBaseVolumeDriver, driver.FibreChannelDriver):
+
+    VERSION = "1.0.0"
+
+    def __init__(self, *args, **kwargs):
+        execute = kwargs.pop("execute", utils.execute)
+        super(PureFCDriver, self).__init__(execute=execute, *args, **kwargs)
+        self._storage_protocol = "FC"
+        self._lookup_service = fczm_utils.create_lookup_service()
+
+    def do_setup(self, context):
+        super(PureFCDriver, self).do_setup(context)
+
+    def _get_host(self, connector):
+        """Return dict describing existing Purity host object or None."""
+        hosts = self._array.list_hosts()
+        for host in hosts:
+            for wwn in connector["wwpns"]:
+                if wwn in str(host["wwn"]).lower():
+                    return host
+
+    def _get_array_wwns(self):
+        """Return list of wwns from the array"""
+        ports = self._array.list_ports()
+        return [port["wwn"] for port in ports if port["wwn"]]
+
+    @log_debug_trace
+    @fczm_utils.AddFCZone
+    def initialize_connection(self, volume, connector, initiator_data=None):
+        """Allow connection to connector and return connection info."""
+
+        connection = self._connect(volume, connector, initiator_data)
+        target_wwns = self._get_array_wwns()
+        init_targ_map = self._build_initiator_target_map(target_wwns,
+                                                         connector)
+        properties = {
+            "driver_volume_type": "fibre_channel",
+            "data": {
+                'target_discovered': True,
+                "target_lun": connection["lun"],
+                "target_wwn": target_wwns,
+                'access_mode': 'rw',
+                'initiator_target_map': init_targ_map,
+            }
+        }
+
+        return properties
+
+    @utils.synchronized('PureFCDriver._connect', external=True)
+    def _connect(self, volume, connector, initiator_data):
+        """Connect the host and volume; return dict describing connection."""
+        wwns = connector["wwpns"]
+
+        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})
+        else:
+            host_name = self._generate_purity_host_name(connector["host"])
+            LOG.info(_LI("Creating host object %(host_name)r with WWN:"
+                         " %(wwn)s."), {"host_name": host_name, "wwn": wwns})
+            self._array.create_host(host_name, wwnlist=wwns)
+
+        return self._connect_host_to_vol(host_name, vol_name)
+
+    def _build_initiator_target_map(self, target_wwns, connector):
+        """Build the target_wwns and the initiator target map."""
+        init_targ_map = {}
+
+        if self._lookup_service:
+            # use FC san lookup to determine which NSPs to use
+            # for the new VLUN.
+            dev_map = self._lookup_service.get_device_mapping_from_network(
+                connector['wwpns'],
+                target_wwns)
+
+            for fabric_name in dev_map:
+                fabric = dev_map[fabric_name]
+                for initiator in fabric['initiator_port_wwn_list']:
+                    if initiator not in init_targ_map:
+                        init_targ_map[initiator] = []
+                    init_targ_map[initiator] += fabric['target_port_wwn_list']
+                    init_targ_map[initiator] = list(set(
+                        init_targ_map[initiator]))
+        else:
+            init_targ_map = dict.fromkeys(connector["wwpns"], target_wwns)
+
+        return init_targ_map
+
+    @log_debug_trace
+    @fczm_utils.RemoveFCZone
+    def terminate_connection(self, volume, connector, **kwargs):
+        """Terminate connection."""
+        no_more_connections = self._disconnect(volume, connector, **kwargs)
+
+        properties = {"driver_volume_type": "fibre_channel", "data": {}}
+
+        if no_more_connections:
+            target_wwns = self._get_array_wwns()
+            init_targ_map = self._build_initiator_target_map(target_wwns,
+                                                             connector)
+            properties["data"] = {"target_wwn": target_wwns,
+                                  "initiator_target_map": init_targ_map}
+
+        return properties