From 8db2c109b4fad9da6f7b4640f0e6e0d2a6c3f24f Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Fri, 1 May 2015 12:56:48 -0400 Subject: [PATCH] NetApp E-Series: Add Fibre Channel Support 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 Co-Authored-By: Yogesh Kshirsagar Change-Id: I130f473aaa27ace4cd16a98f75c797aa967715b3 --- .../volume/drivers/netapp/eseries/fakes.py | 155 ++++++- .../drivers/netapp/eseries/test_client.py | 144 ++++++ .../drivers/netapp/eseries/test_fc_driver.py | 30 ++ .../netapp/eseries/test_host_mapper.py | 41 +- .../drivers/netapp/eseries/test_library.py | 432 +++++++++++++++--- cinder/volume/drivers/netapp/common.py | 4 +- .../volume/drivers/netapp/eseries/client.py | 53 ++- .../drivers/netapp/eseries/fc_driver.py | 103 +++++ .../drivers/netapp/eseries/host_mapper.py | 37 +- .../volume/drivers/netapp/eseries/library.py | 230 ++++++++-- test-requirements.txt | 1 + 11 files changed, 1067 insertions(+), 163 deletions(-) create mode 100644 cinder/tests/unit/volume/drivers/netapp/eseries/test_fc_driver.py create mode 100644 cinder/volume/drivers/netapp/eseries/fc_driver.py diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py b/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py index 1f931e906..548670d56 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py @@ -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] diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py index 8aef18cf9..537219b7b 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py @@ -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 @@ -13,9 +14,13 @@ # 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 index 000000000..9163eedfd --- /dev/null +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/test_fc_driver.py @@ -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) diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_host_mapper.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_host_mapper.py index 7561ff589..d9ae779b4 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_host_mapper.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/test_host_mapper.py @@ -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( diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py index eb2652e60..12cd23509 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py @@ -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 @@ -16,20 +17,22 @@ # 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) diff --git a/cinder/volume/drivers/netapp/common.py b/cinder/volume/drivers/netapp/common.py index 90480993b..afb074c42 100644 --- a/cinder/volume/drivers/netapp/common.py +++ b/cinder/volume/drivers/netapp/common.py @@ -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' }} diff --git a/cinder/volume/drivers/netapp/eseries/client.py b/cinder/volume/drivers/netapp/eseries/client.py index 4e3e24147..867183c1b 100644 --- a/cinder/volume/drivers/netapp/eseries/client.py +++ b/cinder/volume/drivers/netapp/eseries/client.py @@ -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 index 000000000..1de849df6 --- /dev/null +++ b/cinder/volume/drivers/netapp/eseries/fc_driver.py @@ -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) diff --git a/cinder/volume/drivers/netapp/eseries/host_mapper.py b/cinder/volume/drivers/netapp/eseries/host_mapper.py index 8d79efaf8..e464da451 100644 --- a/cinder/volume/drivers/netapp/eseries/host_mapper.py +++ b/cinder/volume/drivers/netapp/eseries/host_mapper.py @@ -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 diff --git a/cinder/volume/drivers/netapp/eseries/library.py b/cinder/volume/drivers/netapp/eseries/library.py index ff8ef8454..5f2e41680 100644 --- a/cinder/volume/drivers/netapp/eseries/library.py +++ b/cinder/volume/drivers/netapp/eseries/library.py @@ -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 diff --git a/test-requirements.txt b/test-requirements.txt index aee98e550..f596004f5 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -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 -- 2.45.2