]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
NetApp E-Series: Add Fibre Channel Support
authorAlex Meade <mr.alex.meade@gmail.com>
Fri, 1 May 2015 16:56:48 +0000 (12:56 -0400)
committerAlex Meade <mr.alex.meade@gmail.com>
Thu, 11 Jun 2015 14:31:43 +0000 (10:31 -0400)
The NetApp driver for E-series product lines currently support iSCSI. This
patch adds Fibre Channel support to the E-Series driver. This driver reuses
the same functionality as the E-Series iSCSI driver except for the
initialize_connection and terminate_connection driver methods which make
full use of Cinder's FibreChannel zone manager.

DocImpact
Implements blueprint: add-fibre-channel-support-to-netapp-eseries
Co-Authored-By: Alex Meade <mr.alex.meade@gmail.com>
Co-Authored-By: Yogesh Kshirsagar <ykshirsa@netapp.com>
Change-Id: I130f473aaa27ace4cd16a98f75c797aa967715b3

cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py
cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py
cinder/tests/unit/volume/drivers/netapp/eseries/test_fc_driver.py [new file with mode: 0644]
cinder/tests/unit/volume/drivers/netapp/eseries/test_host_mapper.py
cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py
cinder/volume/drivers/netapp/common.py
cinder/volume/drivers/netapp/eseries/client.py
cinder/volume/drivers/netapp/eseries/fc_driver.py [new file with mode: 0644]
cinder/volume/drivers/netapp/eseries/host_mapper.py
cinder/volume/drivers/netapp/eseries/library.py
test-requirements.txt

index 1f931e90657089996518e7407bfd16a2a3d111cb..548670d566640c8e7d615c39427e1c2e28f3e813 100644 (file)
@@ -1,4 +1,6 @@
-# Copyright (c) - 2015, Alex Meade.  All Rights Reserved.
+# Copyright (c) - 2015, Alex Meade
+# Copyright (c) - 2015, Yogesh Kshirsagar
+#  All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
 #    not use this file except in compliance with the License. You may obtain
@@ -80,6 +82,47 @@ VOLUME = {
 INITIATOR_NAME = 'iqn.1998-01.com.vmware:localhost-28a58148'
 INITIATOR_NAME_2 = 'iqn.1998-01.com.vmware:localhost-28a58149'
 INITIATOR_NAME_3 = 'iqn.1998-01.com.vmware:localhost-28a58150'
+WWPN = '20130080E5322230'
+WWPN_2 = '20230080E5322230'
+
+FC_TARGET_WWPNS = [
+    '500a098280feeba5',
+    '500a098290feeba5',
+    '500a098190feeba5',
+    '500a098180feeba5'
+]
+
+FC_I_T_MAP = {
+    '20230080E5322230': [
+        '500a098280feeba5',
+        '500a098290feeba5'
+    ],
+    '20130080E5322230': [
+        '500a098190feeba5',
+        '500a098180feeba5'
+    ]
+}
+
+FC_FABRIC_MAP = {
+    'fabricB': {
+        'target_port_wwn_list': [
+            '500a098190feeba5',
+            '500a098180feeba5'
+        ],
+        'initiator_port_wwn_list': [
+            '20130080E5322230'
+        ]
+    },
+    'fabricA': {
+        'target_port_wwn_list': [
+            '500a098290feeba5',
+            '500a098280feeba5'
+        ],
+        'initiator_port_wwn_list': [
+            '20230080E5322230'
+        ]
+    }
+}
 
 HOST = {
     'isSAControlled': False,
@@ -249,6 +292,102 @@ HARDWARE_INVENTORY = {
             'interfaceRef': '2202040000000000000000000000000000000000',
             'iqn': 'iqn.1992-01.com.lsi:2365.60080e500023c73400000000515af323'
         }
+    ],
+    'fibrePorts': [
+        {
+            "channel": 1,
+            "loopID": 126,
+            "speed": 800,
+            "hardAddress": 6,
+            "nodeName": "20020080E5322230",
+            "portName": "20130080E5322230",
+            "portId": "011700",
+            "topology": "fabric",
+            "part": "PM8032          ",
+            "revision": 8,
+            "chanMiswire": False,
+            "esmMiswire": False,
+            "linkStatus": "up",
+            "isDegraded": False,
+            "speedControl": "auto",
+            "maxSpeed": 800,
+            "speedNegError": False,
+            "reserved1": "000000000000000000000000",
+            "reserved2": "",
+            "ddsChannelState": 0,
+            "ddsStateReason": 0,
+            "ddsStateWho": 0,
+            "isLocal": True,
+            "channelPorts": [],
+            "currentInterfaceSpeed": "speed8gig",
+            "maximumInterfaceSpeed": "speed8gig",
+            "interfaceRef": "2202020000000000000000000000000000000000",
+            "physicalLocation": {
+                "trayRef": "0000000000000000000000000000000000000000",
+                "slot": 0,
+                "locationParent": {
+                    "refType": "generic",
+                    "controllerRef": None,
+                    "symbolRef": "0000000000000000000000000000000000000000",
+                    "typedReference": None
+                },
+                "locationPosition": 0
+            },
+            "isTrunkCapable": False,
+            "trunkMiswire": False,
+            "protectionInformationCapable": True,
+            "controllerId": "070000000000000000000002",
+            "interfaceId": "2202020000000000000000000000000000000000",
+            "addressId": "20130080E5322230",
+            "niceAddressId": "20:13:00:80:E5:32:22:30"
+        },
+        {
+            "channel": 2,
+            "loopID": 126,
+            "speed": 800,
+            "hardAddress": 7,
+            "nodeName": "20020080E5322230",
+            "portName": "20230080E5322230",
+            "portId": "011700",
+            "topology": "fabric",
+            "part": "PM8032          ",
+            "revision": 8,
+            "chanMiswire": False,
+            "esmMiswire": False,
+            "linkStatus": "up",
+            "isDegraded": False,
+            "speedControl": "auto",
+            "maxSpeed": 800,
+            "speedNegError": False,
+            "reserved1": "000000000000000000000000",
+            "reserved2": "",
+            "ddsChannelState": 0,
+            "ddsStateReason": 0,
+            "ddsStateWho": 0,
+            "isLocal": True,
+            "channelPorts": [],
+            "currentInterfaceSpeed": "speed8gig",
+            "maximumInterfaceSpeed": "speed8gig",
+            "interfaceRef": "2202030000000000000000000000000000000000",
+            "physicalLocation": {
+                "trayRef": "0000000000000000000000000000000000000000",
+                "slot": 0,
+                "locationParent": {
+                    "refType": "generic",
+                    "controllerRef": None,
+                    "symbolRef": "0000000000000000000000000000000000000000",
+                    "typedReference": None
+                },
+                "locationPosition": 0
+            },
+            "isTrunkCapable": False,
+            "trunkMiswire": False,
+            "protectionInformationCapable": True,
+            "controllerId": "070000000000000000000002",
+            "interfaceId": "2202030000000000000000000000000000000000",
+            "addressId": "20230080E5322230",
+            "niceAddressId": "20:23:00:80:E5:32:22:30"
+        },
     ]
 }
 
@@ -375,7 +514,7 @@ class FakeEseriesClient(object):
     def set_host_group_for_host(self, *args, **kwargs):
         pass
 
-    def create_host_with_port(self, *args, **kwargs):
+    def create_host_with_ports(self, *args, **kwargs):
         return HOST
 
     def list_hosts(self):
@@ -390,6 +529,15 @@ class FakeEseriesClient(object):
     def get_volume_mappings(self):
         return [VOLUME_MAPPING]
 
+    def get_volume_mappings_for_volume(self, volume):
+        return [VOLUME_MAPPING]
+
+    def get_volume_mappings_for_host(self, host_ref):
+        return [VOLUME_MAPPING]
+
+    def get_volume_mappings_for_host_group(self, hg_ref):
+        return [VOLUME_MAPPING]
+
     def delete_volume_mapping(self):
         return
 
@@ -444,3 +592,6 @@ class FakeEseriesClient(object):
 
     def delete_snapshot_volume(self, *args, **kwargs):
         pass
+
+    def list_target_wwpns(self, *args, **kwargs):
+        return [WWPN_2]
index 8aef18cf93e5f128cd7a04dfb598d43895231692..537219b7bdafde7c0d6dd1095f936646c5628de6 100644 (file)
@@ -1,4 +1,5 @@
 # Copyright (c) 2014 Alex Meade
+# Copyright (c) 2015 Yogesh Kshirsagar
 # All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import copy
+
 import mock
 
 from cinder import test
+from cinder.tests.unit.volume.drivers.netapp.eseries import fakes as \
+    eseries_fake
 from cinder.volume.drivers.netapp.eseries import client
 
 
@@ -44,3 +49,142 @@ class NetAppEseriesClientDriverTestCase(test.TestCase):
         for call in self.mock_log.debug.mock_calls:
             __, args, __ = call
             self.assertNotIn(self.fake_password, args[0])
+
+    def test_list_target_wwpns(self):
+        fake_hardware_inventory = copy.deepcopy(
+            eseries_fake.HARDWARE_INVENTORY)
+
+        mock_hardware_inventory = mock.Mock(
+            return_value=fake_hardware_inventory)
+        self.mock_object(self.my_client, 'list_hardware_inventory',
+                         mock_hardware_inventory)
+        expected_wwpns = [eseries_fake.WWPN, eseries_fake.WWPN_2]
+
+        actual_wwpns = self.my_client.list_target_wwpns()
+
+        self.assertEqual(expected_wwpns, actual_wwpns)
+
+    def test_list_target_wwpns_single_wwpn(self):
+        fake_hardware_inventory = copy.deepcopy(
+            eseries_fake.HARDWARE_INVENTORY)
+
+        fake_hardware_inventory['fibrePorts'] = [
+            fake_hardware_inventory['fibrePorts'][0]
+        ]
+        mock_hardware_inventory = mock.Mock(
+            return_value=fake_hardware_inventory)
+        self.mock_object(self.my_client, 'list_hardware_inventory',
+                         mock_hardware_inventory)
+        expected_wwpns = [eseries_fake.WWPN]
+
+        actual_wwpns = self.my_client.list_target_wwpns()
+
+        self.assertEqual(expected_wwpns, actual_wwpns)
+
+    def test_list_target_wwpns_no_wwpn(self):
+        fake_hardware_inventory = copy.deepcopy(
+            eseries_fake.HARDWARE_INVENTORY)
+
+        fake_hardware_inventory['fibrePorts'] = []
+        mock_hardware_inventory = mock.Mock(
+            return_value=fake_hardware_inventory)
+        self.mock_object(self.my_client, 'list_hardware_inventory',
+                         mock_hardware_inventory)
+        expected_wwpns = []
+
+        actual_wwpns = self.my_client.list_target_wwpns()
+
+        self.assertEqual(expected_wwpns, actual_wwpns)
+
+    def test_create_host_from_ports_fc(self):
+        label = 'fake_host'
+        host_type = 'linux'
+        port_type = 'fc'
+        port_ids = [eseries_fake.WWPN, eseries_fake.WWPN_2]
+        expected_ports = [
+            {'type': port_type, 'port': eseries_fake.WWPN, 'label': mock.ANY},
+            {'type': port_type, 'port': eseries_fake.WWPN_2,
+             'label': mock.ANY}]
+        mock_create_host = self.mock_object(self.my_client, 'create_host')
+
+        self.my_client.create_host_with_ports(label, host_type, port_ids,
+                                              port_type)
+
+        mock_create_host.assert_called_once_with(label, host_type,
+                                                 expected_ports, None)
+
+    def test_host_from_ports_with_no_ports_provided_fc(self):
+        label = 'fake_host'
+        host_type = 'linux'
+        port_type = 'fc'
+        port_ids = []
+        expected_ports = []
+        mock_create_host = self.mock_object(self.my_client, 'create_host')
+
+        self.my_client.create_host_with_ports(label, host_type, port_ids,
+                                              port_type)
+
+        mock_create_host.assert_called_once_with(label, host_type,
+                                                 expected_ports, None)
+
+    def test_create_host_from_ports_iscsi(self):
+        label = 'fake_host'
+        host_type = 'linux'
+        port_type = 'iscsi'
+        port_ids = [eseries_fake.INITIATOR_NAME,
+                    eseries_fake.INITIATOR_NAME_2]
+        expected_ports = [
+            {'type': port_type, 'port': eseries_fake.INITIATOR_NAME,
+             'label': mock.ANY},
+            {'type': port_type, 'port': eseries_fake.INITIATOR_NAME_2,
+             'label': mock.ANY}]
+        mock_create_host = self.mock_object(self.my_client, 'create_host')
+
+        self.my_client.create_host_with_ports(label, host_type, port_ids,
+                                              port_type)
+
+        mock_create_host.assert_called_once_with(label, host_type,
+                                                 expected_ports, None)
+
+    def test_get_volume_mappings_for_volume(self):
+        volume_mapping_1 = copy.deepcopy(eseries_fake.VOLUME_MAPPING)
+        volume_mapping_2 = copy.deepcopy(eseries_fake.VOLUME_MAPPING)
+        volume_mapping_2['volumeRef'] = '2'
+        self.mock_object(self.my_client, 'get_volume_mappings',
+                         mock.Mock(return_value=[volume_mapping_1,
+                                                 volume_mapping_2]))
+
+        mappings = self.my_client.get_volume_mappings_for_volume(
+            eseries_fake.VOLUME)
+
+        self.assertEqual([volume_mapping_1], mappings)
+
+    def test_get_volume_mappings_for_host(self):
+        volume_mapping_1 = copy.deepcopy(
+            eseries_fake.VOLUME_MAPPING)
+        volume_mapping_2 = copy.deepcopy(eseries_fake.VOLUME_MAPPING)
+        volume_mapping_2['volumeRef'] = '2'
+        volume_mapping_2['mapRef'] = 'hostRef'
+        self.mock_object(self.my_client, 'get_volume_mappings',
+                         mock.Mock(return_value=[volume_mapping_1,
+                                                 volume_mapping_2]))
+
+        mappings = self.my_client.get_volume_mappings_for_host(
+            'hostRef')
+
+        self.assertEqual([volume_mapping_2], mappings)
+
+    def test_get_volume_mappings_for_hostgroup(self):
+        volume_mapping_1 = copy.deepcopy(
+            eseries_fake.VOLUME_MAPPING)
+        volume_mapping_2 = copy.deepcopy(eseries_fake.VOLUME_MAPPING)
+        volume_mapping_2['volumeRef'] = '2'
+        volume_mapping_2['mapRef'] = 'hostGroupRef'
+        self.mock_object(self.my_client, 'get_volume_mappings',
+                         mock.Mock(return_value=[volume_mapping_1,
+                                                 volume_mapping_2]))
+
+        mappings = self.my_client.get_volume_mappings_for_host_group(
+            'hostGroupRef')
+
+        self.assertEqual([volume_mapping_2], mappings)
diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_fc_driver.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_fc_driver.py
new file mode 100644 (file)
index 0000000..9163eed
--- /dev/null
@@ -0,0 +1,30 @@
+# Copyright (c) 2015 Alex Meade
+# Copyright (c) 2015 Yogesh Kshirsagar
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import mock
+
+from cinder import test
+import cinder.volume.drivers.netapp.eseries.fc_driver as fc
+from cinder.volume.drivers.netapp import utils as na_utils
+
+
+class NetAppESeriesFibreChannelDriverTestCase(test.TestCase):
+
+    @mock.patch.object(na_utils, 'validate_instantiation')
+    def test_instantiation(self, mock_validate_instantiation):
+        fc.NetAppEseriesFibreChannelDriver(configuration=mock.Mock())
+
+        self.assertTrue(mock_validate_instantiation.called)
index 7561ff58900155d045fd7e1b98e0a2d25b77eda0..d9ae779b4b73f573e38be0b1716a0c1184f0ac06 100644 (file)
@@ -64,18 +64,6 @@ class NetAppEseriesHostMapperTestCase(test.TestCase):
 
         self.client = eseries_fakes.FakeEseriesClient()
 
-    def test_get_host_mapping_for_vol_frm_array(self):
-        volume_mapping_1 = copy.deepcopy(eseries_fakes.VOLUME_MAPPING)
-        volume_mapping_2 = copy.deepcopy(eseries_fakes.VOLUME_MAPPING)
-        volume_mapping_2['volumeRef'] = '2'
-        self.mock_object(self.client, 'get_volume_mappings',
-                         mock.Mock(return_value=[volume_mapping_1,
-                                                 volume_mapping_2]))
-        mappings = host_mapper.get_host_mapping_for_vol_frm_array(
-            self.client, eseries_fakes.VOLUME)
-
-        self.assertEqual([volume_mapping_1], mappings)
-
     def test_unmap_volume_from_host_volume_mapped_to_host(self):
         fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME)
         fake_eseries_volume['listOfMappings'] = [
@@ -603,7 +591,7 @@ class NetAppEseriesHostMapperTestCase(test.TestCase):
 
     def test_host_full(self):
         fake_host = copy.deepcopy(eseries_fakes.HOST)
-        self.mock_object(host_mapper, '_get_vol_mapping_for_host_frm_array',
+        self.mock_object(self.client, 'get_volume_mappings_for_host',
                          mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
         self.assertTrue(host_mapper._is_host_full(self.client, fake_host))
 
@@ -611,7 +599,8 @@ class NetAppEseriesHostMapperTestCase(test.TestCase):
         fake_host = copy.deepcopy(eseries_fakes.HOST)
         with mock.patch('random.sample') as mock_random:
             mock_random.return_value = [3]
-            lun = host_mapper._get_free_lun(self.client, fake_host, False)
+            lun = host_mapper._get_free_lun(self.client, fake_host, False,
+                                            [])
         self.assertEqual(3, lun)
 
     def test_get_free_lun_host_full(self):
@@ -621,34 +610,31 @@ class NetAppEseriesHostMapperTestCase(test.TestCase):
         self.assertRaises(
             exception.NetAppDriverException,
             host_mapper._get_free_lun,
-            self.client, fake_host, False)
+            self.client, fake_host, False, FAKE_USED_UP_MAPPINGS)
 
     def test_get_free_lun_no_unused_luns(self):
         fake_host = copy.deepcopy(eseries_fakes.HOST)
-        self.mock_object(self.client, 'get_volume_mappings',
-                         mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
-        lun = host_mapper._get_free_lun(self.client, fake_host, False)
+        lun = host_mapper._get_free_lun(self.client, fake_host, False,
+                                        FAKE_USED_UP_MAPPINGS)
         self.assertEqual(255, lun)
 
     def test_get_free_lun_no_unused_luns_host_not_full(self):
         fake_host = copy.deepcopy(eseries_fakes.HOST)
-        self.mock_object(self.client, 'get_volume_mappings',
-                         mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
         self.mock_object(host_mapper, '_is_host_full',
                          mock.Mock(return_value=False))
-        lun = host_mapper._get_free_lun(self.client, fake_host, False)
+        lun = host_mapper._get_free_lun(self.client, fake_host, False,
+                                        FAKE_USED_UP_MAPPINGS)
         self.assertEqual(255, lun)
 
     def test_get_free_lun_no_lun_available(self):
         fake_host = copy.deepcopy(eseries_fakes.HOST_3)
-        self.mock_object(self.client, 'get_volume_mappings',
-                         mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
-        self.mock_object(host_mapper, '_get_vol_mapping_for_host_frm_array',
+        self.mock_object(self.client, 'get_volume_mappings_for_host',
                          mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
 
         self.assertRaises(exception.NetAppDriverException,
                           host_mapper._get_free_lun,
-                          self.client, fake_host, False)
+                          self.client, fake_host, False,
+                          FAKE_USED_UP_MAPPINGS)
 
     def test_get_free_lun_multiattach_enabled_no_unused_ids(self):
         fake_host = copy.deepcopy(eseries_fakes.HOST_3)
@@ -657,7 +643,8 @@ class NetAppEseriesHostMapperTestCase(test.TestCase):
 
         self.assertRaises(exception.NetAppDriverException,
                           host_mapper._get_free_lun,
-                          self.client, fake_host, True)
+                          self.client, fake_host, True,
+                          FAKE_USED_UP_MAPPINGS)
 
     def test_get_lun_by_mapping(self):
         used_luns = host_mapper._get_used_lun_ids_for_mappings(FAKE_MAPPINGS)
@@ -674,7 +661,7 @@ class NetAppEseriesHostMapperTestCase(test.TestCase):
 
     def test_no_lun_id_available_on_host(self):
         fake_host = copy.deepcopy(eseries_fakes.HOST_3)
-        self.mock_object(host_mapper, '_get_vol_mapping_for_host_frm_array',
+        self.mock_object(self.client, 'get_volume_mappings_for_host',
                          mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
 
         self.assertFalse(host_mapper._is_lun_id_available_on_host(
index eb2652e607c7cac6ebb9a11e7e8c4aade77cc919..12cd23509fbb81d54645cd822ab32b02845e65a1 100644 (file)
@@ -1,6 +1,7 @@
-# Copyright (c) 2014 Andrew Kerr.  All rights reserved.
-# Copyright (c) 2015 Alex Meade.  All rights reserved.
-# Copyright (c) 2015 Rushil Chugh.  All rights reserved.
+# Copyright (c) 2014 Andrew Kerr
+# Copyright (c) 2015 Alex Meade
+# Copyright (c) 2015 Rushil Chugh
+# Copyright (c) 2015 Yogesh Kshirsagar
 # All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
 #    under the License.
 
 import copy
+import ddt
 
 import mock
-import six
 
 from cinder import exception
 from cinder import test
 from cinder.tests.unit import fake_snapshot
 from cinder.tests.unit.volume.drivers.netapp.eseries import fakes as \
-    eseries_fakes
+    eseries_fake
 from cinder.volume.drivers.netapp.eseries import client as es_client
+from cinder.volume.drivers.netapp.eseries import exception as eseries_exc
 from cinder.volume.drivers.netapp.eseries import host_mapper
 from cinder.volume.drivers.netapp.eseries import library
 from cinder.volume.drivers.netapp.eseries import utils
 from cinder.volume.drivers.netapp import utils as na_utils
+from cinder.zonemanager import utils as fczm_utils
 
 
 def get_fake_volume():
@@ -45,22 +48,23 @@ def get_fake_volume():
     }
 
 
+@ddt.ddt
 class NetAppEseriesLibraryTestCase(test.TestCase):
     def setUp(self):
         super(NetAppEseriesLibraryTestCase, self).setUp()
 
         kwargs = {'configuration':
-                  eseries_fakes.create_configuration_eseries()}
+                  eseries_fake.create_configuration_eseries()}
 
         self.library = library.NetAppESeriesLibrary('FAKE', **kwargs)
-        self.library._client = eseries_fakes.FakeEseriesClient()
+        self.library._client = eseries_fake.FakeEseriesClient()
         self.library.check_for_setup_error()
 
     def test_do_setup(self):
         self.mock_object(self.library,
                          '_check_mode_get_or_register_storage_system')
         self.mock_object(es_client, 'RestClient',
-                         eseries_fakes.FakeEseriesClient)
+                         eseries_fake.FakeEseriesClient)
         mock_check_flags = self.mock_object(na_utils, 'check_flags')
         self.library.do_setup(mock.Mock())
 
@@ -207,7 +211,7 @@ class NetAppEseriesLibraryTestCase(test.TestCase):
                          ssc_stats)
 
     def test_terminate_connection_iscsi_no_hosts(self):
-        connector = {'initiator': eseries_fakes.INITIATOR_NAME}
+        connector = {'initiator': eseries_fake.INITIATOR_NAME}
 
         self.mock_object(self.library._client, 'list_hosts',
                          mock.Mock(return_value=[]))
@@ -218,18 +222,17 @@ class NetAppEseriesLibraryTestCase(test.TestCase):
                           connector)
 
     def test_terminate_connection_iscsi_volume_not_mapped(self):
-        connector = {'initiator': eseries_fakes.INITIATOR_NAME}
-        err = self.assertRaises(exception.NetAppDriverException,
-                                self.library.terminate_connection_iscsi,
-                                get_fake_volume(),
-                                connector)
-        self.assertIn("not currently mapped to host", six.text_type(err))
+        connector = {'initiator': eseries_fake.INITIATOR_NAME}
+        self.assertRaises(eseries_exc.VolumeNotMapped,
+                          self.library.terminate_connection_iscsi,
+                          get_fake_volume(),
+                          connector)
 
     def test_terminate_connection_iscsi_volume_mapped(self):
-        connector = {'initiator': eseries_fakes.INITIATOR_NAME}
-        fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME)
+        connector = {'initiator': eseries_fake.INITIATOR_NAME}
+        fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
         fake_eseries_volume['listOfMappings'] = [
-            eseries_fakes.VOLUME_MAPPING
+            eseries_fake.VOLUME_MAPPING
         ]
         self.mock_object(self.library._client, 'list_volumes',
                          mock.Mock(return_value=[fake_eseries_volume]))
@@ -241,54 +244,58 @@ class NetAppEseriesLibraryTestCase(test.TestCase):
 
     def test_terminate_connection_iscsi_not_mapped_initiator_does_not_exist(
             self):
-        connector = {'initiator': eseries_fakes.INITIATOR_NAME}
+        connector = {'initiator': eseries_fake.INITIATOR_NAME}
         self.mock_object(self.library._client, 'list_hosts',
-                         mock.Mock(return_value=[eseries_fakes.HOST_2]))
+                         mock.Mock(return_value=[eseries_fake.HOST_2]))
         self.assertRaises(exception.NotFound,
                           self.library.terminate_connection_iscsi,
                           get_fake_volume(),
                           connector)
 
     def test_initialize_connection_iscsi_volume_not_mapped(self):
-        connector = {'initiator': eseries_fakes.INITIATOR_NAME}
-        self.mock_object(self.library._client, 'get_volume_mappings',
+        connector = {'initiator': eseries_fake.INITIATOR_NAME}
+        self.mock_object(self.library._client,
+                         'get_volume_mappings_for_volume',
                          mock.Mock(return_value=[]))
         self.mock_object(host_mapper, 'map_volume_to_single_host',
                          mock.Mock(
-                             return_value=eseries_fakes.VOLUME_MAPPING))
+                             return_value=eseries_fake.VOLUME_MAPPING))
 
         self.library.initialize_connection_iscsi(get_fake_volume(), connector)
 
-        self.assertTrue(self.library._client.get_volume_mappings.called)
+        self.assertTrue(
+            self.library._client.get_volume_mappings_for_volume.called)
         self.assertTrue(host_mapper.map_volume_to_single_host.called)
 
     def test_initialize_connection_iscsi_volume_not_mapped_host_does_not_exist(
             self):
-        connector = {'initiator': eseries_fakes.INITIATOR_NAME}
-        self.mock_object(self.library._client, 'get_volume_mappings',
+        connector = {'initiator': eseries_fake.INITIATOR_NAME}
+        self.mock_object(self.library._client,
+                         'get_volume_mappings_for_volume',
                          mock.Mock(return_value=[]))
         self.mock_object(self.library._client, 'list_hosts',
                          mock.Mock(return_value=[]))
-        self.mock_object(self.library._client, 'create_host_with_port',
-                         mock.Mock(return_value=eseries_fakes.HOST))
+        self.mock_object(self.library._client, 'create_host_with_ports',
+                         mock.Mock(return_value=eseries_fake.HOST))
         self.mock_object(host_mapper, 'map_volume_to_single_host',
                          mock.Mock(
-                             return_value=eseries_fakes.VOLUME_MAPPING))
+                             return_value=eseries_fake.VOLUME_MAPPING))
 
         self.library.initialize_connection_iscsi(get_fake_volume(), connector)
 
-        self.assertTrue(self.library._client.get_volume_mappings.called)
+        self.assertTrue(
+            self.library._client.get_volume_mappings_for_volume.called)
         self.assertTrue(self.library._client.list_hosts.called)
-        self.assertTrue(self.library._client.create_host_with_port.called)
+        self.assertTrue(self.library._client.create_host_with_ports.called)
         self.assertTrue(host_mapper.map_volume_to_single_host.called)
 
     def test_initialize_connection_iscsi_volume_already_mapped_to_target_host(
             self):
         """Should be a no-op"""
-        connector = {'initiator': eseries_fakes.INITIATOR_NAME}
+        connector = {'initiator': eseries_fake.INITIATOR_NAME}
         self.mock_object(host_mapper, 'map_volume_to_single_host',
                          mock.Mock(
-                             return_value=eseries_fakes.VOLUME_MAPPING))
+                             return_value=eseries_fake.VOLUME_MAPPING))
 
         self.library.initialize_connection_iscsi(get_fake_volume(), connector)
 
@@ -296,10 +303,10 @@ class NetAppEseriesLibraryTestCase(test.TestCase):
 
     def test_initialize_connection_iscsi_volume_mapped_to_another_host(self):
         """Should raise error saying multiattach not enabled"""
-        connector = {'initiator': eseries_fakes.INITIATOR_NAME}
+        connector = {'initiator': eseries_fake.INITIATOR_NAME}
         fake_mapping_to_other_host = copy.deepcopy(
-            eseries_fakes.VOLUME_MAPPING)
-        fake_mapping_to_other_host['mapRef'] = eseries_fakes.HOST_2[
+            eseries_fake.VOLUME_MAPPING)
+        fake_mapping_to_other_host['mapRef'] = eseries_fake.HOST_2[
             'hostRef']
         self.mock_object(host_mapper, 'map_volume_to_single_host',
                          mock.Mock(
@@ -311,6 +318,297 @@ class NetAppEseriesLibraryTestCase(test.TestCase):
 
         self.assertTrue(host_mapper.map_volume_to_single_host.called)
 
+    @ddt.data(eseries_fake.WWPN,
+              fczm_utils.get_formatted_wwn(eseries_fake.WWPN))
+    def test_get_host_with_matching_port_wwpn(self, port_id):
+        port_ids = [port_id]
+        host = copy.deepcopy(eseries_fake.HOST)
+        host.update(
+            {
+                'hostSidePorts': [{'label': 'NewStore', 'type': 'fc',
+                                   'address': eseries_fake.WWPN}]
+            }
+        )
+        host_2 = copy.deepcopy(eseries_fake.HOST_2)
+        host_2.update(
+            {
+                'hostSidePorts': [{'label': 'NewStore', 'type': 'fc',
+                                   'address': eseries_fake.WWPN_2}]
+            }
+        )
+        host_list = [host, host_2]
+        self.mock_object(self.library._client,
+                         'list_hosts',
+                         mock.Mock(return_value=host_list))
+
+        actual_host = self.library._get_host_with_matching_port(
+            port_ids)
+
+        self.assertEqual(host, actual_host)
+
+    def test_get_host_with_matching_port_iqn(self):
+        port_ids = [eseries_fake.INITIATOR_NAME]
+        host = copy.deepcopy(eseries_fake.HOST)
+        host.update(
+            {
+                'hostSidePorts': [{'label': 'NewStore', 'type': 'iscsi',
+                                   'address': eseries_fake.INITIATOR_NAME}]
+            }
+        )
+        host_2 = copy.deepcopy(eseries_fake.HOST_2)
+        host_2.update(
+            {
+                'hostSidePorts': [{'label': 'NewStore', 'type': 'iscsi',
+                                   'address': eseries_fake.INITIATOR_NAME_2}]
+            }
+        )
+        host_list = [host, host_2]
+        self.mock_object(self.library._client,
+                         'list_hosts',
+                         mock.Mock(return_value=host_list))
+
+        actual_host = self.library._get_host_with_matching_port(
+            port_ids)
+
+        self.assertEqual(host, actual_host)
+
+    def test_terminate_connection_fc_no_hosts(self):
+        connector = {'wwpns': [eseries_fake.WWPN]}
+
+        self.mock_object(self.library._client, 'list_hosts',
+                         mock.Mock(return_value=[]))
+
+        self.assertRaises(exception.NotFound,
+                          self.library.terminate_connection_fc,
+                          get_fake_volume(),
+                          connector)
+
+    def test_terminate_connection_fc_volume_not_mapped(self):
+        connector = {'wwpns': [eseries_fake.WWPN]}
+        fake_host = copy.deepcopy(eseries_fake.HOST)
+        fake_host['hostSidePorts'] = [{
+            'label': 'NewStore',
+            'type': 'fc',
+            'address': eseries_fake.WWPN
+        }]
+
+        self.mock_object(self.library._client, 'list_hosts',
+                         mock.Mock(return_value=[fake_host]))
+
+        self.assertRaises(eseries_exc.VolumeNotMapped,
+                          self.library.terminate_connection_fc,
+                          get_fake_volume(),
+                          connector)
+
+    def test_terminate_connection_fc_volume_mapped(self):
+        connector = {'wwpns': [eseries_fake.WWPN]}
+        fake_host = copy.deepcopy(eseries_fake.HOST)
+        fake_host['hostSidePorts'] = [{
+            'label': 'NewStore',
+            'type': 'fc',
+            'address': eseries_fake.WWPN
+        }]
+        fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
+        fake_eseries_volume['listOfMappings'] = [
+            copy.deepcopy(eseries_fake.VOLUME_MAPPING)
+        ]
+        self.mock_object(self.library._client, 'list_hosts',
+                         mock.Mock(return_value=[fake_host]))
+        self.mock_object(self.library._client, 'list_volumes',
+                         mock.Mock(return_value=[fake_eseries_volume]))
+        self.mock_object(host_mapper, 'unmap_volume_from_host')
+
+        self.library.terminate_connection_fc(get_fake_volume(), connector)
+
+        self.assertTrue(host_mapper.unmap_volume_from_host.called)
+
+    def test_terminate_connection_fc_volume_mapped_no_cleanup_zone(self):
+        connector = {'wwpns': [eseries_fake.WWPN]}
+        fake_host = copy.deepcopy(eseries_fake.HOST)
+        fake_host['hostSidePorts'] = [{
+            'label': 'NewStore',
+            'type': 'fc',
+            'address': eseries_fake.WWPN
+        }]
+        expected_target_info = {
+            'driver_volume_type': 'fibre_channel',
+            'data': {},
+        }
+        fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
+        fake_eseries_volume['listOfMappings'] = [
+            copy.deepcopy(eseries_fake.VOLUME_MAPPING)
+        ]
+        self.mock_object(self.library._client, 'list_hosts',
+                         mock.Mock(return_value=[fake_host]))
+        self.mock_object(self.library._client, 'list_volumes',
+                         mock.Mock(return_value=[fake_eseries_volume]))
+        self.mock_object(host_mapper, 'unmap_volume_from_host')
+        self.mock_object(self.library._client, 'get_volume_mappings_for_host',
+                         mock.Mock(return_value=[copy.deepcopy
+                                                 (eseries_fake.
+                                                  VOLUME_MAPPING)]))
+
+        target_info = self.library.terminate_connection_fc(get_fake_volume(),
+                                                           connector)
+        self.assertDictEqual(expected_target_info, target_info)
+
+        self.assertTrue(host_mapper.unmap_volume_from_host.called)
+
+    def test_terminate_connection_fc_volume_mapped_cleanup_zone(self):
+        connector = {'wwpns': [eseries_fake.WWPN]}
+        fake_host = copy.deepcopy(eseries_fake.HOST)
+        fake_host['hostSidePorts'] = [{
+            'label': 'NewStore',
+            'type': 'fc',
+            'address': eseries_fake.WWPN
+        }]
+        expected_target_info = {
+            'driver_volume_type': 'fibre_channel',
+            'data': {
+                'target_wwn': [eseries_fake.WWPN_2],
+                'initiator_target_map': {
+                    eseries_fake.WWPN: [eseries_fake.WWPN_2]
+                },
+            },
+        }
+        fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
+        fake_eseries_volume['listOfMappings'] = [
+            copy.deepcopy(eseries_fake.VOLUME_MAPPING)
+        ]
+        self.mock_object(self.library._client, 'list_hosts',
+                         mock.Mock(return_value=[fake_host]))
+        self.mock_object(self.library._client, 'list_volumes',
+                         mock.Mock(return_value=[fake_eseries_volume]))
+        self.mock_object(host_mapper, 'unmap_volume_from_host')
+        self.mock_object(self.library._client, 'get_volume_mappings_for_host',
+                         mock.Mock(return_value=[]))
+
+        target_info = self.library.terminate_connection_fc(get_fake_volume(),
+                                                           connector)
+        self.assertDictEqual(expected_target_info, target_info)
+
+        self.assertTrue(host_mapper.unmap_volume_from_host.called)
+
+    def test_terminate_connection_fc_not_mapped_host_with_wwpn_does_not_exist(
+            self):
+        connector = {'wwpns': [eseries_fake.WWPN]}
+        self.mock_object(self.library._client, 'list_hosts',
+                         mock.Mock(return_value=[eseries_fake.HOST_2]))
+        self.assertRaises(exception.NotFound,
+                          self.library.terminate_connection_fc,
+                          get_fake_volume(),
+                          connector)
+
+    def test_initialize_connection_fc_volume_not_mapped(self):
+        connector = {'wwpns': [eseries_fake.WWPN]}
+        self.mock_object(self.library._client,
+                         'get_volume_mappings_for_volume',
+                         mock.Mock(return_value=[]))
+        self.mock_object(host_mapper, 'map_volume_to_single_host',
+                         mock.Mock(
+                             return_value=eseries_fake.VOLUME_MAPPING))
+        expected_target_info = {
+            'driver_volume_type': 'fibre_channel',
+            'data': {
+                'target_discovered': True,
+                'target_lun': 0,
+                'target_wwn': [eseries_fake.WWPN_2],
+                'access_mode': 'rw',
+                'initiator_target_map': {
+                    eseries_fake.WWPN: [eseries_fake.WWPN_2]
+                },
+            },
+        }
+
+        target_info = self.library.initialize_connection_fc(get_fake_volume(),
+                                                            connector)
+
+        self.assertTrue(
+            self.library._client.get_volume_mappings_for_volume.called)
+        self.assertTrue(host_mapper.map_volume_to_single_host.called)
+        self.assertDictEqual(expected_target_info, target_info)
+
+    def test_initialize_connection_fc_volume_not_mapped_host_does_not_exist(
+            self):
+        connector = {'wwpns': [eseries_fake.WWPN]}
+        self.library.driver_protocol = 'FC'
+        self.mock_object(self.library._client,
+                         'get_volume_mappings_for_volume',
+                         mock.Mock(return_value=[]))
+        self.mock_object(self.library._client, 'list_hosts',
+                         mock.Mock(return_value=[]))
+        self.mock_object(self.library._client, 'create_host_with_ports',
+                         mock.Mock(return_value=eseries_fake.HOST))
+        self.mock_object(host_mapper, 'map_volume_to_single_host',
+                         mock.Mock(
+                             return_value=eseries_fake.VOLUME_MAPPING))
+
+        self.library.initialize_connection_fc(get_fake_volume(), connector)
+
+        self.library._client.create_host_with_ports.assert_called_once_with(
+            mock.ANY, mock.ANY,
+            [fczm_utils.get_formatted_wwn(eseries_fake.WWPN)],
+            port_type='fc', group_id=None
+        )
+
+    def test_initialize_connection_fc_volume_already_mapped_to_target_host(
+            self):
+        """Should be a no-op"""
+        connector = {'wwpns': [eseries_fake.WWPN]}
+        self.mock_object(host_mapper, 'map_volume_to_single_host',
+                         mock.Mock(
+                             return_value=eseries_fake.VOLUME_MAPPING))
+
+        self.library.initialize_connection_fc(get_fake_volume(), connector)
+
+        self.assertTrue(host_mapper.map_volume_to_single_host.called)
+
+    def test_initialize_connection_fc_volume_mapped_to_another_host(self):
+        """Should raise error saying multiattach not enabled"""
+        connector = {'wwpns': [eseries_fake.WWPN]}
+        fake_mapping_to_other_host = copy.deepcopy(
+            eseries_fake.VOLUME_MAPPING)
+        fake_mapping_to_other_host['mapRef'] = eseries_fake.HOST_2[
+            'hostRef']
+        self.mock_object(host_mapper, 'map_volume_to_single_host',
+                         mock.Mock(
+                             side_effect=exception.NetAppDriverException))
+
+        self.assertRaises(exception.NetAppDriverException,
+                          self.library.initialize_connection_fc,
+                          get_fake_volume(), connector)
+
+        self.assertTrue(host_mapper.map_volume_to_single_host.called)
+
+    def test_initialize_connection_fc_no_target_wwpns(self):
+        """Should be a no-op"""
+        connector = {'wwpns': [eseries_fake.WWPN]}
+        self.mock_object(host_mapper, 'map_volume_to_single_host',
+                         mock.Mock(
+                             return_value=eseries_fake.VOLUME_MAPPING))
+        self.mock_object(self.library._client, 'list_target_wwpns',
+                         mock.Mock(return_value=[]))
+
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.library.initialize_connection_fc,
+                          get_fake_volume(), connector)
+        self.assertTrue(host_mapper.map_volume_to_single_host.called)
+
+    def test_build_initiator_target_map_fc_with_lookup_service(
+            self):
+        connector = {'wwpns': [eseries_fake.WWPN, eseries_fake.WWPN_2]}
+        self.library.lookup_service = mock.Mock()
+        self.library.lookup_service.get_device_mapping_from_network = (
+            mock.Mock(return_value=eseries_fake.FC_FABRIC_MAP))
+
+        (target_wwpns, initiator_target_map, num_paths) = (
+            self.library._build_initiator_target_map_fc(connector))
+
+        self.assertSetEqual(set(eseries_fake.FC_TARGET_WWPNS),
+                            set(target_wwpns))
+        self.assertDictEqual(eseries_fake.FC_I_T_MAP, initiator_target_map)
+        self.assertEqual(4, num_paths)
+
 
 class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
     """Test driver behavior when the netapp_enable_multiattach
@@ -319,20 +617,20 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
 
     def setUp(self):
         super(NetAppEseriesLibraryMultiAttachTestCase, self).setUp()
-        config = eseries_fakes.create_configuration_eseries()
+        config = eseries_fake.create_configuration_eseries()
         config.netapp_enable_multiattach = True
 
         kwargs = {'configuration': config}
 
         self.library = library.NetAppESeriesLibrary("FAKE", **kwargs)
-        self.library._client = eseries_fakes.FakeEseriesClient()
+        self.library._client = eseries_fake.FakeEseriesClient()
         self.library.check_for_setup_error()
 
     def test_do_setup_host_group_already_exists(self):
         mock_check_flags = self.mock_object(na_utils, 'check_flags')
         self.mock_object(self.library,
                          '_check_mode_get_or_register_storage_system')
-        fake_rest_client = eseries_fakes.FakeEseriesClient()
+        fake_rest_client = eseries_fake.FakeEseriesClient()
         self.mock_object(self.library, '_create_rest_client',
                          mock.Mock(return_value=fake_rest_client))
         mock_create = self.mock_object(fake_rest_client, 'create_host_group')
@@ -344,7 +642,7 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
 
     def test_do_setup_host_group_does_not_exist(self):
         mock_check_flags = self.mock_object(na_utils, 'check_flags')
-        fake_rest_client = eseries_fakes.FakeEseriesClient()
+        fake_rest_client = eseries_fake.FakeEseriesClient()
         self.mock_object(self.library, '_create_rest_client',
                          mock.Mock(return_value=fake_rest_client))
         mock_get_host_group = self.mock_object(
@@ -360,17 +658,17 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
 
     def test_create_volume(self):
         self.library._client.create_volume = mock.Mock(
-            return_value=eseries_fakes.VOLUME)
+            return_value=eseries_fake.VOLUME)
 
         self.library.create_volume(get_fake_volume())
         self.assertTrue(self.library._client.create_volume.call_count)
 
     def test_create_volume_too_many_volumes(self):
         self.library._client.list_volumes = mock.Mock(
-            return_value=[eseries_fakes.VOLUME for __ in
+            return_value=[eseries_fake.VOLUME for __ in
                           range(utils.MAX_LUNS_PER_HOST_GROUP + 1)])
         self.library._client.create_volume = mock.Mock(
-            return_value=eseries_fakes.VOLUME)
+            return_value=eseries_fake.VOLUME)
 
         self.assertRaises(exception.NetAppDriverException,
                           self.library.create_volume,
@@ -378,7 +676,7 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
         self.assertFalse(self.library._client.create_volume.call_count)
 
     def test_create_volume_from_snapshot(self):
-        fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME)
+        fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
         self.mock_object(self.library, "_schedule_and_create_volume",
                          mock.Mock(return_value=fake_eseries_volume))
         self.mock_object(self.library, "_create_snapshot_volume",
@@ -395,7 +693,7 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
             1, self.library._client.delete_snapshot_volume.call_count)
 
     def test_create_volume_from_snapshot_create_fails(self):
-        fake_dest_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME)
+        fake_dest_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
         self.mock_object(self.library, "_schedule_and_create_volume",
                          mock.Mock(return_value=fake_dest_eseries_volume))
         self.mock_object(self.library, "_create_snapshot_volume",
@@ -419,7 +717,7 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
             fake_dest_eseries_volume['volumeRef'])
 
     def test_create_volume_from_snapshot_copy_job_fails(self):
-        fake_dest_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME)
+        fake_dest_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
         self.mock_object(self.library, "_schedule_and_create_volume",
                          mock.Mock(return_value=fake_dest_eseries_volume))
         self.mock_object(self.library, "_create_snapshot_volume",
@@ -428,7 +726,7 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
         self.mock_object(self.library._client, "delete_volume")
 
         fake_failed_volume_copy_job = copy.deepcopy(
-            eseries_fakes.VOLUME_COPY_JOB)
+            eseries_fake.VOLUME_COPY_JOB)
         fake_failed_volume_copy_job['status'] = 'failed'
         self.mock_object(self.library._client,
                          "create_volume_copy_job",
@@ -452,13 +750,13 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
             fake_dest_eseries_volume['volumeRef'])
 
     def test_create_volume_from_snapshot_fail_to_delete_snapshot_volume(self):
-        fake_dest_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME)
+        fake_dest_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
         fake_dest_eseries_volume['volumeRef'] = 'fake_volume_ref'
         self.mock_object(self.library, "_schedule_and_create_volume",
                          mock.Mock(return_value=fake_dest_eseries_volume))
         self.mock_object(self.library, "_create_snapshot_volume",
                          mock.Mock(return_value=copy.deepcopy(
-                             eseries_fakes.VOLUME)))
+                             eseries_fake.VOLUME)))
         self.mock_object(self.library._client, "delete_snapshot_volume",
                          mock.Mock(side_effect=exception.NetAppDriverException)
                          )
@@ -477,48 +775,52 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
 
     def test_map_volume_to_host_volume_not_mapped(self):
         """Map the volume directly to destination host."""
-        self.mock_object(self.library._client, 'get_volume_mappings',
+        self.mock_object(self.library._client,
+                         'get_volume_mappings_for_volume',
                          mock.Mock(return_value=[]))
         self.mock_object(host_mapper, 'map_volume_to_single_host',
                          mock.Mock(
-                             return_value=eseries_fakes.VOLUME_MAPPING))
+                             return_value=eseries_fake.VOLUME_MAPPING))
 
         self.library.map_volume_to_host(get_fake_volume(),
-                                        eseries_fakes.VOLUME,
-                                        eseries_fakes.INITIATOR_NAME_2)
+                                        eseries_fake.VOLUME,
+                                        eseries_fake.INITIATOR_NAME_2)
 
-        self.assertTrue(self.library._client.get_volume_mappings.called)
+        self.assertTrue(
+            self.library._client.get_volume_mappings_for_volume.called)
         self.assertTrue(host_mapper.map_volume_to_single_host.called)
 
     def test_map_volume_to_host_volume_not_mapped_host_does_not_exist(self):
         """Should create the host map directly to the host."""
         self.mock_object(self.library._client, 'list_hosts',
                          mock.Mock(return_value=[]))
-        self.mock_object(self.library._client, 'create_host_with_port',
+        self.mock_object(self.library._client, 'create_host_with_ports',
                          mock.Mock(
-                             return_value=eseries_fakes.HOST_2))
-        self.mock_object(self.library._client, 'get_volume_mappings',
+                             return_value=eseries_fake.HOST_2))
+        self.mock_object(self.library._client,
+                         'get_volume_mappings_for_volume',
                          mock.Mock(return_value=[]))
         self.mock_object(host_mapper, 'map_volume_to_single_host',
                          mock.Mock(
-                             return_value=eseries_fakes.VOLUME_MAPPING))
+                             return_value=eseries_fake.VOLUME_MAPPING))
 
         self.library.map_volume_to_host(get_fake_volume(),
-                                        eseries_fakes.VOLUME,
-                                        eseries_fakes.INITIATOR_NAME_2)
+                                        eseries_fake.VOLUME,
+                                        eseries_fake.INITIATOR_NAME_2)
 
-        self.assertTrue(self.library._client.create_host_with_port.called)
-        self.assertTrue(self.library._client.get_volume_mappings.called)
+        self.assertTrue(self.library._client.create_host_with_ports.called)
+        self.assertTrue(
+            self.library._client.get_volume_mappings_for_volume.called)
         self.assertTrue(host_mapper.map_volume_to_single_host.called)
 
     def test_map_volume_to_host_volume_already_mapped(self):
         """Should be a no-op."""
         self.mock_object(host_mapper, 'map_volume_to_multiple_hosts',
                          mock.Mock(
-                             return_value=eseries_fakes.VOLUME_MAPPING))
+                             return_value=eseries_fake.VOLUME_MAPPING))
 
         self.library.map_volume_to_host(get_fake_volume(),
-                                        eseries_fakes.VOLUME,
-                                        eseries_fakes.INITIATOR_NAME)
+                                        eseries_fake.VOLUME,
+                                        eseries_fake.INITIATOR_NAME)
 
         self.assertTrue(host_mapper.map_volume_to_multiple_hosts.called)
index 90480993b9728d42123ba47703c035b82bf4403e..afb074c42265da57bb9e5a658a03e49c8033dbd1 100644 (file)
@@ -1,5 +1,6 @@
 # Copyright (c) 2014 Navneet Singh.  All rights reserved.
 # Copyright (c) 2014 Clinton Knight.  All rights reserved.
+# Copyright (c) 2015 Alex Meade.  All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
 #    not use this file except in compliance with the License. You may obtain
@@ -49,7 +50,8 @@ NETAPP_UNIFIED_DRIVER_REGISTRY = {
     },
     'eseries':
     {
-        'iscsi': ESERIES_PATH + '.iscsi_driver.NetAppEseriesISCSIDriver'
+        'iscsi': ESERIES_PATH + '.iscsi_driver.NetAppEseriesISCSIDriver',
+        'fc': ESERIES_PATH + '.fc_driver.NetAppEseriesFibreChannelDriver'
     }}
 
 
index 4e3e24147ef1ff673e29b942c243b3bf08075737..867183c1b928c8900439e66f6ffae889cd7740d3 100644 (file)
@@ -1,7 +1,9 @@
-# Copyright (c) 2014 NetApp, Inc.  All rights reserved.
-# Copyright (c) 2014 Navneet Singh.  All rights reserved.
-# Copyright (c) 2015 Alex Meade.  All Rights Reserved.
-# Copyright (c) 2015 Rushil Chugh.  All Rights Reserved.
+# Copyright (c) 2014 NetApp, Inc
+# Copyright (c) 2014 Navneet Singh
+# Copyright (c) 2015 Alex Meade
+# Copyright (c) 2015 Rushil Chugh
+# Copyright (c) 2015 Yogesh Kshirsagar
+#  All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
 #    not use this file except in compliance with the License. You may obtain
@@ -20,9 +22,11 @@ Client classes for web services.
 
 import copy
 import json
+import uuid
 
 from oslo_log import log as logging
 import requests
+import six
 from six.moves import urllib
 
 from cinder import exception
@@ -192,6 +196,25 @@ class RestClient(WebserviceClient):
         path = "/storage-systems/{system-id}/volume-mappings"
         return self._invoke('GET', path)
 
+    def get_volume_mappings_for_volume(self, volume):
+        """Gets all host mappings for given volume from array."""
+        mappings = self.get_volume_mappings() or []
+        host_maps = filter(lambda x: x.get('volumeRef') == volume['volumeRef'],
+                           mappings)
+        return host_maps
+
+    def get_volume_mappings_for_host(self, host_ref):
+        """Gets all volume mappings for given host from array."""
+        mappings = self.get_volume_mappings() or []
+        host_maps = filter(lambda x: x.get('mapRef') == host_ref, mappings)
+        return host_maps
+
+    def get_volume_mappings_for_host_group(self, hg_ref):
+        """Gets all volume mappings for given host group from array."""
+        mappings = self.get_volume_mappings() or []
+        hg_maps = filter(lambda x: x.get('mapRef') == hg_ref, mappings)
+        return hg_maps
+
     def create_volume_mapping(self, object_id, target_id, lun):
         """Creates volume mapping on array."""
         path = "/storage-systems/{system-id}/volume-mappings"
@@ -222,6 +245,13 @@ class RestClient(WebserviceClient):
         path = "/storage-systems/{system-id}/hardware-inventory"
         return self._invoke('GET', path)
 
+    def list_target_wwpns(self):
+        """Lists the world-wide port names of the target."""
+        inventory = self.list_hardware_inventory()
+        fc_ports = inventory.get("fibrePorts", [])
+        wwpns = [port['portName'] for port in fc_ports]
+        return wwpns
+
     def create_host_group(self, label):
         """Creates a host group on the array."""
         path = "/storage-systems/{system-id}/host-groups"
@@ -264,11 +294,18 @@ class RestClient(WebserviceClient):
         data.setdefault('ports', ports if ports else None)
         return self._invoke('POST', path, data)
 
-    def create_host_with_port(self, label, host_type, port_id,
-                              port_label, port_type='iscsi', group_id=None):
+    def create_host_with_ports(self, label, host_type, port_ids,
+                               port_type='iscsi', group_id=None):
         """Creates host on array with given port information."""
-        port = {'type': port_type, 'port': port_id, 'label': port_label}
-        return self.create_host(label, host_type, [port], group_id)
+        if port_type == 'fc':
+            port_ids = [six.text_type(wwpn).replace(':', '')
+                        for wwpn in port_ids]
+        ports = []
+        for port_id in port_ids:
+            port_label = utils.convert_uuid_to_es_fmt(uuid.uuid4())
+            port = {'type': port_type, 'port': port_id, 'label': port_label}
+            ports.append(port)
+        return self.create_host(label, host_type, ports, group_id)
 
     def update_host(self, host_ref, data):
         """Updates host type for a given host."""
diff --git a/cinder/volume/drivers/netapp/eseries/fc_driver.py b/cinder/volume/drivers/netapp/eseries/fc_driver.py
new file mode 100644 (file)
index 0000000..1de849d
--- /dev/null
@@ -0,0 +1,103 @@
+# Copyright (c) - 2014, Alex Meade.  All rights reserved.
+# Copyright (c) - 2015, Yogesh Kshirsagar.  All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+"""
+Volume driver for NetApp E-Series FibreChannel storage systems.
+"""
+
+from oslo_log import log as logging
+
+from cinder.volume import driver
+from cinder.volume.drivers.netapp.eseries import library
+from cinder.volume.drivers.netapp import utils as na_utils
+from cinder.zonemanager import utils as fczm_utils
+
+LOG = logging.getLogger(__name__)
+
+
+class NetAppEseriesFibreChannelDriver(driver.BaseVD,
+                                      driver.ManageableVD,
+                                      driver.ExtendVD,
+                                      driver.CloneableVD,
+                                      driver.TransferVD,
+                                      driver.SnapshotVD):
+    """NetApp E-Series FibreChannel volume driver."""
+
+    DRIVER_NAME = 'NetApp_FibreChannel_ESeries'
+
+    def __init__(self, *args, **kwargs):
+        super(NetAppEseriesFibreChannelDriver, self).__init__(*args, **kwargs)
+        na_utils.validate_instantiation(**kwargs)
+        self.library = library.NetAppESeriesLibrary(self.DRIVER_NAME,
+                                                    'FC', **kwargs)
+
+    def do_setup(self, context):
+        self.library.do_setup(context)
+
+    def check_for_setup_error(self):
+        self.library.check_for_setup_error()
+
+    def create_volume(self, volume):
+        self.library.create_volume(volume)
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        self.library.create_volume_from_snapshot(volume, snapshot)
+
+    def create_cloned_volume(self, volume, src_vref):
+        self.library.create_cloned_volume(volume, src_vref)
+
+    def delete_volume(self, volume):
+        self.library.delete_volume(volume)
+
+    def create_snapshot(self, snapshot):
+        self.library.create_snapshot(snapshot)
+
+    def delete_snapshot(self, snapshot):
+        self.library.delete_snapshot(snapshot)
+
+    def get_volume_stats(self, refresh=False):
+        return self.library.get_volume_stats(refresh)
+
+    def extend_volume(self, volume, new_size):
+        self.library.extend_volume(volume, new_size)
+
+    def ensure_export(self, context, volume):
+        return self.library.ensure_export(context, volume)
+
+    def create_export(self, context, volume):
+        return self.library.create_export(context, volume)
+
+    def remove_export(self, context, volume):
+        self.library.remove_export(context, volume)
+
+    def manage_existing(self, volume, existing_ref):
+        return self.library.manage_existing(volume, existing_ref)
+
+    def manage_existing_get_size(self, volume, existing_ref):
+        return self.library.manage_existing_get_size(volume, existing_ref)
+
+    def unmanage(self, volume):
+        return self.library.unmanage(volume)
+
+    @fczm_utils.AddFCZone
+    def initialize_connection(self, volume, connector, **kwargs):
+        return self.library.initialize_connection_fc(volume, connector)
+
+    @fczm_utils.RemoveFCZone
+    def terminate_connection(self, volume, connector, **kwargs):
+        return self.library.terminate_connection_fc(volume, connector,
+                                                    **kwargs)
+
+    def get_pool(self, volume):
+        return self.library.get_pool(volume)
index 8d79efaf80fe303573568e67c3e653ee4f684045..e464da451088daea6f8595512c90759f31d0d059 100644 (file)
@@ -1,4 +1,5 @@
 ï»¿# Copyright (c) 2015 Alex Meade.  All Rights Reserved.
+# Copyright (c) 2015 Yogesh Kshirsagar.  All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
 #    not use this file except in compliance with the License. You may obtain
@@ -39,7 +40,8 @@ def map_volume_to_single_host(client, volume, eseries_vol, host,
 
     # If volume is not mapped on the backend, map directly to host
     if not vol_map:
-        lun = _get_free_lun(client, host, multiattach_enabled)
+        mappings = client.get_volume_mappings_for_host(host['hostRef'])
+        lun = _get_free_lun(client, host, multiattach_enabled, mappings)
         return client.create_volume_mapping(eseries_vol['volumeRef'],
                                             host['hostRef'], lun)
 
@@ -66,7 +68,9 @@ def map_volume_to_single_host(client, volume, eseries_vol, host,
             LOG.debug("Volume %(vol)s is not currently attached, moving "
                       "existing mapping to host %(host)s.",
                       {'vol': volume['id'], 'host': host['label']})
-            lun = _get_free_lun(client, host, multiattach_enabled)
+            mappings = client.get_volume_mappings_for_host(
+                host['hostRef'])
+            lun = _get_free_lun(client, host, multiattach_enabled, mappings)
             return client.move_volume_mapping_via_symbol(
                 vol_map.get('mapRef'), host['hostRef'], lun
             )
@@ -150,9 +154,8 @@ def map_volume_to_multiple_hosts(client, volume, eseries_vol, target_host,
     return mapping
 
 
-def _get_free_lun(client, host, multiattach_enabled):
+def _get_free_lun(client, host, multiattach_enabled, mappings):
     """Returns least used LUN ID available on the given host."""
-    mappings = client.get_volume_mappings()
     if not _is_host_full(client, host):
         unused_luns = _get_unused_lun_ids(mappings)
         if unused_luns:
@@ -190,13 +193,13 @@ def _get_used_lun_id_counter(mapping):
 
 def _is_host_full(client, host):
     """Checks whether maximum volumes attached to a host have been reached."""
-    luns = _get_vol_mapping_for_host_frm_array(client, host['hostRef'])
+    luns = client.get_volume_mappings_for_host(host['hostRef'])
     return len(luns) >= utils.MAX_LUNS_PER_HOST
 
 
 def _is_lun_id_available_on_host(client, host, lun_id):
     """Returns a boolean value depending on whether a LUN ID is available."""
-    mapping = _get_vol_mapping_for_host_frm_array(client, host['hostRef'])
+    mapping = client.get_volume_mappings_for_host(host['hostRef'])
     used_lun_ids = _get_used_lun_ids_for_mappings(mapping)
     return lun_id not in used_lun_ids
 
@@ -210,20 +213,6 @@ def _get_used_lun_ids_for_mappings(mappings):
     return used_luns
 
 
-def _get_vol_mapping_for_host_frm_array(client, host_ref):
-    """Gets all volume mappings for given host from array."""
-    mappings = client.get_volume_mappings() or []
-    host_maps = filter(lambda x: x.get('mapRef') == host_ref, mappings)
-    return host_maps
-
-
-def _get_vol_mapping_for_host_group_frm_array(client, hg_ref):
-    """Gets all volume mappings for given host from array."""
-    mappings = client.get_volume_mappings() or []
-    hg_maps = filter(lambda x: x.get('mapRef') == hg_ref, mappings)
-    return hg_maps
-
-
 def unmap_volume_from_host(client, volume, host, mapping):
     # Volume is mapped directly to host, so delete the mapping
     if mapping.get('mapRef') == host['hostRef']:
@@ -255,11 +244,3 @@ def unmap_volume_from_host(client, volume, host, mapping):
         LOG.debug("Volume %s is mapped directly to multiattach host group but "
                   "is not currently attached; removing mapping.", volume['id'])
         client.delete_volume_mapping(mapping['lunMappingRef'])
-
-
-def get_host_mapping_for_vol_frm_array(client, volume):
-    """Gets all host mappings for given volume from array."""
-    mappings = client.get_volume_mappings() or []
-    host_maps = filter(lambda x: x.get('volumeRef') == volume['volumeRef'],
-                       mappings)
-    return host_maps
index ff8ef845421d44f744ad0c616f0ad50fa8dab9af..5f2e4168023e812e992292aa0ce38d1cc69f9ff2 100644 (file)
@@ -1,7 +1,8 @@
-# Copyright (c) 2014 NetApp, Inc.  All Rights Reserved.
-# Copyright (c) 2015 Alex Meade.  All Rights Reserved.
-# Copyright (c) 2015 Rushil Chugh.  All Rights Reserved.
-# Copyright (c) 2015 Navneet Singh.  All Rights Reserved.
+# Copyright (c) 2015 Alex Meade
+# Copyright (c) 2015 Rushil Chugh
+# Copyright (c) 2015 Navneet Singh
+# Copyright (c) 2015 Yogesh Kshirsagar
+#  All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
 #    not use this file except in compliance with the License. You may obtain
@@ -38,6 +39,7 @@ from cinder.volume.drivers.netapp.eseries import utils
 from cinder.volume.drivers.netapp import options as na_opts
 from cinder.volume.drivers.netapp import utils as na_utils
 from cinder.volume import utils as volume_utils
+from cinder.zonemanager import utils as fczm_utils
 
 
 LOG = logging.getLogger(__name__)
@@ -101,6 +103,7 @@ class NetAppESeriesLibrary(object):
         self.configuration.append_config_values(na_opts.netapp_transport_opts)
         self.configuration.append_config_values(na_opts.netapp_eseries_opts)
         self.configuration.append_config_values(na_opts.netapp_san_opts)
+        self.lookup_service = fczm_utils.create_lookup_service()
         self._backend_name = self.configuration.safe_get(
             "volume_backend_name") or "NetApp_ESeries"
         self.driver_name = driver_name
@@ -187,7 +190,7 @@ class NetAppESeriesLibrary(object):
                           {'host': host, 'e': e})
                 raise exception.NoValidHost(
                     _("Controller IP '%(host)s' could not be resolved: %(e)s.")
-                    % {'host': host, 'e': six.text_type(e)})
+                    % {'host': host, 'e': e})
 
         ips = self.configuration.netapp_controller_ips
         ips = [i.strip() for i in ips.split(",")]
@@ -516,11 +519,11 @@ class NetAppESeriesLibrary(object):
         """Removes an export for a volume."""
         pass
 
-    def map_volume_to_host(self, volume, eseries_volume, initiator_name):
+    def map_volume_to_host(self, volume, eseries_volume, initiators):
         """Ensures the specified initiator has access to the volume."""
-        existing_maps = host_mapper.get_host_mapping_for_vol_frm_array(
-            self._client, eseries_volume)
-        host = self._get_or_create_host(initiator_name, self.host_type)
+        existing_maps = self._client.get_volume_mappings_for_volume(
+            eseries_volume)
+        host = self._get_or_create_host(initiators, self.host_type)
         # There can only be one or zero mappings on a volume in E-Series
         current_map = existing_maps[0] if existing_maps else None
 
@@ -537,11 +540,168 @@ class NetAppESeriesLibrary(object):
                 self.configuration.netapp_enable_multiattach)
         return mapping
 
+    def initialize_connection_fc(self, volume, connector):
+        """Initializes the connection and returns connection info.
+
+        Assigns the specified volume to a compute node/host so that it can be
+        used from that host.
+
+        The driver returns a driver_volume_type of 'fibre_channel'.
+        The target_wwn can be a single entry or a list of wwns that
+        correspond to the list of remote wwn(s) that will export the volume.
+        Example return values:
+            {
+                'driver_volume_type': 'fibre_channel'
+                'data': {
+                    'target_discovered': True,
+                    'target_lun': 1,
+                    'target_wwn': '500a098280feeba5',
+                    'access_mode': 'rw',
+                    'initiator_target_map': {
+                        '21000024ff406cc3': ['500a098280feeba5'],
+                        '21000024ff406cc2': ['500a098280feeba5']
+                    }
+                }
+            }
+
+            or
+
+             {
+                'driver_volume_type': 'fibre_channel'
+                'data': {
+                    'target_discovered': True,
+                    'target_lun': 1,
+                    'target_wwn': ['500a098280feeba5', '500a098290feeba5',
+                                   '500a098190feeba5', '500a098180feeba5'],
+                    'access_mode': 'rw',
+                    'initiator_target_map': {
+                        '21000024ff406cc3': ['500a098280feeba5',
+                                             '500a098290feeba5'],
+                        '21000024ff406cc2': ['500a098190feeba5',
+                                             '500a098180feeba5']
+                    }
+                }
+            }
+        """
+
+        initiators = [fczm_utils.get_formatted_wwn(wwpn)
+                      for wwpn in connector['wwpns']]
+
+        eseries_vol = self._get_volume(volume['name_id'])
+        mapping = self.map_volume_to_host(volume, eseries_vol,
+                                          initiators)
+        lun_id = mapping['lun']
+
+        initiator_info = self._build_initiator_target_map_fc(connector)
+        target_wwpns, initiator_target_map, num_paths = initiator_info
+
+        if target_wwpns:
+            msg = ("Successfully fetched target details for LUN %(id)s "
+                   "and initiator(s) %(initiators)s.")
+            msg_fmt = {'id': volume['id'], 'initiators': initiators}
+            LOG.debug(msg, msg_fmt)
+        else:
+            msg = _('Failed to get LUN target details for the LUN %s.')
+            raise exception.VolumeBackendAPIException(data=msg % volume['id'])
+
+        target_info = {'driver_volume_type': 'fibre_channel',
+                       'data': {'target_discovered': True,
+                                'target_lun': int(lun_id),
+                                'target_wwn': target_wwpns,
+                                'access_mode': 'rw',
+                                'initiator_target_map': initiator_target_map}}
+
+        return target_info
+
+    def terminate_connection_fc(self, volume, connector, **kwargs):
+        """Disallow connection from connector.
+
+        Return empty data if other volumes are in the same zone.
+        The FibreChannel ZoneManager doesn't remove zones
+        if there isn't an initiator_target_map in the
+        return of terminate_connection.
+
+        :returns: data - the target_wwns and initiator_target_map if the
+                         zone is to be removed, otherwise the same map with
+                         an empty dict for the 'data' key
+        """
+
+        eseries_vol = self._get_volume(volume['name_id'])
+        initiators = [fczm_utils.get_formatted_wwn(wwpn)
+                      for wwpn in connector['wwpns']]
+        host = self._get_host_with_matching_port(initiators)
+        mappings = eseries_vol.get('listOfMappings', [])
+
+        # There can only be one or zero mappings on a volume in E-Series
+        mapping = mappings[0] if mappings else None
+
+        if not mapping:
+            raise eseries_exc.VolumeNotMapped(volume_id=volume['id'],
+                                              host=host['label'])
+        host_mapper.unmap_volume_from_host(self._client, volume, host, mapping)
+
+        info = {'driver_volume_type': 'fibre_channel',
+                'data': {}}
+
+        if len(self._client.get_volume_mappings_for_host(
+                host['hostRef'])) == 0:
+            # No more exports for this host, so tear down zone.
+            LOG.info(_LI("Need to remove FC Zone, building initiator "
+                         "target map."))
+
+            initiator_info = self._build_initiator_target_map_fc(connector)
+            target_wwpns, initiator_target_map, num_paths = initiator_info
+
+            info['data'] = {'target_wwn': target_wwpns,
+                            'initiator_target_map': initiator_target_map}
+
+        return info
+
+    def _build_initiator_target_map_fc(self, connector):
+        """Build the target_wwns and the initiator target map."""
+
+        # get WWPNs from controller and strip colons
+        all_target_wwpns = self._client.list_target_wwpns()
+        all_target_wwpns = [six.text_type(wwpn).replace(':', '')
+                            for wwpn in all_target_wwpns]
+
+        target_wwpns = []
+        init_targ_map = {}
+        num_paths = 0
+
+        if self.lookup_service:
+            # Use FC SAN lookup to determine which ports are visible.
+            dev_map = self.lookup_service.get_device_mapping_from_network(
+                connector['wwpns'],
+                all_target_wwpns)
+
+            for fabric_name in dev_map:
+                fabric = dev_map[fabric_name]
+                target_wwpns += fabric['target_port_wwn_list']
+                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]))
+                    for target in init_targ_map[initiator]:
+                        num_paths += 1
+            target_wwpns = list(set(target_wwpns))
+        else:
+            initiator_wwns = connector['wwpns']
+            target_wwpns = all_target_wwpns
+
+            for initiator in initiator_wwns:
+                init_targ_map[initiator] = target_wwpns
+
+        return target_wwpns, init_targ_map, num_paths
+
     def initialize_connection_iscsi(self, volume, connector):
         """Allow connection to connector and return connection info."""
         initiator_name = connector['initiator']
         eseries_vol = self._get_volume(volume['name_id'])
-        mapping = self.map_volume_to_host(volume, eseries_vol, initiator_name)
+        mapping = self.map_volume_to_host(volume, eseries_vol,
+                                          [initiator_name])
 
         lun_id = mapping['lun']
         msg_fmt = {'id': volume['id'], 'initiator_name': initiator_name}
@@ -599,10 +759,10 @@ class NetAppESeriesLibrary(object):
         raise exception.NetAppDriverException(
             msg % self._client.get_system_id())
 
-    def _get_or_create_host(self, port_id, host_type):
+    def _get_or_create_host(self, port_ids, host_type):
         """Fetch or create a host by given port."""
         try:
-            host = self._get_host_with_port(port_id)
+            host = self._get_host_with_matching_port(port_ids)
             ht_def = self._get_host_type_definition(host_type)
             if host.get('hostTypeIndex') != ht_def.get('index'):
                 try:
@@ -615,30 +775,36 @@ class NetAppESeriesLibrary(object):
             return host
         except exception.NotFound as e:
             LOG.warning(_LW("Message - %s."), e.msg)
-            return self._create_host(port_id, host_type)
+            return self._create_host(port_ids, host_type)
 
-    def _get_host_with_port(self, port_id):
+    def _get_host_with_matching_port(self, port_ids):
         """Gets or creates a host with given port id."""
+        # Remove any extra colons
+        port_ids = [six.text_type(wwpn).replace(':', '')
+                    for wwpn in port_ids]
         hosts = self._client.list_hosts()
-        for host in hosts:
-            if host.get('hostSidePorts'):
-                ports = host.get('hostSidePorts')
-                for port in ports:
-                    if (port.get('type') == 'iscsi'
-                            and port.get('address') == port_id):
-                        return host
-        msg = _("Host with port %(port)s not found.")
-        raise exception.NotFound(msg % {'port': port_id})
-
-    def _create_host(self, port_id, host_type, host_group=None):
+        for port_id in port_ids:
+            for host in hosts:
+                if host.get('hostSidePorts'):
+                    ports = host.get('hostSidePorts')
+                    for port in ports:
+                        address = port.get('address').upper().replace(':', '')
+                        if address == port_id.upper():
+                            return host
+        msg = _("Host with ports %(ports)s not found.")
+        raise exception.NotFound(msg % {'ports': port_ids})
+
+    def _create_host(self, port_ids, host_type, host_group=None):
         """Creates host on system with given initiator as port_id."""
-        LOG.info(_LI("Creating host with port %s."), port_id)
-        label = utils.convert_uuid_to_es_fmt(uuid.uuid4())
-        port_label = utils.convert_uuid_to_es_fmt(uuid.uuid4())
+        LOG.info(_LI("Creating host with ports %s."), port_ids)
+        host_label = utils.convert_uuid_to_es_fmt(uuid.uuid4())
         host_type = self._get_host_type_definition(host_type)
-        return self._client.create_host_with_port(label, host_type,
-                                                  port_id, port_label,
-                                                  group_id=host_group)
+        port_type = self.driver_protocol.lower()
+        return self._client.create_host_with_ports(host_label,
+                                                   host_type,
+                                                   port_ids,
+                                                   group_id=host_group,
+                                                   port_type=port_type)
 
     def _get_host_type_definition(self, host_type):
         """Gets supported host type if available on storage system."""
@@ -652,7 +818,7 @@ class NetAppESeriesLibrary(object):
         """Disallow connection from connector."""
         eseries_vol = self._get_volume(volume['name_id'])
         initiator = connector['initiator']
-        host = self._get_host_with_port(initiator)
+        host = self._get_host_with_matching_port([initiator])
         mappings = eseries_vol.get('listOfMappings', [])
 
         # There can only be one or zero mappings on a volume in E-Series
index aee98e550863570ca41a9e009ffb7a099638aeef..f596004f561e1b0f85578ee25035b753d0e4c976 100644 (file)
@@ -6,6 +6,7 @@
 hacking<0.11,>=0.10.0
 
 coverage>=3.6
+ddt>=0.7.0
 discover
 fixtures>=0.3.14
 mock>=1.0