From 05eeea8bfff84153e47f1d7a3d71e5c37063a93e Mon Sep 17 00:00:00 2001 From: Clinton Knight Date: Thu, 9 Oct 2014 10:56:25 -0400 Subject: [PATCH] FibreChannel drivers for NetApp Data ONTAP storage controllers This patch adds FibreChannel support to NetApp's Cinder drivers for Data ONTAP (7-mode and Cluster-mode). The drivers make full use of Cinder's FibreChannel zone manager. Implements blueprint add-fibre-channel-support-to-netapp-drivers Change-Id: Ifbda275e4a60dda144a169ef00a4ea5e548dfa03 --- .../drivers/netapp/dataontap/__init__.py | 0 .../dataontap/client/test_client_7mode.py | 138 +++++++-- .../dataontap/client/test_client_base.py | 66 +++++ .../dataontap/client/test_client_cmode.py | 193 +++++++++--- .../volume/drivers/netapp/dataontap/fakes.py | 75 +++++ .../netapp/dataontap/test_block_7mode.py | 277 +++++++++++++++++- .../netapp/dataontap/test_block_base.py | 256 +++++++++++++--- .../netapp/dataontap/test_block_cmode.py | 142 +++++++-- .../volume/drivers/netapp/eseries/__init__.py | 0 cinder/tests/volume/drivers/netapp/fakes.py | 44 +++ cinder/volume/drivers/netapp/common.py | 9 +- .../drivers/netapp/dataontap/block_7mode.py | 79 +++-- .../drivers/netapp/dataontap/block_base.py | 203 +++++++++++-- .../drivers/netapp/dataontap/block_cmode.py | 11 +- .../netapp/dataontap/client/client_7mode.py | 112 +++++-- .../netapp/dataontap/client/client_base.py | 28 +- .../netapp/dataontap/client/client_cmode.py | 135 ++++++--- .../drivers/netapp/dataontap/fc_7mode.py | 85 ++++++ .../drivers/netapp/dataontap/fc_cmode.py | 85 ++++++ cinder/volume/drivers/netapp/options.py | 16 +- cinder/volume/drivers/netapp/utils.py | 6 + 21 files changed, 1690 insertions(+), 270 deletions(-) create mode 100644 cinder/tests/volume/drivers/netapp/dataontap/__init__.py create mode 100644 cinder/tests/volume/drivers/netapp/dataontap/fakes.py create mode 100644 cinder/tests/volume/drivers/netapp/eseries/__init__.py create mode 100644 cinder/tests/volume/drivers/netapp/fakes.py create mode 100644 cinder/volume/drivers/netapp/dataontap/fc_7mode.py create mode 100644 cinder/volume/drivers/netapp/dataontap/fc_cmode.py diff --git a/cinder/tests/volume/drivers/netapp/dataontap/__init__.py b/cinder/tests/volume/drivers/netapp/dataontap/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_7mode.py b/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_7mode.py index bb1791be3..01ace7447 100644 --- a/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_7mode.py +++ b/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_7mode.py @@ -20,9 +20,10 @@ import mock import six from cinder import test +from cinder.tests.volume.drivers.netapp.dataontap import fakes as fake from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_7mode - +from cinder.volume.drivers.netapp.utils import hashabledict CONNECTION_INFO = {'hostname': 'hostname', 'transport_type': 'https', @@ -51,7 +52,7 @@ class NetApp7modeClientTestCase(test.TestCase): def tearDown(self): super(NetApp7modeClientTestCase, self).tearDown() - def test_get_target_details_no_targets(self): + def test_get_iscsi_target_details_no_targets(self): response = netapp_api.NaElement( etree.XML(""" @@ -59,11 +60,11 @@ class NetApp7modeClientTestCase(test.TestCase): """)) self.connection.invoke_successfully.return_value = response - target_list = self.client.get_target_details() + target_list = self.client.get_iscsi_target_details() self.assertEqual([], target_list) - def test_get_target_details(self): + def test_get_iscsi_target_details(self): expected_target = { "address": "127.0.0.1", "port": "1337", @@ -81,7 +82,7 @@ class NetApp7modeClientTestCase(test.TestCase): """ % expected_target)) self.connection.invoke_successfully.return_value = response - target_list = self.client.get_target_details() + target_list = self.client.get_iscsi_target_details() self.assertEqual([expected_target], target_list) @@ -121,8 +122,9 @@ class NetApp7modeClientTestCase(test.TestCase): self.assertEqual(2, len(luns)) - def test_get_igroup_by_initiator_none_found(self): - initiator = 'initiator' + def test_get_igroup_by_initiators_none_found(self): + initiators = fake.FC_FORMATTED_INITIATORS[0] + response = netapp_api.NaElement( etree.XML(""" @@ -130,36 +132,89 @@ class NetApp7modeClientTestCase(test.TestCase): """)) self.connection.invoke_successfully.return_value = response - igroup = self.client.get_igroup_by_initiator(initiator) + igroup = self.client.get_igroup_by_initiators(initiators) self.assertEqual([], igroup) - def test_get_igroup_by_initiator(self): - initiator = 'initiator' - expected_igroup = { - "initiator-group-os-type": None, - "initiator-group-type": "1337", - "initiator-group-name": "vserver", - } + def test_get_igroup_by_initiators(self): + initiators = [fake.FC_FORMATTED_INITIATORS[0]] response = netapp_api.NaElement( etree.XML(""" - - - - - initiator - - - %(initiator-group-type)s - %(initiator-group-name)s - - - """ % expected_igroup)) + + + %(initiator-group-name)s + %(initiator-group-type)s + 1477ee47-0e1f-4b35-a82c-dcca0b76fc44 + + linux + 0 + false + + false + true + true + + true + + + 21:00:00:24:ff:40:6c:c3 + + + + + """ % fake.IGROUP1)) + self.connection.invoke_successfully.return_value = response + + igroups = self.client.get_igroup_by_initiators(initiators) + + # make these lists of dicts comparable using hashable dictionaries + igroups = set([hashabledict(igroup) for igroup in igroups]) + expected = set([hashabledict(fake.IGROUP1)]) + + self.assertSetEqual(igroups, expected) + + def test_get_igroup_by_initiators_multiple(self): + initiators = fake.FC_FORMATTED_INITIATORS + response = netapp_api.NaElement( + etree.XML(""" + + + %(initiator-group-name)s + %(initiator-group-type)s + 1477ee47-0e1f-4b35-a82c-dcca0b76fc44 + + linux + + + 21:00:00:24:ff:40:6c:c3 + + + 21:00:00:24:ff:40:6c:c2 + + + + + openstack-igroup2 + fcp + 1477ee47-0e1f-4b35-a82c-dcca0b76fc44 + + linux + + + 21:00:00:24:ff:40:6c:c2 + + + + """ % fake.IGROUP1)) self.connection.invoke_successfully.return_value = response - igroup = self.client.get_igroup_by_initiator(initiator) + igroups = self.client.get_igroup_by_initiators(initiators) - self.assertEqual([expected_igroup], igroup) + # make these lists of dicts comparable using hashable dictionaries + igroups = set([hashabledict(igroup) for igroup in igroups]) + expected = set([hashabledict(fake.IGROUP1)]) + + self.assertSetEqual(igroups, expected) def test_clone_lun(self): fake_clone_start = netapp_api.NaElement( @@ -561,3 +616,28 @@ class NetApp7modeClientTestCase(test.TestCase): actual_request = _args[0] self.assertEqual('net-ifconfig-get', actual_request.get_name()) self.assertEqual(expected_response, actual_response) + + def test_get_fc_target_wwpns(self): + wwpn1 = '50:0a:09:81:90:fe:eb:a5' + wwpn2 = '50:0a:09:82:90:fe:eb:a5' + response = netapp_api.NaElement( + etree.XML(""" + + + + %(wwpn1)s + true + 1a + + + %(wwpn2)s + true + 1b + + + """ % {'wwpn1': wwpn1, 'wwpn2': wwpn2})) + self.connection.invoke_successfully.return_value = response + + wwpns = self.client.get_fc_target_wwpns() + + self.assertSetEqual(set(wwpns), set([wwpn1, wwpn2])) diff --git a/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_base.py b/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_base.py index b84a53b23..e0f73a537 100644 --- a/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_base.py +++ b/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_base.py @@ -19,6 +19,7 @@ import mock import six from cinder import test +import cinder.tests.volume.drivers.netapp.dataontap.fakes as fake from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_base @@ -61,7 +62,9 @@ class NetAppBaseClientTestCase(test.TestCase): def test_get_ontapi_version_cached(self): self.connection.get_api_version.return_value = (1, 20) + major, minor = self.client.get_ontapi_version() + self.assertEqual(1, self.connection.get_api_version.call_count) self.assertEqual(1, major) self.assertEqual(20, minor) @@ -69,6 +72,7 @@ class NetAppBaseClientTestCase(test.TestCase): def test_check_is_naelement(self): element = netapp_api.NaElement('name') + self.assertIsNone(self.client.check_is_naelement(element)) self.assertRaises(ValueError, self.client.check_is_naelement, None) @@ -366,6 +370,7 @@ class NetAppBaseClientTestCase(test.TestCase): self.connection.invoke_successfully.return_value = mock_response geometry = self.client.get_lun_geometry(path) + self.assertEqual(expected_keys, set(geometry.keys())) def test_get_lun_geometry_with_api_error(self): @@ -380,6 +385,7 @@ class NetAppBaseClientTestCase(test.TestCase): fake_response = netapp_api.NaElement('volume') fake_response.add_node_with_children('options', test='blah') self.connection.invoke_successfully.return_value = fake_response + options = self.client.get_volume_options('volume') self.assertEqual(1, len(options)) @@ -387,6 +393,7 @@ class NetAppBaseClientTestCase(test.TestCase): def test_get_volume_options_with_no_options(self): fake_response = netapp_api.NaElement('options') self.connection.invoke_successfully.return_value = fake_response + options = self.client.get_volume_options('volume') self.assertEqual([], options) @@ -396,7 +403,66 @@ class NetAppBaseClientTestCase(test.TestCase): new_path = '/vol/%s/%s' % (self.fake_volume, self.fake_lun) fake_response = netapp_api.NaElement('options') self.connection.invoke_successfully.return_value = fake_response + self.client.move_lun(path, new_path) self.connection.invoke_successfully.assert_called_once_with( mock.ANY, True) + + def test_get_igroup_by_initiators(self): + self.assertRaises(NotImplementedError, + self.client.get_igroup_by_initiators, + fake.FC_FORMATTED_INITIATORS) + + def test_get_fc_target_wwpns(self): + self.assertRaises(NotImplementedError, + self.client.get_fc_target_wwpns) + + def test_has_luns_mapped_to_initiator(self): + initiator = fake.FC_FORMATTED_INITIATORS[0] + version_response = netapp_api.NaElement( + etree.XML(""" + + + + /vol/cinder1/volume-9be956b3-9854-4a5c-a7f5-13a16da52c9c + openstack-4b57a80b-ebca-4d27-bd63-48ac5408d08b + + 0 + + + /vol/cinder1/volume-ac90433c-a560-41b3-9357-7f3f80071eb5 + openstack-4b57a80b-ebca-4d27-bd63-48ac5408d08b + + 1 + + + """)) + + self.connection.invoke_successfully.return_value = version_response + + self.assertTrue(self.client._has_luns_mapped_to_initiator(initiator)) + + def test_has_luns_mapped_to_initiator_not_mapped(self): + initiator = fake.FC_FORMATTED_INITIATORS[0] + version_response = netapp_api.NaElement( + etree.XML(""" + + + """)) + self.connection.invoke_successfully.return_value = version_response + self.assertFalse(self.client._has_luns_mapped_to_initiator(initiator)) + + @mock.patch.object(client_base.Client, '_has_luns_mapped_to_initiator') + def test_has_luns_mapped_to_initiators(self, + mock_has_luns_mapped_to_initiator): + initiators = fake.FC_FORMATTED_INITIATORS + mock_has_luns_mapped_to_initiator.return_value = True + self.assertTrue(self.client.has_luns_mapped_to_initiators(initiators)) + + @mock.patch.object(client_base.Client, '_has_luns_mapped_to_initiator') + def test_has_luns_mapped_to_initiators_not_mapped( + self, mock_has_luns_mapped_to_initiator): + initiators = fake.FC_FORMATTED_INITIATORS + mock_has_luns_mapped_to_initiator.return_value = False + self.assertFalse(self.client.has_luns_mapped_to_initiators(initiators)) diff --git a/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_cmode.py b/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_cmode.py index 40f74a888..b9ef9ae90 100644 --- a/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_cmode.py +++ b/cinder/tests/volume/drivers/netapp/dataontap/client/test_client_cmode.py @@ -23,6 +23,7 @@ from cinder import exception from cinder import test from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_cmode +from cinder.volume.drivers.netapp.utils import hashabledict CONNECTION_INFO = {'hostname': 'hostname', @@ -52,18 +53,18 @@ class NetAppCmodeClientTestCase(test.TestCase): def tearDown(self): super(NetAppCmodeClientTestCase, self).tearDown() - def test_get_target_details_no_targets(self): + def test_get_iscsi_target_details_no_targets(self): response = netapp_api.NaElement( etree.XML(""" 1 """)) self.connection.invoke_successfully.return_value = response - target_list = self.client.get_target_details() + target_list = self.client.get_iscsi_target_details() self.assertEqual([], target_list) - def test_get_target_details(self): + def test_get_iscsi_target_details(self): expected_target = { "address": "127.0.0.1", "port": "1337", @@ -84,7 +85,7 @@ class NetAppCmodeClientTestCase(test.TestCase): """ % expected_target)) self.connection.invoke_successfully.return_value = response - target_list = self.client.get_target_details() + target_list = self.client.get_iscsi_target_details() self.assertEqual([expected_target], target_list) @@ -241,67 +242,169 @@ class NetAppCmodeClientTestCase(test.TestCase): """)) self.connection.invoke_successfully.return_value = response - igroup = self.client.get_igroup_by_initiator(initiator) + igroup = self.client.get_igroup_by_initiators([initiator]) self.assertEqual([], igroup) - def test_get_igroup_by_initiator(self): - initiator = 'initiator' + def test_get_igroup_by_initiators(self): + initiators = ['11:22:33:44:55:66:77:88'] expected_igroup = { - "initiator-group-os-type": None, - "initiator-group-type": "1337", - "initiator-group-name": "vserver", + 'initiator-group-os-type': 'default', + 'initiator-group-type': 'fcp', + 'initiator-group-name': 'openstack-igroup1', } + response = netapp_api.NaElement( etree.XML(""" - 1 - - - %(initiator-group-type)s - %(initiator-group-name)s - - - """ % expected_igroup)) + + + true + %(initiator-group-name)s + default + false + 0 + %(initiator-group-type)s + true + f8aa707a-57fa-11e4-ad08-123478563412 + + false + + + 11:22:33:44:55:66:77:88 + + + cinder-iscsi + + + 1 + """ % expected_igroup)) self.connection.invoke_successfully.return_value = response - igroup = self.client.get_igroup_by_initiator(initiator) + igroups = self.client.get_igroup_by_initiators(initiators) - self.assertEqual([expected_igroup], igroup) + # make these lists of dicts comparable using hashable dictionaries + igroups = set([hashabledict(igroup) for igroup in igroups]) + expected = set([hashabledict(expected_igroup)]) - def test_get_igroup_by_initiator_multiple_pages(self): - initiator = 'initiator' + self.assertSetEqual(igroups, expected) + + def test_get_igroup_by_initiators_multiple(self): + initiators = ['11:22:33:44:55:66:77:88', '88:77:66:55:44:33:22:11'] expected_igroup = { - "initiator-group-os-type": None, - "initiator-group-type": "1337", - "initiator-group-name": "vserver", + 'initiator-group-os-type': 'default', + 'initiator-group-type': 'fcp', + 'initiator-group-name': 'openstack-igroup1', } + response = netapp_api.NaElement( etree.XML(""" - 1 - - - %(initiator-group-type)s - %(initiator-group-name)s - - - blah - """ % expected_igroup)) + + + true + %(initiator-group-name)s + default + false + 0 + %(initiator-group-type)s + true + f8aa707a-57fa-11e4-ad08-123478563412 + + false + + + 11:22:33:44:55:66:77:88 + + + 88:77:66:55:44:33:22:11 + + + cinder-iscsi + + + 1 + """ % expected_igroup)) + self.connection.invoke_successfully.return_value = response + + igroups = self.client.get_igroup_by_initiators(initiators) + + # make these lists of dicts comparable using hashable dictionaries + igroups = set([hashabledict(igroup) for igroup in igroups]) + expected = set([hashabledict(expected_igroup)]) + + self.assertSetEqual(igroups, expected) + + def test_get_igroup_by_initiators_multiple_pages(self): + initiator = '11:22:33:44:55:66:77:88' + expected_igroup1 = { + 'initiator-group-os-type': 'default', + 'initiator-group-type': 'fcp', + 'initiator-group-name': 'openstack-igroup1', + } + expected_igroup2 = { + 'initiator-group-os-type': 'default', + 'initiator-group-type': 'fcp', + 'initiator-group-name': 'openstack-igroup2', + } + response_1 = netapp_api.NaElement( + etree.XML(""" + + + true + %(initiator-group-name)s + default + false + 0 + %(initiator-group-type)s + true + f8aa707a-57fa-11e4-ad08-123478563412 + + false + + + 11:22:33:44:55:66:77:88 + + + cinder-iscsi + + + 12345 + 1 + """ % expected_igroup1)) response_2 = netapp_api.NaElement( etree.XML(""" - 1 - - - %(initiator-group-type)s - %(initiator-group-name)s - - - """ % expected_igroup)) - self.connection.invoke_successfully.side_effect = [response, + + + true + %(initiator-group-name)s + default + false + 0 + %(initiator-group-type)s + true + f8aa707a-57fa-11e4-ad08-123478563412 + + false + + + 11:22:33:44:55:66:77:88 + + + cinder-iscsi + + + 1 + """ % expected_igroup2)) + self.connection.invoke_successfully.side_effect = [response_1, response_2] - igroup = self.client.get_igroup_by_initiator(initiator) + igroups = self.client.get_igroup_by_initiators([initiator]) + + # make these lists of dicts comparable using hashable dictionaries + igroups = set([hashabledict(igroup) for igroup in igroups]) + expected = set([hashabledict(expected_igroup1), + hashabledict(expected_igroup2)]) - self.assertEqual([expected_igroup, expected_igroup], igroup) + self.assertSetEqual(igroups, expected) def test_clone_lun(self): self.client.clone_lun('volume', 'fakeLUN', 'newFakeLUN') diff --git a/cinder/tests/volume/drivers/netapp/dataontap/fakes.py b/cinder/tests/volume/drivers/netapp/dataontap/fakes.py new file mode 100644 index 000000000..7ff493ee3 --- /dev/null +++ b/cinder/tests/volume/drivers/netapp/dataontap/fakes.py @@ -0,0 +1,75 @@ +# Copyright (c) - 2014, Clinton Knight. 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 = 'f10d1a84-9b7b-427e-8fec-63c48b509a56' +LUN = 'ee6b4cc7-477b-4016-aa0c-7127b4e3af86' +SIZE = '1024' +METADATA = {'OsType': 'linux', 'SpaceReserved': 'true'} + +UUID1 = '12345678-1234-5678-1234-567812345678' +LUN1 = '/vol/vol0/lun1' +IGROUP1_NAME = 'openstack-igroup1' +VSERVER1_NAME = 'openstack-vserver' + +FC_VOLUME = {'name': 'fake_volume'} + +FC_INITIATORS = ['21000024ff406cc3', '21000024ff406cc2'] +FC_FORMATTED_INITIATORS = ['21:00:00:24:ff:40:6c:c3', + '21:00:00:24:ff:40:6c:c2'] + +FC_TARGET_WWPNS = ['500a098280feeba5', '500a098290feeba5', + '500a098190feeba5', '500a098180feeba5'] + +FC_FORMATTED_TARGET_WWPNS = ['50:0a:09:82:80:fe:eb:a5', + '50:0a:09:82:90:fe:eb:a5', + '50:0a:09:81:90:fe:eb:a5', + '50:0a:09:81:80:fe:eb:a5'] + +FC_CONNECTOR = {'ip': '1.1.1.1', + 'host': 'fake_host', + 'wwnns': ['20000024ff406cc3', '20000024ff406cc2'], + 'wwpns': ['21000024ff406cc3', '21000024ff406cc2']} + +FC_I_T_MAP = {'21000024ff406cc3': ['500a098280feeba5', '500a098290feeba5'], + '21000024ff406cc2': ['500a098190feeba5', '500a098180feeba5']} + +FC_I_T_MAP_COMPLETE = {'21000024ff406cc3': FC_TARGET_WWPNS, + '21000024ff406cc2': FC_TARGET_WWPNS} + +FC_FABRIC_MAP = {'fabricB': + {'target_port_wwn_list': + ['500a098190feeba5', '500a098180feeba5'], + 'initiator_port_wwn_list': ['21000024ff406cc2']}, + 'fabricA': + {'target_port_wwn_list': + ['500a098290feeba5', '500a098280feeba5'], + 'initiator_port_wwn_list': ['21000024ff406cc3']}} + +FC_TARGET_INFO = {'driver_volume_type': 'fibre_channel', + 'data': {'target_lun': '1', + 'initiator_target_map': FC_I_T_MAP, + 'access_mode': 'rw', + 'target_wwn': FC_TARGET_WWPNS, + 'target_discovered': True}} + +FC_TARGET_INFO_EMPTY = {'driver_volume_type': 'fibre_channel', 'data': {}} + +FC_TARGET_INFO_UNMAP = {'driver_volume_type': 'fibre_channel', + 'data': {'target_wwn': FC_TARGET_WWPNS, + 'initiator_target_map': FC_I_T_MAP}} + +IGROUP1 = {'initiator-group-os-type': 'linux', + 'initiator-group-type': 'fcp', + 'initiator-group-name': IGROUP1_NAME} diff --git a/cinder/tests/volume/drivers/netapp/dataontap/test_block_7mode.py b/cinder/tests/volume/drivers/netapp/dataontap/test_block_7mode.py index ef5baf920..aee2a3b5d 100644 --- a/cinder/tests/volume/drivers/netapp/dataontap/test_block_7mode.py +++ b/cinder/tests/volume/drivers/netapp/dataontap/test_block_7mode.py @@ -1,6 +1,5 @@ # Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2014 Clinton Knight. All rights reserved. -# 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 @@ -17,19 +16,22 @@ Mock unit tests for the NetApp block storage 7-mode library """ -import uuid +from lxml import etree import mock -import six +from cinder import exception from cinder import test +import cinder.tests.volume.drivers.netapp.dataontap.fakes as fake +import cinder.tests.volume.drivers.netapp.fakes as na_fakes from cinder.volume.drivers.netapp.dataontap import block_7mode +from cinder.volume.drivers.netapp.dataontap.block_7mode import \ + NetAppBlockStorage7modeLibrary as block_lib_7mode +from cinder.volume.drivers.netapp.dataontap.block_base import \ + NetAppBlockStorageLibrary as block_lib from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api - -FAKE_VOLUME = six.text_type(uuid.uuid4()) -FAKE_LUN = six.text_type(uuid.uuid4()) -FAKE_SIZE = '1024' -FAKE_METADATA = {'OsType': 'linux', 'SpaceReserved': 'true'} +from cinder.volume.drivers.netapp.dataontap.client.api import NaApiError +from cinder.volume.drivers.netapp.dataontap.client import client_base class NetAppBlockStorage7modeLibraryTestCase(test.TestCase): @@ -38,17 +40,231 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase): def setUp(self): super(NetAppBlockStorage7modeLibraryTestCase, self).setUp() - kwargs = {'configuration': mock.Mock()} - self.library = block_7mode.NetAppBlockStorage7modeLibrary('driver', - 'protocol', - **kwargs) + kwargs = {'configuration': self.get_config_7mode()} + self.library = block_lib_7mode('driver', 'protocol', **kwargs) self.library.zapi_client = mock.Mock() + self.zapi_client = self.library.zapi_client self.library.vfiler = mock.Mock() def tearDown(self): super(NetAppBlockStorage7modeLibraryTestCase, self).tearDown() + def get_config_7mode(self): + config = na_fakes.create_configuration_7mode() + config.netapp_storage_protocol = 'iscsi' + config.netapp_login = 'admin' + config.netapp_password = 'pass' + config.netapp_server_hostname = '127.0.0.1' + config.netapp_transport_type = 'http' + config.netapp_server_port = '80' + return config + + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.MagicMock(return_value=(1, 20))) + @mock.patch.object(block_lib_7mode, '_get_root_volume_name') + @mock.patch.object(block_lib_7mode, '_do_partner_setup') + @mock.patch.object(block_lib, 'do_setup') + def test_do_setup(self, super_do_setup, mock_do_partner_setup, + mock_get_root_volume_name): + mock_get_root_volume_name.return_value = 'vol0' + context = mock.Mock() + + self.library.do_setup(context) + + super_do_setup.assert_called_once_with(context) + mock_do_partner_setup.assert_called_once_with() + mock_get_root_volume_name.assert_called_once_with() + + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.MagicMock(return_value=(1, 20))) + def test_do_partner_setup(self): + self.library.configuration.netapp_partner_backend_name = 'partner' + + self.library._do_partner_setup() + + self.assertIsNotNone(self.library.partner_zapi_client) + + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.MagicMock(return_value=(1, 20))) + def test_do_partner_setup_no_partner(self): + + self.library._do_partner_setup() + + self.assertFalse(hasattr(self.library, 'partner_zapi_client')) + + @mock.patch.object(block_lib, 'check_for_setup_error') + def test_check_for_setup_error(self, super_check_for_setup_error): + self.zapi_client.get_ontapi_version.return_value = (1, 9) + + self.library.check_for_setup_error() + + super_check_for_setup_error.assert_called_once_with() + + def test_check_for_setup_error_too_old(self): + self.zapi_client.get_ontapi_version.return_value = (1, 8) + self.assertRaises(exception.VolumeBackendAPIException, + self.library.check_for_setup_error) + + def test_find_mapped_lun_igroup(self): + response = netapp_api.NaElement(etree.XML(""" + + + + %(initiator-group-name)s + %(initiator-group-type)s + 1477ee47-0e1f-4b35-a82c-dcca0b76fc44 + + linux + 0 + false + + false + true + true + + true + + + 21:00:00:24:ff:40:6c:c3 + + + 21:00:00:24:ff:40:6c:c2 + + Centos + + + + 2 + + + """ % fake.IGROUP1)) + initiators = fake.FC_FORMATTED_INITIATORS + self.zapi_client.get_lun_map.return_value = response + + (igroup, lun_id) = self.library._find_mapped_lun_igroup('path', + initiators) + + self.assertEqual(igroup, fake.IGROUP1_NAME) + self.assertEqual(lun_id, '2') + + def test_find_mapped_lun_igroup_initiator_mismatch(self): + response = netapp_api.NaElement(etree.XML(""" + + + + openstack-igroup1 + fcp + 1477ee47-0e1f-4b35-a82c-dcca0b76fc44 + + linux + 0 + false + + false + true + true + + true + + + 21:00:00:24:ff:40:6c:c3 + + + 2 + + + """)) + initiators = fake.FC_FORMATTED_INITIATORS + self.zapi_client.get_lun_map.return_value = response + + (igroup, lun_id) = self.library._find_mapped_lun_igroup('path', + initiators) + + self.assertIsNone(igroup) + self.assertIsNone(lun_id) + + def test_find_mapped_lun_igroup_no_igroups(self): + response = netapp_api.NaElement(etree.XML(""" + + + """)) + initiators = fake.FC_FORMATTED_INITIATORS + self.zapi_client.get_lun_map.return_value = response + + (igroup, lun_id) = self.library._find_mapped_lun_igroup('path', + initiators) + + self.assertIsNone(igroup) + self.assertIsNone(lun_id) + + def test_find_mapped_lun_igroup_raises(self): + self.zapi_client.get_lun_map.side_effect = NaApiError + initiators = fake.FC_FORMATTED_INITIATORS + self.assertRaises(NaApiError, + self.library._find_mapped_lun_igroup, + 'path', + initiators) + + def test_has_luns_mapped_to_initiators_local_map(self): + initiator_list = fake.FC_FORMATTED_INITIATORS + self.zapi_client.has_luns_mapped_to_initiators.return_value = True + self.library.partner_zapi_client = mock.Mock() + + result = self.library._has_luns_mapped_to_initiators(initiator_list) + + self.assertTrue(result) + self.zapi_client.has_luns_mapped_to_initiators.assert_called_once_with( + initiator_list) + self.assertEqual(0, self.library.partner_zapi_client. + has_luns_mapped_to_initiators.call_count) + + def test_has_luns_mapped_to_initiators_partner_map(self): + initiator_list = fake.FC_FORMATTED_INITIATORS + self.zapi_client.has_luns_mapped_to_initiators.return_value = False + self.library.partner_zapi_client = mock.Mock() + self.library.partner_zapi_client.has_luns_mapped_to_initiators.\ + return_value = True + + result = self.library._has_luns_mapped_to_initiators(initiator_list) + + self.assertTrue(result) + self.zapi_client.has_luns_mapped_to_initiators.assert_called_once_with( + initiator_list) + self.library.partner_zapi_client.has_luns_mapped_to_initiators.\ + assert_called_with(initiator_list) + + def test_has_luns_mapped_to_initiators_no_maps(self): + initiator_list = fake.FC_FORMATTED_INITIATORS + self.zapi_client.has_luns_mapped_to_initiators.return_value = False + self.library.partner_zapi_client = mock.Mock() + self.library.partner_zapi_client.has_luns_mapped_to_initiators.\ + return_value = False + + result = self.library._has_luns_mapped_to_initiators(initiator_list) + + self.assertFalse(result) + self.zapi_client.has_luns_mapped_to_initiators.assert_called_once_with( + initiator_list) + self.library.partner_zapi_client.has_luns_mapped_to_initiators.\ + assert_called_with(initiator_list) + + def test_has_luns_mapped_to_initiators_no_partner(self): + initiator_list = fake.FC_FORMATTED_INITIATORS + self.zapi_client.has_luns_mapped_to_initiators.return_value = False + self.library.partner_zapi_client = mock.Mock() + self.library.partner_zapi_client.has_luns_mapped_to_initiators.\ + return_value = True + + result = self.library._has_luns_mapped_to_initiators( + initiator_list, include_partner=False) + + self.assertFalse(result) + self.zapi_client.has_luns_mapped_to_initiators.assert_called_once_with( + initiator_list) + self.assertEqual(0, self.library.partner_zapi_client. + has_luns_mapped_to_initiators.call_count) + def test_clone_lun_zero_block_count(self): """Test for when clone lun is not passed a block count.""" @@ -88,22 +304,51 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase): '/vol/fake/fakeLUN', '/vol/fake/newFakeLUN', 'fakeLUN', 'newFakeLUN', 'true', block_count=0, dest_block=0, src_block=0) + def test_get_fc_target_wwpns(self): + ports1 = [fake.FC_FORMATTED_TARGET_WWPNS[0], + fake.FC_FORMATTED_TARGET_WWPNS[1]] + ports2 = [fake.FC_FORMATTED_TARGET_WWPNS[2], + fake.FC_FORMATTED_TARGET_WWPNS[3]] + self.zapi_client.get_fc_target_wwpns.return_value = ports1 + self.library.partner_zapi_client = mock.Mock() + self.library.partner_zapi_client.get_fc_target_wwpns.return_value = \ + ports2 + + result = self.library._get_fc_target_wwpns() + + self.assertSetEqual(set(fake.FC_FORMATTED_TARGET_WWPNS), set(result)) + + def test_get_fc_target_wwpns_no_partner(self): + ports1 = [fake.FC_FORMATTED_TARGET_WWPNS[0], + fake.FC_FORMATTED_TARGET_WWPNS[1]] + ports2 = [fake.FC_FORMATTED_TARGET_WWPNS[2], + fake.FC_FORMATTED_TARGET_WWPNS[3]] + self.zapi_client.get_fc_target_wwpns.return_value = ports1 + self.library.partner_zapi_client = mock.Mock() + self.library.partner_zapi_client.get_fc_target_wwpns.return_value = \ + ports2 + + result = self.library._get_fc_target_wwpns(include_partner=False) + + self.assertSetEqual(set(ports1), set(result)) + @mock.patch.object(block_7mode.NetAppBlockStorage7modeLibrary, '_refresh_volume_info', mock.Mock()) @mock.patch.object(block_7mode.NetAppBlockStorage7modeLibrary, '_get_pool_stats', mock.Mock()) def test_vol_stats_calls_provide_ems(self): self.library.zapi_client.provide_ems = mock.Mock() + self.library.get_volume_stats(refresh=True) + self.assertEqual(self.library.zapi_client.provide_ems.call_count, 1) def test_create_lun(self): self.library.vol_refresh_voluntary = False - self.library._create_lun(FAKE_VOLUME, FAKE_LUN, FAKE_SIZE, - FAKE_METADATA) + self.library._create_lun(fake.VOLUME, fake.LUN, + fake.SIZE, fake.METADATA) self.library.zapi_client.create_lun.assert_called_once_with( - FAKE_VOLUME, FAKE_LUN, FAKE_SIZE, FAKE_METADATA, None) - + fake.VOLUME, fake.LUN, fake.SIZE, fake.METADATA, None) self.assertTrue(self.library.vol_refresh_voluntary) diff --git a/cinder/tests/volume/drivers/netapp/dataontap/test_block_base.py b/cinder/tests/volume/drivers/netapp/dataontap/test_block_base.py index 62787837a..392db4315 100644 --- a/cinder/tests/volume/drivers/netapp/dataontap/test_block_base.py +++ b/cinder/tests/volume/drivers/netapp/dataontap/test_block_base.py @@ -1,6 +1,5 @@ # Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2014 Clinton Knight. All rights reserved. -# 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 @@ -17,13 +16,18 @@ Mock unit tests for the NetApp block storage library """ + import uuid import mock from cinder import exception from cinder import test +from cinder.tests.volume.drivers.netapp.dataontap import fakes as fake from cinder.volume.drivers.netapp.dataontap import block_base +from cinder.volume.drivers.netapp.dataontap.block_base import \ + NetAppBlockStorageLibrary as block_lib +from cinder.volume.drivers.netapp.dataontap.client.api import NaApiError from cinder.volume.drivers.netapp import utils as na_utils @@ -33,52 +37,45 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase): super(NetAppBlockStorageLibraryTestCase, self).setUp() kwargs = {'configuration': mock.Mock()} - self.library = block_base.NetAppBlockStorageLibrary('driver', - 'protocol', - **kwargs) + self.library = block_lib('driver', 'protocol', **kwargs) self.library.zapi_client = mock.Mock() + self.zapi_client = self.library.zapi_client self.mock_request = mock.Mock() def tearDown(self): super(NetAppBlockStorageLibraryTestCase, self).tearDown() - @mock.patch.object(block_base.NetAppBlockStorageLibrary, '_get_lun_attr', + @mock.patch.object(block_lib, '_get_lun_attr', mock.Mock(return_value={'Volume': 'vol1'})) def test_get_pool(self): pool = self.library.get_pool({'name': 'volume-fake-uuid'}) self.assertEqual(pool, 'vol1') - @mock.patch.object(block_base.NetAppBlockStorageLibrary, '_get_lun_attr', + @mock.patch.object(block_lib, '_get_lun_attr', mock.Mock(return_value=None)) def test_get_pool_no_metadata(self): pool = self.library.get_pool({'name': 'volume-fake-uuid'}) self.assertEqual(pool, None) - @mock.patch.object(block_base.NetAppBlockStorageLibrary, '_get_lun_attr', + @mock.patch.object(block_lib, '_get_lun_attr', mock.Mock(return_value=dict())) def test_get_pool_volume_unknown(self): pool = self.library.get_pool({'name': 'volume-fake-uuid'}) self.assertEqual(pool, None) - @mock.patch.object(block_base.NetAppBlockStorageLibrary, '_create_lun', - mock.Mock()) - @mock.patch.object(block_base.NetAppBlockStorageLibrary, - '_create_lun_handle', - mock.Mock()) - @mock.patch.object(block_base.NetAppBlockStorageLibrary, - '_add_lun_to_table', - mock.Mock()) + @mock.patch.object(block_lib, '_create_lun', mock.Mock()) + @mock.patch.object(block_lib, '_create_lun_handle', mock.Mock()) + @mock.patch.object(block_lib, '_add_lun_to_table', mock.Mock()) @mock.patch.object(na_utils, 'get_volume_extra_specs', mock.Mock(return_value=None)) - @mock.patch.object(block_base, 'LOG', - mock.Mock()) + @mock.patch.object(block_base, 'LOG', mock.Mock()) def test_create_volume(self): self.library.create_volume({'name': 'lun1', 'size': 100, 'id': uuid.uuid4(), 'host': 'hostname@backend#vol1'}) self.library._create_lun.assert_called_once_with( 'vol1', 'lun1', 107374182400, mock.ANY, None) - self.assertEqual(0, block_base.LOG.warn.call_count) + self.assertEqual(0, block_base.LOG.warning.call_count) def test_create_volume_no_pool_provided_by_scheduler(self): self.assertRaises(exception.InvalidHost, self.library.create_volume, @@ -86,12 +83,206 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase): 'id': uuid.uuid4(), 'host': 'hostname@backend'}) # missing pool - @mock.patch.object(block_base.NetAppBlockStorageLibrary, - '_create_lun', mock.Mock()) - @mock.patch.object(block_base.NetAppBlockStorageLibrary, - '_create_lun_handle', mock.Mock()) - @mock.patch.object(block_base.NetAppBlockStorageLibrary, - '_add_lun_to_table', mock.Mock()) + @mock.patch.object(block_lib, '_get_lun_attr') + @mock.patch.object(block_lib, '_get_or_create_igroup') + def test_map_lun(self, mock_get_or_create_igroup, mock_get_lun_attr): + os = 'linux' + protocol = 'fcp' + mock_get_lun_attr.return_value = {'Path': fake.LUN1, 'OsType': os} + mock_get_or_create_igroup.return_value = fake.IGROUP1_NAME + self.zapi_client.map_lun.return_value = '1' + + lun_id = self.library._map_lun('fake_volume', + fake.FC_FORMATTED_INITIATORS, + protocol, None) + + self.assertEqual(lun_id, '1') + mock_get_or_create_igroup.assert_called_once_with( + fake.FC_FORMATTED_INITIATORS, protocol, os) + self.zapi_client.map_lun.assert_called_once_with( + fake.LUN1, fake.IGROUP1_NAME, lun_id=None) + + @mock.patch.object(block_lib, '_get_lun_attr') + @mock.patch.object(block_lib, '_get_or_create_igroup') + @mock.patch.object(block_lib, '_find_mapped_lun_igroup') + def test_map_lun_preexisting(self, mock_find_mapped_lun_igroup, + mock_get_or_create_igroup, mock_get_lun_attr): + os = 'linux' + protocol = 'fcp' + mock_get_lun_attr.return_value = {'Path': fake.LUN1, 'OsType': os} + mock_get_or_create_igroup.return_value = fake.IGROUP1_NAME + mock_find_mapped_lun_igroup.return_value = (fake.IGROUP1_NAME, '2') + self.zapi_client.map_lun.side_effect = NaApiError + + lun_id = self.library._map_lun( + 'fake_volume', fake.FC_FORMATTED_INITIATORS, protocol, None) + + self.assertEqual(lun_id, '2') + mock_find_mapped_lun_igroup.assert_called_once_with( + fake.LUN1, fake.FC_FORMATTED_INITIATORS) + + @mock.patch.object(block_lib, '_get_lun_attr') + @mock.patch.object(block_lib, '_get_or_create_igroup') + @mock.patch.object(block_lib, '_find_mapped_lun_igroup') + def test_map_lun_api_error(self, mock_find_mapped_lun_igroup, + mock_get_or_create_igroup, mock_get_lun_attr): + os = 'linux' + protocol = 'fcp' + mock_get_lun_attr.return_value = {'Path': fake.LUN1, 'OsType': os} + mock_get_or_create_igroup.return_value = fake.IGROUP1_NAME + mock_find_mapped_lun_igroup.return_value = (None, None) + self.zapi_client.map_lun.side_effect = NaApiError + + self.assertRaises(NaApiError, self.library._map_lun, 'fake_volume', + fake.FC_FORMATTED_INITIATORS, protocol, None) + + @mock.patch.object(block_lib, '_find_mapped_lun_igroup') + def test_unmap_lun(self, mock_find_mapped_lun_igroup): + mock_find_mapped_lun_igroup.return_value = (fake.IGROUP1_NAME, 1) + + self.library._unmap_lun(fake.LUN1, fake.FC_FORMATTED_INITIATORS) + + self.zapi_client.unmap_lun.assert_called_once_with(fake.LUN1, + fake.IGROUP1_NAME) + + def test_find_mapped_lun_igroup(self): + self.assertRaises(NotImplementedError, + self.library._find_mapped_lun_igroup, + fake.LUN1, + fake.FC_FORMATTED_INITIATORS) + + def test_has_luns_mapped_to_initiators(self): + self.zapi_client.has_luns_mapped_to_initiators.return_value = True + self.assertTrue(self.library._has_luns_mapped_to_initiators( + fake.FC_FORMATTED_INITIATORS)) + self.zapi_client.has_luns_mapped_to_initiators.assert_called_once_with( + fake.FC_FORMATTED_INITIATORS) + + def test_get_or_create_igroup_preexisting(self): + self.zapi_client.get_igroup_by_initiators.return_value = [fake.IGROUP1] + + igroup_name = self.library._get_or_create_igroup( + fake.FC_FORMATTED_INITIATORS, 'fcp', 'linux') + + self.assertEqual(igroup_name, fake.IGROUP1_NAME) + self.zapi_client.get_igroup_by_initiators.assert_called_once_with( + fake.FC_FORMATTED_INITIATORS) + + @mock.patch.object(uuid, 'uuid4', mock.Mock(return_value=fake.UUID1)) + def test_get_or_create_igroup_none_preexisting(self): + self.zapi_client.get_igroup_by_initiators.return_value = [] + + igroup_name = self.library._get_or_create_igroup( + fake.FC_FORMATTED_INITIATORS, 'fcp', 'linux') + + self.assertEqual(igroup_name, 'openstack-' + fake.UUID1) + self.zapi_client.create_igroup.assert_called_once_with( + igroup_name, 'fcp', 'linux') + self.assertEqual(len(fake.FC_FORMATTED_INITIATORS), + self.zapi_client.add_igroup_initiator.call_count) + + def test_get_fc_target_wwpns(self): + self.assertRaises(NotImplementedError, + self.library._get_fc_target_wwpns) + + @mock.patch.object(block_lib, '_build_initiator_target_map') + @mock.patch.object(block_lib, '_map_lun') + def test_initialize_connection_fc(self, mock_map_lun, + mock_build_initiator_target_map): + self.maxDiff = None + mock_map_lun.return_value = '1' + mock_build_initiator_target_map.return_value = (fake.FC_TARGET_WWPNS, + fake.FC_I_T_MAP, 4) + + target_info = self.library.initialize_connection_fc(fake.FC_VOLUME, + fake.FC_CONNECTOR) + + self.assertDictEqual(target_info, fake.FC_TARGET_INFO) + mock_map_lun.assert_called_once_with( + 'fake_volume', fake.FC_FORMATTED_INITIATORS, 'fcp', None) + + @mock.patch.object(block_lib, '_build_initiator_target_map') + @mock.patch.object(block_lib, '_map_lun') + def test_initialize_connection_fc_no_wwpns( + self, mock_map_lun, mock_build_initiator_target_map): + + mock_map_lun.return_value = '1' + mock_build_initiator_target_map.return_value = (None, None, 0) + self.assertRaises(exception.VolumeBackendAPIException, + self.library.initialize_connection_fc, + fake.FC_VOLUME, + fake.FC_CONNECTOR) + + @mock.patch.object(block_lib, '_has_luns_mapped_to_initiators') + @mock.patch.object(block_lib, '_unmap_lun') + @mock.patch.object(block_lib, '_get_lun_attr') + def test_terminate_connection_fc(self, mock_get_lun_attr, mock_unmap_lun, + mock_has_luns_mapped_to_initiators): + + mock_get_lun_attr.return_value = {'Path': fake.LUN1} + mock_unmap_lun.return_value = None + mock_has_luns_mapped_to_initiators.return_value = True + + target_info = self.library.terminate_connection_fc(fake.FC_VOLUME, + fake.FC_CONNECTOR) + + self.assertDictEqual(target_info, fake.FC_TARGET_INFO_EMPTY) + mock_unmap_lun.assert_called_once_with(fake.LUN1, + fake.FC_FORMATTED_INITIATORS) + + @mock.patch.object(block_lib, '_build_initiator_target_map') + @mock.patch.object(block_lib, '_has_luns_mapped_to_initiators') + @mock.patch.object(block_lib, '_unmap_lun') + @mock.patch.object(block_lib, '_get_lun_attr') + def test_terminate_connection_fc_no_more_luns( + self, mock_get_lun_attr, mock_unmap_lun, + mock_has_luns_mapped_to_initiators, + mock_build_initiator_target_map): + + mock_get_lun_attr.return_value = {'Path': fake.LUN1} + mock_unmap_lun.return_value = None + mock_has_luns_mapped_to_initiators.return_value = False + mock_build_initiator_target_map.return_value = (fake.FC_TARGET_WWPNS, + fake.FC_I_T_MAP, 4) + + target_info = self.library.terminate_connection_fc(fake.FC_VOLUME, + fake.FC_CONNECTOR) + + self.assertDictEqual(target_info, fake.FC_TARGET_INFO_UNMAP) + + @mock.patch.object(block_lib, '_get_fc_target_wwpns') + def test_build_initiator_target_map_no_lookup_service( + self, mock_get_fc_target_wwpns): + + self.library.lookup_service = None + mock_get_fc_target_wwpns.return_value = fake.FC_FORMATTED_TARGET_WWPNS + + (target_wwpns, init_targ_map, num_paths) = \ + self.library._build_initiator_target_map(fake.FC_CONNECTOR) + + self.assertSetEqual(set(fake.FC_TARGET_WWPNS), set(target_wwpns)) + self.assertDictEqual(fake.FC_I_T_MAP_COMPLETE, init_targ_map) + self.assertEqual(0, num_paths) + + @mock.patch.object(block_lib, '_get_fc_target_wwpns') + def test_build_initiator_target_map_with_lookup_service( + self, mock_get_fc_target_wwpns): + + self.library.lookup_service = mock.Mock() + self.library.lookup_service.get_device_mapping_from_network.\ + return_value = fake.FC_FABRIC_MAP + mock_get_fc_target_wwpns.return_value = fake.FC_FORMATTED_TARGET_WWPNS + + (target_wwpns, init_targ_map, num_paths) = \ + self.library._build_initiator_target_map(fake.FC_CONNECTOR) + + self.assertSetEqual(set(fake.FC_TARGET_WWPNS), set(target_wwpns)) + self.assertDictEqual(fake.FC_I_T_MAP, init_targ_map) + self.assertEqual(4, num_paths) + + @mock.patch.object(block_lib, '_create_lun', mock.Mock()) + @mock.patch.object(block_lib, '_create_lun_handle', mock.Mock()) + @mock.patch.object(block_lib, '_add_lun_to_table', mock.Mock()) @mock.patch.object(na_utils, 'LOG', mock.Mock()) @mock.patch.object(na_utils, 'get_volume_extra_specs', mock.Mock(return_value={'netapp:raid_type': 'raid4'})) @@ -100,16 +291,14 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase): self.library.create_volume({'name': 'lun1', 'size': 100, 'id': uuid.uuid4(), 'host': 'hostname@backend#vol1'}) + warn_msg = 'Extra spec netapp:raid_type is obsolete. ' \ 'Use netapp_raid_type instead.' - na_utils.LOG.warn.assert_called_once_with(warn_msg) - - @mock.patch.object(block_base.NetAppBlockStorageLibrary, - '_create_lun', mock.Mock()) - @mock.patch.object(block_base.NetAppBlockStorageLibrary, - '_create_lun_handle', mock.Mock()) - @mock.patch.object(block_base.NetAppBlockStorageLibrary, - '_add_lun_to_table', mock.Mock()) + na_utils.LOG.warning.assert_called_once_with(warn_msg) + + @mock.patch.object(block_lib, '_create_lun', mock.Mock()) + @mock.patch.object(block_lib, '_create_lun_handle', mock.Mock()) + @mock.patch.object(block_lib, '_add_lun_to_table', mock.Mock()) @mock.patch.object(na_utils, 'LOG', mock.Mock()) @mock.patch.object(na_utils, 'get_volume_extra_specs', mock.Mock(return_value={'netapp_thick_provisioned': @@ -119,6 +308,7 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase): self.library.create_volume({'name': 'lun1', 'size': 100, 'id': uuid.uuid4(), 'host': 'hostname@backend#vol1'}) + warn_msg = 'Extra spec netapp_thick_provisioned is deprecated. ' \ 'Use netapp_thin_provisioned instead.' - na_utils.LOG.warn.assert_called_once_with(warn_msg) + na_utils.LOG.warning.assert_called_once_with(warn_msg) diff --git a/cinder/tests/volume/drivers/netapp/dataontap/test_block_cmode.py b/cinder/tests/volume/drivers/netapp/dataontap/test_block_cmode.py index 39f29648f..8cf19a549 100644 --- a/cinder/tests/volume/drivers/netapp/dataontap/test_block_cmode.py +++ b/cinder/tests/volume/drivers/netapp/dataontap/test_block_cmode.py @@ -1,6 +1,5 @@ # Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2014 Clinton Knight. All rights reserved. -# 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 @@ -17,20 +16,21 @@ Mock unit tests for the NetApp block storage C-mode library """ -import uuid import mock -import six from cinder import test +import cinder.tests.volume.drivers.netapp.dataontap.fakes as fake +import cinder.tests.volume.drivers.netapp.fakes as na_fakes +from cinder.volume.drivers.netapp.dataontap.block_base import \ + NetAppBlockStorageLibrary as block_lib from cinder.volume.drivers.netapp.dataontap import block_cmode +from cinder.volume.drivers.netapp.dataontap.block_cmode import \ + NetAppBlockStorageCmodeLibrary as block_lib_cmode from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api +from cinder.volume.drivers.netapp.dataontap.client import client_base from cinder.volume.drivers.netapp.dataontap import ssc_cmode - -FAKE_VOLUME = six.text_type(uuid.uuid4()) -FAKE_LUN = six.text_type(uuid.uuid4()) -FAKE_SIZE = '1024' -FAKE_METADATA = {'OsType': 'linux', 'SpaceReserved': 'true'} +from cinder.volume.drivers.netapp import utils as na_utils class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): @@ -39,17 +39,114 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): def setUp(self): super(NetAppBlockStorageCmodeLibraryTestCase, self).setUp() - kwargs = {'configuration': mock.Mock()} - self.library = block_cmode.NetAppBlockStorageCmodeLibrary('driver', - 'protocol', - **kwargs) + kwargs = {'configuration': self.get_config_cmode()} + self.library = block_lib_cmode('driver', 'protocol', **kwargs) + self.library.zapi_client = mock.Mock() + self.zapi_client = self.library.zapi_client self.library.vserver = mock.Mock() self.library.ssc_vols = None def tearDown(self): super(NetAppBlockStorageCmodeLibraryTestCase, self).tearDown() + def get_config_cmode(self): + config = na_fakes.create_configuration_cmode() + config.netapp_storage_protocol = 'iscsi' + config.netapp_login = 'admin' + config.netapp_password = 'pass' + config.netapp_server_hostname = '127.0.0.1' + config.netapp_transport_type = 'https' + config.netapp_server_port = '443' + config.netapp_vserver = 'openstack' + return config + + @mock.patch.object(client_base.Client, 'get_ontapi_version', + mock.MagicMock(return_value=(1, 20))) + @mock.patch.object(na_utils, 'check_flags') + @mock.patch.object(block_lib, 'do_setup') + def test_do_setup(self, super_do_setup, mock_check_flags): + context = mock.Mock() + + self.library.do_setup(context) + + super_do_setup.assert_called_once_with(context) + self.assertEqual(1, mock_check_flags.call_count) + + @mock.patch.object(block_lib, 'check_for_setup_error') + @mock.patch.object(ssc_cmode, 'check_ssc_api_permissions') + def test_check_for_setup_error(self, mock_check_ssc_api_permissions, + super_check_for_setup_error): + + self.library.check_for_setup_error() + + super_check_for_setup_error.assert_called_once_with() + mock_check_ssc_api_permissions.assert_called_once_with( + self.library.zapi_client) + + def test_find_mapped_lun_igroup(self): + igroups = [fake.IGROUP1] + self.zapi_client.get_igroup_by_initiators.return_value = igroups + + lun_maps = [{'initiator-group': fake.IGROUP1_NAME, + 'lun-id': '1', + 'vserver': fake.VSERVER1_NAME}] + self.zapi_client.get_lun_map.return_value = lun_maps + + (igroup, lun_id) = self.library._find_mapped_lun_igroup( + fake.LUN1, fake.FC_FORMATTED_INITIATORS) + + self.assertEqual(fake.IGROUP1_NAME, igroup) + self.assertEqual('1', lun_id) + + def test_find_mapped_lun_igroup_initiator_mismatch(self): + self.zapi_client.get_igroup_by_initiators.return_value = [] + + lun_maps = [{'initiator-group': fake.IGROUP1_NAME, + 'lun-id': '1', + 'vserver': fake.VSERVER1_NAME}] + self.zapi_client.get_lun_map.return_value = lun_maps + + (igroup, lun_id) = self.library._find_mapped_lun_igroup( + fake.LUN1, fake.FC_FORMATTED_INITIATORS) + + self.assertIsNone(igroup) + self.assertIsNone(lun_id) + + def test_find_mapped_lun_igroup_name_mismatch(self): + igroups = [{'initiator-group-os-type': 'linux', + 'initiator-group-type': 'fcp', + 'initiator-group-name': 'igroup2'}] + self.zapi_client.get_igroup_by_initiators.return_value = igroups + + lun_maps = [{'initiator-group': fake.IGROUP1_NAME, + 'lun-id': '1', + 'vserver': fake.VSERVER1_NAME}] + self.zapi_client.get_lun_map.return_value = lun_maps + + (igroup, lun_id) = self.library._find_mapped_lun_igroup( + fake.LUN1, fake.FC_FORMATTED_INITIATORS) + + self.assertIsNone(igroup) + self.assertIsNone(lun_id) + + def test_find_mapped_lun_igroup_no_igroup_prefix(self): + igroups = [{'initiator-group-os-type': 'linux', + 'initiator-group-type': 'fcp', + 'initiator-group-name': 'igroup2'}] + self.zapi_client.get_igroup_by_initiators.return_value = igroups + + lun_maps = [{'initiator-group': 'igroup2', + 'lun-id': '1', + 'vserver': fake.VSERVER1_NAME}] + self.zapi_client.get_lun_map.return_value = lun_maps + + (igroup, lun_id) = self.library._find_mapped_lun_igroup( + fake.LUN1, fake.FC_FORMATTED_INITIATORS) + + self.assertIsNone(igroup) + self.assertIsNone(lun_id) + def test_clone_lun_zero_block_count(self): """Test for when clone lun is not passed a block count.""" @@ -92,24 +189,31 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): 'fakeLUN', 'fakeLUN', 'newFakeLUN', 'true', block_count=0, dest_block=0, src_block=0) + def test_get_fc_target_wwpns(self): + ports = [fake.FC_FORMATTED_TARGET_WWPNS[0], + fake.FC_FORMATTED_TARGET_WWPNS[1]] + self.zapi_client.get_fc_target_wwpns.return_value = ports + + result = self.library._get_fc_target_wwpns() + + self.assertSetEqual(set(ports), set(result)) + @mock.patch.object(ssc_cmode, 'refresh_cluster_ssc', mock.Mock()) @mock.patch.object(block_cmode.NetAppBlockStorageCmodeLibrary, '_get_pool_stats', mock.Mock()) def test_vol_stats_calls_provide_ems(self): self.library.zapi_client.provide_ems = mock.Mock() + self.library.get_volume_stats(refresh=True) + self.assertEqual(self.library.zapi_client.provide_ems.call_count, 1) def test_create_lun(self): self.library._update_stale_vols = mock.Mock() - self.library._create_lun(FAKE_VOLUME, - FAKE_LUN, - FAKE_SIZE, - FAKE_METADATA) + self.library._create_lun(fake.VOLUME, fake.LUN, + fake.SIZE, fake.METADATA) self.library.zapi_client.create_lun.assert_called_once_with( - FAKE_VOLUME, FAKE_LUN, FAKE_SIZE, - FAKE_METADATA, None) - + fake.VOLUME, fake.LUN, fake.SIZE, fake.METADATA, None) self.assertEqual(1, self.library._update_stale_vols.call_count) diff --git a/cinder/tests/volume/drivers/netapp/eseries/__init__.py b/cinder/tests/volume/drivers/netapp/eseries/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/tests/volume/drivers/netapp/fakes.py b/cinder/tests/volume/drivers/netapp/fakes.py new file mode 100644 index 000000000..d39a52d83 --- /dev/null +++ b/cinder/tests/volume/drivers/netapp/fakes.py @@ -0,0 +1,44 @@ +# Copyright (c) - 2014, Clinton Knight. 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. + + +from cinder.volume import configuration as conf +import cinder.volume.drivers.netapp.options as na_opts + + +def create_configuration(): + config = conf.Configuration(None) + config.append_config_values(na_opts.netapp_connection_opts) + config.append_config_values(na_opts.netapp_transport_opts) + config.append_config_values(na_opts.netapp_basicauth_opts) + config.append_config_values(na_opts.netapp_provisioning_opts) + return config + + +def create_configuration_7mode(): + config = create_configuration() + config.append_config_values(na_opts.netapp_7mode_opts) + return config + + +def create_configuration_cmode(): + config = create_configuration() + config.append_config_values(na_opts.netapp_cluster_opts) + return config + + +def create_configuration_eseries(): + config = create_configuration() + config.append_config_values(na_opts.netapp_eseries_opts) + return config diff --git a/cinder/volume/drivers/netapp/common.py b/cinder/volume/drivers/netapp/common.py index 36e2b7ef2..70415b73d 100644 --- a/cinder/volume/drivers/netapp/common.py +++ b/cinder/volume/drivers/netapp/common.py @@ -42,12 +42,14 @@ netapp_unified_plugin_registry =\ {'ontap_cluster': { 'iscsi': DATAONTAP_PATH + '.iscsi_cmode.NetAppCmodeISCSIDriver', - 'nfs': DATAONTAP_PATH + '.nfs_cmode.NetAppCmodeNfsDriver' + 'nfs': DATAONTAP_PATH + '.nfs_cmode.NetAppCmodeNfsDriver', + 'fc': DATAONTAP_PATH + '.fc_cmode.NetAppCmodeFibreChannelDriver' }, 'ontap_7mode': { 'iscsi': DATAONTAP_PATH + '.iscsi_7mode.NetApp7modeISCSIDriver', - 'nfs': DATAONTAP_PATH + '.nfs_7mode.NetApp7modeNfsDriver' + 'nfs': DATAONTAP_PATH + '.nfs_7mode.NetApp7modeNfsDriver', + 'fc': DATAONTAP_PATH + '.fc_7mode.NetApp7modeFibreChannelDriver' }, 'eseries': { @@ -123,8 +125,7 @@ class NetAppDriverFactory(object): if driver_loc is None: raise exception.InvalidInput( reason=_('Protocol %(storage_protocol)s is not supported' - ' for storage family %(storage_family)s') - % fmt) + ' for storage family %(storage_family)s') % fmt) NetAppDriverFactory.check_netapp_driver(driver_loc) kwargs = kwargs or {} diff --git a/cinder/volume/drivers/netapp/dataontap/block_7mode.py b/cinder/volume/drivers/netapp/dataontap/block_7mode.py index a83776f62..bea12f382 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_7mode.py +++ b/cinder/volume/drivers/netapp/dataontap/block_7mode.py @@ -28,6 +28,7 @@ import six from cinder import exception from cinder.i18n import _, _LW from cinder.openstack.common import log as logging +from cinder.volume.configuration import Configuration from cinder.volume.drivers.netapp.dataontap import block_base from cinder.volume.drivers.netapp.dataontap.client import client_7mode from cinder.volume.drivers.netapp import options as na_opts @@ -67,12 +68,31 @@ class NetAppBlockStorage7modeLibrary(block_base. port=self.configuration.netapp_server_port, vfiler=self.vfiler) + self._do_partner_setup() + self.vol_refresh_time = None self.vol_refresh_interval = 1800 self.vol_refresh_running = False self.vol_refresh_voluntary = False self.root_volume_name = self._get_root_volume_name() + def _do_partner_setup(self): + partner_backend = self.configuration.netapp_partner_backend_name + if partner_backend: + config = Configuration(na_opts.netapp_7mode_opts, partner_backend) + config.append_config_values(na_opts.netapp_connection_opts) + config.append_config_values(na_opts.netapp_basicauth_opts) + config.append_config_values(na_opts.netapp_transport_opts) + + self.partner_zapi_client = client_7mode.Client( + None, + transport_type=config.netapp_transport_type, + username=config.netapp_login, + password=config.netapp_password, + hostname=config.netapp_server_hostname, + port=config.netapp_server_port, + vfiler=None) + def check_for_setup_error(self): """Check that the driver is working and can communicate.""" api_version = self.zapi_client.get_ontapi_version() @@ -120,27 +140,40 @@ class NetAppBlockStorage7modeLibrary(block_base. owner = self._get_owner() return '%s:%s' % (owner, metadata['Path']) - def _find_mapped_lun_igroup(self, path, initiator, os=None): - """Find the igroup for mapped LUN with initiator.""" - igroup = None - lun_id = None + def _find_mapped_lun_igroup(self, path, initiator_list): + """Find an igroup for a LUN mapped to the given initiator(s).""" + initiator_set = set(initiator_list) + result = self.zapi_client.get_lun_map(path) - igroups = result.get_child_by_name('initiator-groups') - if igroups: - found = False - igroup_infs = igroups.get_children() - for ig in igroup_infs: - initiators = ig.get_child_by_name('initiators') - init_infs = initiators.get_children() - for info in init_infs: - if info.get_child_content('initiator-name') == initiator: - found = True - igroup = ig.get_child_content('initiator-group-name') - lun_id = ig.get_child_content('lun-id') - break - if found: - break - return igroup, lun_id + initiator_groups = result.get_child_by_name('initiator-groups') + if initiator_groups: + for initiator_group_info in initiator_groups.get_children(): + + initiator_set_for_igroup = set() + for initiator_info in initiator_group_info.get_child_by_name( + 'initiators').get_children(): + initiator_set_for_igroup.add( + initiator_info.get_child_content('initiator-name')) + + if initiator_set == initiator_set_for_igroup: + igroup = initiator_group_info.get_child_content( + 'initiator-group-name') + lun_id = initiator_group_info.get_child_content( + 'lun-id') + return igroup, lun_id + + return None, None + + def _has_luns_mapped_to_initiators(self, initiator_list, + include_partner=True): + """Checks whether any LUNs are mapped to the given initiator(s).""" + if self.zapi_client.has_luns_mapped_to_initiators(initiator_list): + return True + if include_partner and self.partner_zapi_client and \ + self.partner_zapi_client.has_luns_mapped_to_initiators( + initiator_list): + return True + return False def _clone_lun(self, name, new_name, space_reserved='true', src_block=0, dest_block=0, block_count=0): @@ -176,6 +209,12 @@ class NetAppBlockStorage7modeLibrary(block_base. 'is-space-reservation-enabled') return meta_dict + def _get_fc_target_wwpns(self, include_partner=True): + wwpns = self.zapi_client.get_fc_target_wwpns() + if include_partner and self.partner_zapi_client: + wwpns.extend(self.partner_zapi_client.get_fc_target_wwpns()) + return wwpns + def _update_volume_stats(self): """Retrieve stats info from filer.""" diff --git a/cinder/volume/drivers/netapp/dataontap/block_base.py b/cinder/volume/drivers/netapp/dataontap/block_base.py index 5d2c7a2a7..ecbed6eb8 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_base.py +++ b/cinder/volume/drivers/netapp/dataontap/block_base.py @@ -35,6 +35,7 @@ from cinder.volume.drivers.netapp.dataontap.client.api import NaApiError 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__) @@ -81,6 +82,7 @@ class NetAppBlockStorageLibrary(object): self.zapi_client = None self._stats = {} self.lun_table = {} + self.lookup_service = fczm_utils.create_lookup_service() self.app_version = kwargs.get("app_version", "unknown") self.configuration = kwargs['configuration'] @@ -232,10 +234,7 @@ class NetAppBlockStorageLibrary(object): raise NotImplementedError() def _extract_and_populate_luns(self, api_luns): - """Extracts the LUNs from API. - - Populates in the LUN table. - """ + """Extracts the LUNs from API and populates the LUN table.""" for lun in api_luns: meta_dict = self._create_lun_meta(lun) @@ -246,8 +245,8 @@ class NetAppBlockStorageLibrary(object): discovered_lun = NetAppLun(handle, name, size, meta_dict) self._add_lun_to_table(discovered_lun) - def _map_lun(self, name, initiator, initiator_type='iscsi', lun_id=None): - """Maps LUN to the initiator and returns LUN id assigned.""" + def _map_lun(self, name, initiator_list, initiator_type, lun_id=None): + """Maps LUN to the initiator(s) and returns LUN ID assigned.""" metadata = self._get_lun_attr(name, 'metadata') os = metadata['OsType'] path = metadata['Path'] @@ -255,35 +254,42 @@ class NetAppBlockStorageLibrary(object): os = os else: os = 'default' - igroup_name = self._get_or_create_igroup(initiator, + igroup_name = self._get_or_create_igroup(initiator_list, initiator_type, os) try: return self.zapi_client.map_lun(path, igroup_name, lun_id=lun_id) except NaApiError: exc_info = sys.exc_info() - (_igroup, lun_id) = self._find_mapped_lun_igroup(path, initiator) + (_igroup, lun_id) = self._find_mapped_lun_igroup(path, + initiator_list) if lun_id is not None: return lun_id else: raise exc_info[0], exc_info[1], exc_info[2] - def _unmap_lun(self, path, initiator): + def _unmap_lun(self, path, initiator_list): """Unmaps a LUN from given initiator.""" - (igroup_name, _lun_id) = self._find_mapped_lun_igroup(path, initiator) + (igroup_name, _lun_id) = self._find_mapped_lun_igroup(path, + initiator_list) self.zapi_client.unmap_lun(path, igroup_name) - def _find_mapped_lun_igroup(self, path, initiator, os=None): - """Find the igroup for mapped LUN with initiator.""" + def _find_mapped_lun_igroup(self, path, initiator_list): + """Find an igroup for a LUN mapped to the given initiator(s).""" raise NotImplementedError() - def _get_or_create_igroup(self, initiator, initiator_type='iscsi', + def _has_luns_mapped_to_initiators(self, initiator_list): + """Checks whether any LUNs are mapped to the given initiator(s).""" + return self.zapi_client.has_luns_mapped_to_initiators(initiator_list) + + def _get_or_create_igroup(self, initiator_list, initiator_type, os='default'): - """Checks for an igroup for an initiator. + """Checks for an igroup for a set of one or more initiators. Creates igroup if not found. """ - igroups = self.zapi_client.get_igroup_by_initiator(initiator=initiator) + igroups = self.zapi_client.get_igroup_by_initiators(initiator_list) + igroup_name = None for igroup in igroups: if igroup['initiator-group-os-type'] == os: @@ -296,7 +302,8 @@ class NetAppBlockStorageLibrary(object): if not igroup_name: igroup_name = self.IGROUP_PREFIX + six.text_type(uuid.uuid4()) self.zapi_client.create_igroup(igroup_name, initiator_type, os) - self.zapi_client.add_igroup_initiator(igroup_name, initiator) + for initiator in initiator_list: + self.zapi_client.add_igroup_initiator(igroup_name, initiator) return igroup_name def _check_allowed_os(self, os): @@ -348,6 +355,9 @@ class NetAppBlockStorageLibrary(object): def _create_lun_meta(self, lun): raise NotImplementedError() + def _get_fc_target_wwpns(self, include_partner=True): + raise NotImplementedError() + def create_cloned_volume(self, volume, src_vref): """Creates a clone of the specified volume.""" vol_size = volume['size'] @@ -505,12 +515,12 @@ class NetAppBlockStorageLibrary(object): initiator_name = connector['initiator'] name = volume['name'] - lun_id = self._map_lun(name, initiator_name, 'iscsi', None) + lun_id = self._map_lun(name, [initiator_name], 'iscsi', None) msg = _("Mapped LUN %(name)s to the initiator %(initiator_name)s") msg_fmt = {'name': name, 'initiator_name': initiator_name} LOG.debug(msg % msg_fmt) iqn = self.zapi_client.get_iscsi_service_details() - target_details_list = self.zapi_client.get_target_details() + target_details_list = self.zapi_client.get_iscsi_target_details() msg = _("Successfully fetched target details for LUN %(name)s and " "initiator %(initiator_name)s") msg_fmt = {'name': name, 'initiator_name': initiator_name} @@ -565,7 +575,162 @@ class NetAppBlockStorageLibrary(object): name = volume['name'] metadata = self._get_lun_attr(name, 'metadata') path = metadata['Path'] - self._unmap_lun(path, initiator_name) + self._unmap_lun(path, [initiator_name]) msg = _("Unmapped LUN %(name)s from the initiator %(initiator_name)s") msg_fmt = {'name': name, 'initiator_name': initiator_name} LOG.debug(msg % msg_fmt) + + def initialize_connection_fc(self, volume, connector): + """Initializes the connection and returns connection info. + + Assign any created 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']] + volume_name = volume['name'] + + lun_id = self._map_lun(volume_name, initiators, 'fcp', None) + + msg = _("Mapped LUN %(name)s to the initiator(s) %(initiators)s") + msg_fmt = {'name': volume_name, 'initiators': initiators} + LOG.debug(msg % msg_fmt) + + target_wwpns, initiator_target_map, num_paths = \ + self._build_initiator_target_map(connector) + + if target_wwpns: + msg = _("Successfully fetched target details for LUN %(name)s " + "and initiator(s) %(initiators)s") + msg_fmt = {'name': volume_name, '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_name) + + target_info = {'driver_volume_type': 'fibre_channel', + 'data': {'target_discovered': True, + 'target_lun': 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 + """ + + initiators = [fczm_utils.get_formatted_wwn(wwpn) + for wwpn in connector['wwpns']] + name = volume['name'] + metadata = self._get_lun_attr(name, 'metadata') + path = metadata['Path'] + + self._unmap_lun(path, initiators) + + msg = _("Unmapped LUN %(name)s from the initiator %(initiators)s") + msg_fmt = {'name': name, 'initiators': initiators} + LOG.debug(msg % msg_fmt) + + info = {'driver_volume_type': 'fibre_channel', + 'data': {}} + + if not self._has_luns_mapped_to_initiators(initiators): + # No more exports for this host, so tear down zone. + LOG.info(_LI("Need to remove FC Zone, building initiator " + "target map")) + + target_wwpns, initiator_target_map, num_paths = \ + self._build_initiator_target_map(connector) + + info['data'] = {'target_wwn': target_wwpns, + 'initiator_target_map': initiator_target_map} + + return info + + def _build_initiator_target_map(self, connector): + """Build the target_wwns and the initiator target map.""" + + # get WWPNs from controller and strip colons + all_target_wwpns = self._get_fc_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 is not None: + # 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 diff --git a/cinder/volume/drivers/netapp/dataontap/block_cmode.py b/cinder/volume/drivers/netapp/dataontap/block_cmode.py index 774b4c0a7..8ec2e2213 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/block_cmode.py @@ -89,10 +89,10 @@ class NetAppBlockStorageCmodeLibrary(block_base. """Returns LUN handle based on filer type.""" return '%s:%s' % (self.vserver, metadata['Path']) - def _find_mapped_lun_igroup(self, path, initiator, os=None): - """Find the igroup for mapped LUN with initiator.""" - initiator_igroups = self.zapi_client.get_igroup_by_initiator( - initiator=initiator) + def _find_mapped_lun_igroup(self, path, initiator_list): + """Find an igroup for a LUN mapped to the given initiator(s).""" + initiator_igroups = self.zapi_client.get_igroup_by_initiators( + initiator_list) lun_maps = self.zapi_client.get_lun_map(path) if initiator_igroups and lun_maps: for igroup in initiator_igroups: @@ -140,6 +140,9 @@ class NetAppBlockStorageCmodeLibrary(block_base. lun.get_child_content('is-space-reservation-enabled') return meta_dict + def _get_fc_target_wwpns(self, include_partner=True): + return self.zapi_client.get_fc_target_wwpns() + def _configure_tunneling(self, do_tunneling=False): """Configures tunneling for Data ONTAP cluster.""" if do_tunneling: diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py b/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py index 6cbcd3919..207a1a0f7 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py @@ -48,8 +48,41 @@ class Client(client_base.Client): result = server.invoke_successfully(na_element, True) return result - def get_target_details(self): - """Gets the target portal details.""" + def _invoke_7mode_iterator_getter(self, start_api_name, next_api_name, + end_api_name, record_container_tag_name, + maximum=100): + """Invoke a 7-mode iterator-style getter API.""" + data = [] + + start_api = netapp_api.NaElement(start_api_name) + start_result = self.connection.invoke_successfully(start_api) + tag = start_result.get_child_content('tag') + if not tag: + return data + + try: + while True: + next_api = netapp_api.NaElement(next_api_name) + next_api.add_new_child('tag', tag) + next_api.add_new_child('maximum', six.text_type(maximum)) + next_result = self.connection.invoke_successfully(next_api) + records = next_result.get_child_content('records') or 0 + if int(records) == 0: + break + + record_container = next_result.get_child_by_name( + record_container_tag_name) or netapp_api.NaElement('none') + + data.extend(record_container.get_children()) + finally: + end_api = netapp_api.NaElement(end_api_name) + end_api.add_new_child('tag', tag) + self.connection.invoke_successfully(end_api) + + return data + + def get_iscsi_target_details(self): + """Gets the iSCSI target portal details.""" iscsi_if_iter = netapp_api.NaElement('iscsi-portal-list-info') result = self.connection.invoke_successfully(iscsi_if_iter, True) tgt_list = [] @@ -65,6 +98,18 @@ class Client(client_base.Client): tgt_list.append(d) return tgt_list + def get_fc_target_wwpns(self): + """Gets the FC target details.""" + wwpns = [] + port_name_list_api = netapp_api.NaElement('fcp-port-name-list-info') + result = self.connection.invoke_successfully(port_name_list_api) + port_names = result.get_child_by_name('fcp-port-names') + if port_names: + for port_name_info in port_names.get_children(): + wwpn = port_name_info.get_child_content('port-name').lower() + wwpns.append(wwpn) + return wwpns + def get_iscsi_service_details(self): """Returns iscsi iqn.""" iscsi_service_iter = netapp_api.NaElement('iscsi-node-get-name') @@ -97,34 +142,41 @@ class Client(client_base.Client): luns = result.get_child_by_name('luns') return luns.get_children() - def get_igroup_by_initiator(self, initiator): - """Get igroups by initiator.""" - igroup_list = netapp_api.NaElement('igroup-list-info') - result = self.connection.invoke_successfully(igroup_list, True) - igroups = [] - igs = result.get_child_by_name('initiator-groups') - if igs: - ig_infos = igs.get_children() - if ig_infos: - for info in ig_infos: - initiators = info.get_child_by_name('initiators') - init_infos = initiators.get_children() - if init_infos: - for init in init_infos: - if init.get_child_content('initiator-name')\ - == initiator: - d = dict() - d['initiator-group-os-type'] = \ - info.get_child_content( - 'initiator-group-os-type') - d['initiator-group-type'] = \ - info.get_child_content( - 'initiator-group-type') - d['initiator-group-name'] = \ - info.get_child_content( - 'initiator-group-name') - igroups.append(d) - return igroups + def get_igroup_by_initiators(self, initiator_list): + """Get igroups exactly matching a set of initiators.""" + igroup_list = [] + if not initiator_list: + return igroup_list + + initiator_set = set(initiator_list) + + igroup_list_info = netapp_api.NaElement('igroup-list-info') + result = self.connection.invoke_successfully(igroup_list_info, True) + + initiator_groups = result.get_child_by_name( + 'initiator-groups') or netapp_api.NaElement('none') + for initiator_group_info in initiator_groups.get_children(): + + initiator_set_for_igroup = set() + initiators = initiator_group_info.get_child_by_name( + 'initiators') or netapp_api.NaElement('none') + for initiator_info in initiators.get_children(): + initiator_set_for_igroup.add( + initiator_info.get_child_content('initiator-name')) + + if initiator_set == initiator_set_for_igroup: + igroup = {'initiator-group-os-type': + initiator_group_info.get_child_content( + 'initiator-group-os-type'), + 'initiator-group-type': + initiator_group_info.get_child_content( + 'initiator-group-type'), + 'initiator-group-name': + initiator_group_info.get_child_content( + 'initiator-group-name')} + igroup_list.append(igroup) + + return igroup_list def clone_lun(self, path, clone_path, name, new_name, space_reserved='true', src_block=0, diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_base.py b/cinder/volume/drivers/netapp/dataontap/client/client_base.py index 965bed865..09811e3f8 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_base.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_base.py @@ -207,8 +207,12 @@ class Client(object): lun_move.add_new_child("new-path", new_path) self.connection.invoke_successfully(lun_move, True) - def get_target_details(self): - """Gets the target portal details.""" + def get_iscsi_target_details(self): + """Gets the iSCSI target portal details.""" + raise NotImplementedError() + + def get_fc_target_wwpns(self): + """Gets the FC target details.""" raise NotImplementedError() def get_iscsi_service_details(self): @@ -219,10 +223,26 @@ class Client(object): """Gets the list of LUNs on filer.""" raise NotImplementedError() - def get_igroup_by_initiator(self, initiator): - """Get igroups by initiator.""" + def get_igroup_by_initiators(self, initiator_list): + """Get igroups exactly matching a set of initiators.""" raise NotImplementedError() + def _has_luns_mapped_to_initiator(self, initiator): + """Checks whether any LUNs are mapped to the given initiator.""" + lun_list_api = netapp_api.NaElement('lun-initiator-list-map-info') + lun_list_api.add_new_child('initiator', initiator) + result = self.connection.invoke_successfully(lun_list_api, True) + lun_maps_container = result.get_child_by_name( + 'lun-maps') or netapp_api.NaElement('none') + return len(lun_maps_container.get_children()) > 0 + + def has_luns_mapped_to_initiators(self, initiator_list): + """Checks whether any LUNs are mapped to the given initiator(s).""" + for initiator in initiator_list: + if self._has_luns_mapped_to_initiator(initiator): + return True + return False + def get_lun_by_args(self, **args): """Retrieves LUNs with specified args.""" raise NotImplementedError() diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py index 2a83e6221..e63ee82a5 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py @@ -51,8 +51,8 @@ class Client(client_base.Client): def set_vserver(self, vserver): self.connection.set_vserver(vserver) - def get_target_details(self): - """Gets the target portal details.""" + def get_iscsi_target_details(self): + """Gets the iSCSI target portal details.""" iscsi_if_iter = netapp_api.NaElement('iscsi-interface-get-iter') result = self.connection.invoke_successfully(iscsi_if_iter, True) tgt_list = [] @@ -70,6 +70,25 @@ class Client(client_base.Client): tgt_list.append(d) return tgt_list + def get_fc_target_wwpns(self): + """Gets the FC target details.""" + wwpns = [] + port_name_list_api = netapp_api.NaElement('fcp-port-name-get-iter') + port_name_list_api.add_new_child('max-records', '100') + result = self.connection.invoke_successfully(port_name_list_api, True) + num_records = result.get_child_content('num-records') + if num_records and int(num_records) >= 1: + for port_name_info in result.get_child_by_name( + 'attributes-list').get_children(): + + if port_name_info.get_child_content('is-used') != 'true': + continue + + wwpn = port_name_info.get_child_content('port-name').lower() + wwpns.append(wwpn) + + return wwpns + def get_iscsi_service_details(self): """Returns iscsi iqn.""" iscsi_service_iter = netapp_api.NaElement('iscsi-service-get-iter') @@ -100,7 +119,7 @@ class Client(client_base.Client): query = netapp_api.NaElement('query') query.add_child_elem(lun_info) api.add_child_elem(query) - result = self.connection.invoke_successfully(api) + result = self.connection.invoke_successfully(api, True) if result.get_child_by_name('num-records') and\ int(result.get_child_content('num-records')) >= 1: attr_list = result.get_child_by_name('attributes-list') @@ -139,51 +158,81 @@ class Client(client_base.Client): break return map_list - def get_igroup_by_initiator(self, initiator): - """Get igroups by initiator.""" + def _get_igroup_by_initiator_query(self, initiator, tag): + igroup_get_iter = netapp_api.NaElement('igroup-get-iter') + igroup_get_iter.add_new_child('max-records', '100') + if tag: + igroup_get_iter.add_new_child('tag', tag, True) + + query = netapp_api.NaElement('query') + igroup_info = netapp_api.NaElement('initiator-group-info') + query.add_child_elem(igroup_info) + igroup_info.add_new_child('vserver', self.vserver) + initiators = netapp_api.NaElement('initiators') + igroup_info.add_child_elem(initiators) + igroup_get_iter.add_child_elem(query) + initiators.add_node_with_children( + 'initiator-info', **{'initiator-name': initiator}) + + # limit results to just the attributes of interest + desired_attrs = netapp_api.NaElement('desired-attributes') + desired_igroup_info = netapp_api.NaElement('initiator-group-info') + desired_igroup_info.add_node_with_children( + 'initiators', **{'initiator-info': None}) + desired_igroup_info.add_new_child('vserver', None) + desired_igroup_info.add_new_child('initiator-group-name', None) + desired_igroup_info.add_new_child('initiator-group-type', None) + desired_igroup_info.add_new_child('initiator-group-os-type', None) + desired_attrs.add_child_elem(desired_igroup_info) + igroup_get_iter.add_child_elem(desired_attrs) + + return igroup_get_iter + + def get_igroup_by_initiators(self, initiator_list): + """Get igroups exactly matching a set of initiators.""" tag = None igroup_list = [] + if not initiator_list: + return igroup_list + + initiator_set = set(initiator_list) + while True: - igroup_iter = netapp_api.NaElement('igroup-get-iter') - igroup_iter.add_new_child('max-records', '100') - if tag: - igroup_iter.add_new_child('tag', tag, True) - query = netapp_api.NaElement('query') - igroup_iter.add_child_elem(query) - igroup_info = netapp_api.NaElement('initiator-group-info') - query.add_child_elem(igroup_info) - igroup_info.add_new_child('vserver', self.vserver) - initiators = netapp_api.NaElement('initiators') - igroup_info.add_child_elem(initiators) - initiators.add_node_with_children('initiator-info', - **{'initiator-name': initiator}) - des_attrs = netapp_api.NaElement('desired-attributes') - des_ig_info = netapp_api.NaElement('initiator-group-info') - des_attrs.add_child_elem(des_ig_info) - des_ig_info.add_node_with_children('initiators', - **{'initiator-info': None}) - des_ig_info.add_new_child('vserver', None) - des_ig_info.add_new_child('initiator-group-name', None) - des_ig_info.add_new_child('initiator-group-type', None) - des_ig_info.add_new_child('initiator-group-os-type', None) - igroup_iter.add_child_elem(des_attrs) - result = self.connection.invoke_successfully(igroup_iter, False) + # C-mode getter APIs can't do an 'and' query, so match the first + # initiator (which will greatly narrow the search results) and + # filter the rest in this method. + query = self._get_igroup_by_initiator_query(initiator_list[0], tag) + result = self.connection.invoke_successfully(query, True) + tag = result.get_child_content('next-tag') - if result.get_child_content('num-records') and\ - int(result.get_child_content('num-records')) > 0: - attr_list = result.get_child_by_name('attributes-list') - igroups = attr_list.get_children() - for igroup in igroups: - ig = dict() - ig['initiator-group-os-type'] = igroup.get_child_content( - 'initiator-group-os-type') - ig['initiator-group-type'] = igroup.get_child_content( - 'initiator-group-type') - ig['initiator-group-name'] = igroup.get_child_content( - 'initiator-group-name') - igroup_list.append(ig) + num_records = result.get_child_content('num-records') + if num_records and int(num_records) >= 1: + + for igroup_info in result.get_child_by_name( + 'attributes-list').get_children(): + + initiator_set_for_igroup = set() + for initiator_info in igroup_info.get_child_by_name( + 'initiators').get_children(): + + initiator_set_for_igroup.add( + initiator_info.get_child_content('initiator-name')) + + if initiator_set == initiator_set_for_igroup: + igroup = {'initiator-group-os-type': + igroup_info.get_child_content( + 'initiator-group-os-type'), + 'initiator-group-type': + igroup_info.get_child_content( + 'initiator-group-type'), + 'initiator-group-name': + igroup_info.get_child_content( + 'initiator-group-name')} + igroup_list.append(igroup) + if tag is None: break + return igroup_list def clone_lun(self, volume, name, new_name, space_reserved='true', @@ -240,7 +289,7 @@ class Client(client_base.Client): query = netapp_api.NaElement('query') lun_iter.add_child_elem(query) query.add_node_with_children('lun-info', **args) - luns = self.connection.invoke_successfully(lun_iter) + luns = self.connection.invoke_successfully(lun_iter, True) attr_list = luns.get_child_by_name('attributes-list') return attr_list.get_children() diff --git a/cinder/volume/drivers/netapp/dataontap/fc_7mode.py b/cinder/volume/drivers/netapp/dataontap/fc_7mode.py new file mode 100644 index 000000000..ba5a6bc8e --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/fc_7mode.py @@ -0,0 +1,85 @@ +# Copyright (c) - 2014, Clinton Knight. 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 Data ONTAP (7-mode) FibreChannel storage systems. +""" + +from cinder.openstack.common import log as logging +from cinder.volume import driver +from cinder.volume.drivers.netapp.dataontap.block_7mode import \ + NetAppBlockStorage7modeLibrary as lib_7mode +from cinder.zonemanager import utils as fczm_utils + +LOG = logging.getLogger(__name__) + + +class NetApp7modeFibreChannelDriver(driver.FibreChannelDriver): + """NetApp 7-mode FibreChannel volume driver.""" + + DRIVER_NAME = 'NetApp_FibreChannel_7mode_direct' + + def __init__(self, *args, **kwargs): + super(NetApp7modeFibreChannelDriver, self).__init__(*args, **kwargs) + self.library = lib_7mode(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) + + @fczm_utils.AddFCZone + def initialize_connection(self, volume, connector): + 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/dataontap/fc_cmode.py b/cinder/volume/drivers/netapp/dataontap/fc_cmode.py new file mode 100644 index 000000000..fc6fd9242 --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/fc_cmode.py @@ -0,0 +1,85 @@ +# Copyright (c) - 2014, Clinton Knight. 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 Data ONTAP (C-mode) FibreChannel storage systems. +""" + +from cinder.openstack.common import log as logging +from cinder.volume import driver +from cinder.volume.drivers.netapp.dataontap.block_cmode import \ + NetAppBlockStorageCmodeLibrary as lib_cmode +from cinder.zonemanager import utils as fczm_utils + +LOG = logging.getLogger(__name__) + + +class NetAppCmodeFibreChannelDriver(driver.FibreChannelDriver): + """NetApp C-mode FibreChannel volume driver.""" + + DRIVER_NAME = 'NetApp_FibreChannel_Cluster_direct' + + def __init__(self, *args, **kwargs): + super(NetAppCmodeFibreChannelDriver, self).__init__(*args, **kwargs) + self.library = lib_cmode(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) + + @fczm_utils.AddFCZone + def initialize_connection(self, volume, connector): + 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) \ No newline at end of file diff --git a/cinder/volume/drivers/netapp/options.py b/cinder/volume/drivers/netapp/options.py index 8a701616f..c66af37a3 100644 --- a/cinder/volume/drivers/netapp/options.py +++ b/cinder/volume/drivers/netapp/options.py @@ -36,7 +36,8 @@ netapp_proxy_opts = [ cfg.StrOpt('netapp_storage_protocol', default=None, help=('The storage protocol to be used on the data path with ' - 'the storage system; valid values are iscsi or nfs.')), ] + 'the storage system; valid values are iscsi, fc, or ' + 'nfs.')), ] netapp_connection_opts = [ cfg.StrOpt('netapp_server_hostname', @@ -78,8 +79,8 @@ netapp_provisioning_opts = [ cfg.StrOpt('netapp_volume_list', default=None, help=('This option is only utilized when the storage protocol ' - 'is configured to use iSCSI. This option is used to ' - 'restrict provisioning to the specified controller ' + 'is configured to use iSCSI or FC. This option is used ' + 'to restrict provisioning to the specified controller ' 'volumes. Specify the value of this option to be a ' 'comma separated list of NetApp controller volume names ' 'to be used for provisioning.')), ] @@ -107,7 +108,14 @@ netapp_7mode_opts = [ 'driver when connecting to an instance with a storage ' 'family of Data ONTAP operating in 7-Mode. Only use this ' 'option when utilizing the MultiStore feature on the ' - 'NetApp storage system.')), ] + 'NetApp storage system.')), + cfg.StrOpt('netapp_partner_backend_name', + default=None, + help=('The name of the config.conf stanza for a Data ONTAP ' + '(7-mode) HA partner. This option is only used by the ' + 'driver when connecting to an instance with a storage ' + 'family of Data ONTAP operating in 7-Mode, and it is ' + 'required if the storage protocol selected is FC.')), ] netapp_img_cache_opts = [ cfg.IntOpt('thres_avl_size_perc_start', diff --git a/cinder/volume/drivers/netapp/utils.py b/cinder/volume/drivers/netapp/utils.py index 971e16a63..2fc522725 100644 --- a/cinder/volume/drivers/netapp/utils.py +++ b/cinder/volume/drivers/netapp/utils.py @@ -139,6 +139,12 @@ def log_extra_spec_warnings(extra_specs): LOG.warning(msg % args) +class hashabledict(dict): + """A hashable dictionary that is comparable (i.e. in unit tests, etc.)""" + def __hash__(self): + return hash(tuple(sorted(self.items()))) + + class OpenStackInfo(object): """OS/distribution, release, and version. -- 2.45.2