diff --git a/cinder/tests/unit/test_volume_utils.py b/cinder/tests/unit/test_volume_utils.py index 632cc32df22..2015e2bbd7e 100644 --- a/cinder/tests/unit/test_volume_utils.py +++ b/cinder/tests/unit/test_volume_utils.py @@ -1080,3 +1080,16 @@ class VolumeUtilsTestCase(test.TestCase): self.assertEqual(max_over_subscription_ratio, mosr) else: self.assertEqual(float(max_over_subscription_ratio), mosr) + + def test_make_initiator_target_all2all_map(self): + initiator_wwpns = ['ff00000000000000', 'ff00000000000001'] + target_wwpns = ['bb00000000000000', 'bb00000000000001'] + + expected = { + 'ff00000000000000': ['bb00000000000000', 'bb00000000000001'], + 'ff00000000000001': ['bb00000000000000', 'bb00000000000001'] + } + + ret = volume_utils.make_initiator_target_all2all_map(initiator_wwpns, + target_wwpns) + self.assertEqual(ret, expected) diff --git a/cinder/tests/unit/volume/drivers/inspur/instorage/fakes.py b/cinder/tests/unit/volume/drivers/inspur/instorage/fakes.py index 67560ffc287..f58cd63cbb1 100644 --- a/cinder/tests/unit/volume/drivers/inspur/instorage/fakes.py +++ b/cinder/tests/unit/volume/drivers/inspur/instorage/fakes.py @@ -27,6 +27,7 @@ from cinder import exception from cinder import utils from cinder.volume.drivers.inspur.instorage import instorage_const +from cinder.volume.drivers.inspur.instorage import instorage_fc from cinder.volume.drivers.inspur.instorage import instorage_iscsi MCS_POOLS = ['openstack', 'openstack1'] @@ -39,6 +40,21 @@ def get_test_pool(get_all=False): return MCS_POOLS[0] +class FakeInStorageMCSFcDriver(instorage_fc.InStorageMCSFCDriver): + + def __init__(self, *args, **kwargs): + super(FakeInStorageMCSFcDriver, self).__init__(*args, **kwargs) + + def set_fake_storage(self, fake): + self.fake_storage = fake + + def _run_ssh(self, cmd, check_exit_code=True, attempts=1): + utils.check_ssh_injection(cmd) + ret = self.fake_storage.execute_command(cmd, check_exit_code) + + return ret + + class FakeInStorageMCSISCSIDriver(instorage_iscsi.InStorageMCSISCSIDriver): def __init__(self, *args, **kwargs): diff --git a/cinder/tests/unit/volume/drivers/inspur/instorage/test_fc_driver.py b/cinder/tests/unit/volume/drivers/inspur/instorage/test_fc_driver.py new file mode 100644 index 00000000000..20f92d551af --- /dev/null +++ b/cinder/tests/unit/volume/drivers/inspur/instorage/test_fc_driver.py @@ -0,0 +1,596 @@ +# Copyright 2017 Inspur Corp. +# 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. +# +""" +Tests for the Inspur InStorage volume driver. +""" + +from eventlet import greenthread +import mock +from oslo_utils import importutils + +from cinder import context +from cinder import exception +from cinder import test +from cinder.tests.unit import utils as testutils +from cinder.tests.unit.volume.drivers.inspur.instorage import fakes +from cinder.volume import configuration as conf +from cinder.volume.drivers.inspur.instorage import instorage_common +from cinder.volume.drivers.inspur.instorage import instorage_fc +from cinder.volume import volume_types + + +class InStorageMCSFcDriverTestCase(test.TestCase): + + @mock.patch.object(greenthread, 'sleep') + def setUp(self, mock_sleep): + super(InStorageMCSFcDriverTestCase, self).setUp() + self.fc_driver = fakes.FakeInStorageMCSFcDriver( + configuration=conf.Configuration(None)) + self._def_flags = {'san_ip': 'hostname', + 'san_login': 'user', + 'san_password': 'pass', + 'instorage_mcs_volpool_name': ['openstack'], + 'instorage_mcs_localcopy_timeout': 20, + 'instorage_mcs_localcopy_rate': 49, + 'instorage_mcs_allow_tenant_qos': True} + wwpns = ['1234567890123458', '6543210987654323'] + initiator = 'test.initiator.%s' % 123458 + self._connector = {'ip': '1.234.56.78', + 'host': 'instorage-mcs-test', + 'wwpns': wwpns, + 'initiator': initiator} + self.sim = fakes.FakeInStorage(['openstack']) + + self.fc_driver.set_fake_storage(self.sim) + self.ctxt = context.get_admin_context() + + self._reset_flags() + self.ctxt = context.get_admin_context() + db_driver = self.fc_driver.configuration.db_driver + self.db = importutils.import_module(db_driver) + self.fc_driver.db = self.db + self.fc_driver.do_setup(None) + self.fc_driver.check_for_setup_error() + self.fc_driver._assistant.check_lcmapping_interval = 0 + + def _set_flag(self, flag, value): + group = self.fc_driver.configuration.config_group + self.fc_driver.configuration.set_override(flag, value, group) + + def _reset_flags(self): + self.fc_driver.configuration.local_conf.reset() + for k, v in self._def_flags.items(): + self._set_flag(k, v) + + def _create_volume(self, **kwargs): + pool = fakes.get_test_pool() + prop = {'host': 'openstack@mcs#%s' % pool, + 'size': 1} + for p in prop.keys(): + if p not in kwargs: + kwargs[p] = prop[p] + vol = testutils.create_volume(self.ctxt, **kwargs) + self.fc_driver.create_volume(vol) + return vol + + def _delete_volume(self, volume): + self.fc_driver.delete_volume(volume) + self.db.volume_destroy(self.ctxt, volume['id']) + + def _generate_vol_info(self, vol_name, vol_id): + pool = fakes.get_test_pool() + prop = {'mdisk_grp_name': pool} + if vol_name: + prop.update(volume_name=vol_name, + volume_id=vol_id, + volume_size=10) + else: + prop.update(size=10, + volume_type_id=None, + mdisk_grp_name=pool, + host='openstack@mcs#%s' % pool) + vol = testutils.create_volume(self.ctxt, **prop) + return vol + + def _assert_vol_exists(self, name, exists): + is_vol_defined = self.fc_driver._assistant.is_vdisk_defined(name) + self.assertEqual(exists, is_vol_defined) + + def test_instorage_get_host_with_fc_connection(self): + # Create a FC host + del self._connector['initiator'] + assistant = self.fc_driver._assistant + host_name = assistant.create_host(self._connector) + + # Remove the first wwpn from connector, and then try get host + wwpns = self._connector['wwpns'] + wwpns.remove(wwpns[0]) + host_name = assistant.get_host_from_connector(self._connector) + + self.assertIsNotNone(host_name) + + def test_instorage_get_host_with_fc_connection_with_volume(self): + # create a FC volume + extra_spec = {'capabilities:storage_protocol': ' FC'} + vol_type_fc = volume_types.create(self.ctxt, 'FC', extra_spec) + + volume_fc = self._generate_vol_info(None, None) + volume_fc['volume_type_id'] = vol_type_fc['id'] + + self.fc_driver.create_volume(volume_fc) + + connector = {'host': 'instorage-mcs-host', + 'wwnns': ['20000090fa17311e', '20000090fa17311f'], + 'wwpns': ['ff00000000000000', 'ff00000000000001'], + 'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1aaa'} + + self.fc_driver.initialize_connection(volume_fc, connector) + # Create a FC host + assistant = self.fc_driver._assistant + + # tell lsfabric to not return anything + self.sim.error_injection('lsfabric', 'no_hosts') + host_name = assistant.get_host_from_connector( + connector, volume_fc['name']) + self.assertIsNotNone(host_name) + + def test_instorage_get_host_from_connector_with_lshost_failure2(self): + self._connector.pop('initiator') + self._connector['wwpns'] = [] # Clearing will skip over fast-path + assistant = self.fc_driver._assistant + # Add a host to the simulator. We don't need it to match the + # connector since we will force a bad failure for lshost. + self.sim._cmd_mkhost(name='DifferentHost', hbawwpn='123456') + # tell lshost to fail badly while called from + # get_host_from_connector + self.sim.error_injection('lshost', 'bigger_troubles') + self.assertRaises(exception.VolumeBackendAPIException, + assistant.get_host_from_connector, self._connector) + + def test_instorage_get_host_from_connector_not_found(self): + self._connector.pop('initiator') + assistant = self.fc_driver._assistant + # Create some hosts. The first is not related to the connector and + # we use the simulator for that. The second is for the connector. + # We will force the missing_host error for the first host, but + # then tolerate and find the second host on the slow path normally. + self.sim._cmd_mkhost(name='instorage-mcs-test-3', + hbawwpn='1234567') + self.sim._cmd_mkhost(name='instorage-mcs-test-2', + hbawwpn='2345678') + self.sim._cmd_mkhost(name='instorage-mcs-test-1', + hbawwpn='3456789') + self.sim._cmd_mkhost(name='A-Different-host', hbawwpn='9345678') + self.sim._cmd_mkhost(name='B-Different-host', hbawwpn='8345678') + self.sim._cmd_mkhost(name='C-Different-host', hbawwpn='7345678') + + # tell lsfabric to skip rows so that we skip past fast path + self.sim.error_injection('lsfabric', 'remove_rows') + # Run test + host_name = assistant.get_host_from_connector(self._connector) + + self.assertIsNone(host_name) + + def test_instorage_get_host_from_connector_fast_path(self): + self._connector.pop('initiator') + assistant = self.fc_driver._assistant + # Create two hosts. Our lshost will return the hosts in sorted + # Order. The extra host will be returned before the target + # host. If we get detailed lshost info on our host without + # gettting detailed info on the other host we used the fast path + self.sim._cmd_mkhost(name='A-DifferentHost', hbawwpn='123456') + assistant.create_host(self._connector) + # tell lshost to fail while called from get_host_from_connector + self.sim.error_injection('lshost', 'fail_fastpath') + # tell lsfabric to skip rows so that we skip past fast path + self.sim.error_injection('lsfabric', 'remove_rows') + # Run test + host_name = assistant.get_host_from_connector(self._connector) + + self.assertIsNotNone(host_name) + # Need to assert that lshost was actually called. The way + # we do that is check that the next simulator error for lshost + # has not been reset. + self.assertEqual(self.sim._next_cmd_error['lshost'], 'fail_fastpath', + "lshost was not called in the simulator. The " + "queued error still remains.") + + def test_instorage_initiator_multiple_wwpns_connected(self): + + # Generate us a test volume + volume = self._create_volume() + + # Fibre Channel volume type + extra_spec = {'capabilities:storage_protocol': ' FC'} + vol_type = volume_types.create(self.ctxt, 'FC', extra_spec) + + volume['volume_type_id'] = vol_type['id'] + + # Make sure that the volumes have been created + self._assert_vol_exists(volume['name'], True) + + # Set up one WWPN that won't match and one that will. + self.fc_driver._state['storage_nodes']['1']['WWPN'] = [ + '123456789ABCDEF0', 'AABBCCDDEEFF0010'] + + wwpns = ['ff00000000000000', 'ff00000000000001'] + connector = {'host': 'instorage-mcs-test', 'wwpns': wwpns} + + with mock.patch.object(instorage_common.InStorageAssistant, + 'get_conn_fc_wwpns') as get_mappings: + mapped_wwpns = ['AABBCCDDEEFF0001', 'AABBCCDDEEFF0002', + 'AABBCCDDEEFF0010', 'AABBCCDDEEFF0012'] + get_mappings.return_value = mapped_wwpns + + # Initialize the connection + init_ret = self.fc_driver.initialize_connection(volume, connector) + + # Make sure we return all wwpns which where mapped as part of the + # connection + self.assertEqual(mapped_wwpns, + init_ret['data']['target_wwn']) + + def test_instorage_mcs_fc_validate_connector(self): + conn_neither = {'host': 'host'} + conn_iscsi = {'host': 'host', 'initiator': 'foo'} + conn_fc = {'host': 'host', 'wwpns': 'bar', 'wwnns': 'foo'} + conn_both = {'host': 'host', 'initiator': 'foo', 'wwpns': 'bar', + 'wwnns': 'baz'} + + self.fc_driver.validate_connector(conn_fc) + self.fc_driver.validate_connector(conn_both) + self.assertRaises(exception.InvalidConnectorException, + self.fc_driver.validate_connector, conn_iscsi) + self.assertRaises(exception.InvalidConnectorException, + self.fc_driver.validate_connector, conn_neither) + + def test_instorage_terminate_fc_connection(self): + # create a FC volume + volume_fc = self._create_volume() + extra_spec = {'capabilities:storage_protocol': ' FC'} + vol_type_fc = volume_types.create(self.ctxt, 'FC', extra_spec) + volume_fc['volume_type_id'] = vol_type_fc['id'] + + connector = {'host': 'instorage-mcs-host', + 'wwnns': ['20000090fa17311e', '20000090fa17311f'], + 'wwpns': ['ff00000000000000', 'ff00000000000001'], + 'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1aaa'} + + self.fc_driver.initialize_connection(volume_fc, connector) + self.fc_driver.terminate_connection(volume_fc, connector) + + @mock.patch.object(instorage_fc.InStorageMCSFCDriver, + '_do_terminate_connection') + def test_instorage_initialize_fc_connection_failure(self, term_conn): + # create a FC volume + volume_fc = self._create_volume() + extra_spec = {'capabilities:storage_protocol': ' FC'} + vol_type_fc = volume_types.create(self.ctxt, 'FC', extra_spec) + volume_fc['volume_type_id'] = vol_type_fc['id'] + + connector = {'host': 'instorage-mcs-host', + 'wwnns': ['20000090fa17311e', '20000090fa17311f'], + 'wwpns': ['ff00000000000000', 'ff00000000000001'], + 'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1aaa'} + + self.fc_driver._state['storage_nodes'] = {} + self.assertRaises(exception.VolumeBackendAPIException, + self.fc_driver.initialize_connection, + volume_fc, connector) + term_conn.assert_called_once_with(volume_fc, connector) + + def test_instorage_terminate_fc_connection_multi_attach(self): + # create a FC volume + volume_fc = self._create_volume() + extra_spec = {'capabilities:storage_protocol': ' FC'} + vol_type_fc = volume_types.create(self.ctxt, 'FC', extra_spec) + volume_fc['volume_type_id'] = vol_type_fc['id'] + + connector = {'host': 'instorage-mcs-host', + 'wwnns': ['20000090fa17311e', '20000090fa17311f'], + 'wwpns': ['ff00000000000000', 'ff00000000000001'], + 'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1aaa'} + connector2 = {'host': 'INSTORAGE-MCS-HOST', + 'wwnns': ['30000090fa17311e', '30000090fa17311f'], + 'wwpns': ['ffff000000000000', 'ffff000000000001'], + 'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1bbb'} + + # map and unmap the volume to two hosts normal case + self.fc_driver.initialize_connection(volume_fc, connector) + self.fc_driver.initialize_connection(volume_fc, connector2) + # validate that the host entries are created + for conn in [connector, connector2]: + host = self.fc_driver._assistant.get_host_from_connector(conn) + self.assertIsNotNone(host) + self.fc_driver.terminate_connection(volume_fc, connector) + self.fc_driver.terminate_connection(volume_fc, connector2) + # validate that the host entries are deleted + for conn in [connector, connector2]: + host = self.fc_driver._assistant.get_host_from_connector(conn) + self.assertIsNone(host) + # map and unmap the volume to two hosts with the mapping gone + self.fc_driver.initialize_connection(volume_fc, connector) + self.fc_driver.initialize_connection(volume_fc, connector2) + # Test multiple attachments case + host_name = self.fc_driver._assistant.get_host_from_connector( + connector2) + self.fc_driver._assistant.unmap_vol_from_host( + volume_fc['name'], host_name) + host_name = self.fc_driver._assistant.get_host_from_connector( + connector2) + self.assertIsNotNone(host_name) + with mock.patch.object(instorage_common.InStorageSSH, + 'rmvdiskhostmap') as rmmap: + rmmap.side_effect = Exception('boom') + self.fc_driver.terminate_connection(volume_fc, connector2) + host_name = self.fc_driver._assistant.get_host_from_connector( + connector2) + self.assertIsNone(host_name) + # Test single attachment case + self.fc_driver._assistant.unmap_vol_from_host( + volume_fc['name'], host_name) + with mock.patch.object(instorage_common.InStorageSSH, + 'rmvdiskhostmap') as rmmap: + rmmap.side_effect = Exception('boom') + self.fc_driver.terminate_connection(volume_fc, connector) + # validate that the host entries are deleted + for conn in [connector, connector2]: + host = self.fc_driver._assistant.get_host_from_connector(conn) + self.assertIsNone(host) + + def test_instorage_initiator_target_map(self): + # Generate us a test volume + volume = self._create_volume() + + # FIbre Channel volume type + extra_spec = {'capabilities:storage_protocol': ' FC'} + vol_type = volume_types.create(self.ctxt, 'FC', extra_spec) + + volume['volume_type_id'] = vol_type['id'] + + # Make sure that the volumes have been created + self._assert_vol_exists(volume['name'], True) + + wwpns = ['ff00000000000000', 'ff00000000000001'] + connector = {'host': 'instorage-mcs-test', 'wwpns': wwpns} + + # Initialise the connection + init_ret = self.fc_driver.initialize_connection(volume, connector) + + # Check that the initiator_target_map is as expected + init_data = {'driver_volume_type': 'fibre_channel', + 'data': {'initiator_target_map': + {'ff00000000000000': ['AABBCCDDEEFF0011'], + 'ff00000000000001': ['AABBCCDDEEFF0011']}, + 'target_discovered': False, + 'target_lun': 0, + 'target_wwn': ['AABBCCDDEEFF0011'], + 'volume_id': volume['id'] + } + } + + self.assertEqual(init_data, init_ret) + + # Terminate connection + term_ret = self.fc_driver.terminate_connection(volume, connector) + + # Check that the initiator_target_map is as expected + term_data = {'driver_volume_type': 'fibre_channel', + 'data': {'initiator_target_map': + {'ff00000000000000': ['5005076802432ADE', + '5005076802332ADE', + '5005076802532ADE', + '5005076802232ADE', + '5005076802132ADE', + '5005086802132ADE', + '5005086802332ADE', + '5005086802532ADE', + '5005086802232ADE', + '5005086802432ADE'], + 'ff00000000000001': ['5005076802432ADE', + '5005076802332ADE', + '5005076802532ADE', + '5005076802232ADE', + '5005076802132ADE', + '5005086802132ADE', + '5005086802332ADE', + '5005086802532ADE', + '5005086802232ADE', + '5005086802432ADE']} + } + } + + self.assertItemsEqual(term_data, term_ret) + + def test_instorage_mcs_fc_host_maps(self): + # Create two volumes to be used in mappings + + ctxt = context.get_admin_context() + volume1 = self._generate_vol_info(None, None) + self.fc_driver.create_volume(volume1) + volume2 = self._generate_vol_info(None, None) + self.fc_driver.create_volume(volume2) + + # FIbre Channel volume type + extra_spec = {'capabilities:storage_protocol': ' FC'} + vol_type = volume_types.create(self.ctxt, 'FC', extra_spec) + + expected = {'driver_volume_type': 'fibre_channel', + 'data': {'target_lun': 0, + 'target_wwn': ['AABBCCDDEEFF0011'], + 'target_discovered': False}} + + volume1['volume_type_id'] = vol_type['id'] + volume2['volume_type_id'] = vol_type['id'] + + ret = self.fc_driver._assistant.get_host_from_connector( + self._connector) + self.assertIsNone(ret) + + # Make sure that the volumes have been created + self._assert_vol_exists(volume1['name'], True) + self._assert_vol_exists(volume2['name'], True) + + # Initialize connection from the first volume to a host + ret = self.fc_driver.initialize_connection( + volume1, self._connector) + self.assertEqual(expected['driver_volume_type'], + ret['driver_volume_type']) + for k, v in expected['data'].items(): + self.assertEqual(v, ret['data'][k]) + + # Initialize again, should notice it and do nothing + ret = self.fc_driver.initialize_connection( + volume1, self._connector) + self.assertEqual(expected['driver_volume_type'], + ret['driver_volume_type']) + for k, v in expected['data'].items(): + self.assertEqual(v, ret['data'][k]) + + # Try to delete the 1st volume (should fail because it is mapped) + self.assertRaises(exception.VolumeBackendAPIException, + self.fc_driver.delete_volume, + volume1) + + # Check bad output from lsfabric for the 2nd volume + for error in ['remove_field', 'header_mismatch']: + self.sim.error_injection('lsfabric', error) + self.assertRaises(exception.VolumeBackendAPIException, + self.fc_driver.initialize_connection, + volume2, self._connector) + + with mock.patch.object(instorage_common.InStorageAssistant, + 'get_conn_fc_wwpns') as conn_fc_wwpns: + conn_fc_wwpns.return_value = [] + ret = self.fc_driver.initialize_connection(volume2, + self._connector) + + ret = self.fc_driver.terminate_connection(volume1, self._connector) + # For the first volume detach, ret['data'] should be empty + # only ret['driver_volume_type'] returned + self.assertEqual({}, ret['data']) + self.assertEqual('fibre_channel', ret['driver_volume_type']) + ret = self.fc_driver.terminate_connection(volume2, + self._connector) + self.assertEqual('fibre_channel', ret['driver_volume_type']) + # wwpn is randomly created + self.assertNotEqual({}, ret['data']) + + ret = self.fc_driver._assistant.get_host_from_connector( + self._connector) + self.assertIsNone(ret) + + # Test no preferred node + self.sim.error_injection('lsvdisk', 'no_pref_node') + self.assertRaises(exception.VolumeBackendAPIException, + self.fc_driver.initialize_connection, + volume1, self._connector) + + # Initialize connection from the second volume to the host with no + # preferred node set if in simulation mode, otherwise, just + # another initialize connection. + self.sim.error_injection('lsvdisk', 'blank_pref_node') + self.fc_driver.initialize_connection(volume2, self._connector) + + # Try to remove connection from host that doesn't exist (should fail) + conn_no_exist = self._connector.copy() + conn_no_exist['initiator'] = 'i_dont_exist' + conn_no_exist['wwpns'] = ['0000000000000000'] + self.assertRaises(exception.VolumeDriverException, + self.fc_driver.terminate_connection, + volume1, + conn_no_exist) + + # Try to remove connection from volume that isn't mapped (should print + # message but NOT fail) + unmapped_vol = self._generate_vol_info(None, None) + self.fc_driver.create_volume(unmapped_vol) + self.fc_driver.terminate_connection(unmapped_vol, self._connector) + self.fc_driver.delete_volume(unmapped_vol) + + # Remove the mapping from the 1st volume and delete it + self.fc_driver.terminate_connection(volume1, self._connector) + self.fc_driver.delete_volume(volume1) + self._assert_vol_exists(volume1['name'], False) + + # Make sure our host still exists + host_name = self.fc_driver._assistant.get_host_from_connector( + self._connector) + self.assertIsNotNone(host_name) + + # Remove the mapping from the 2nd volume. The host should + # be automatically removed because there are no more mappings. + self.fc_driver.terminate_connection(volume2, self._connector) + + # Check if we successfully terminate connections when the host is not + # specified + fake_conn = {'ip': '127.0.0.1', 'initiator': 'iqn.fake'} + self.fc_driver.initialize_connection(volume2, self._connector) + host_name = self.fc_driver._assistant.get_host_from_connector( + self._connector) + self.assertIsNotNone(host_name) + self.fc_driver.terminate_connection(volume2, fake_conn) + host_name = self.fc_driver._assistant.get_host_from_connector( + self._connector) + self.assertIsNone(host_name) + self.fc_driver.delete_volume(volume2) + self._assert_vol_exists(volume2['name'], False) + + # Delete volume types that we created + volume_types.destroy(ctxt, vol_type['id']) + + ret = (self.fc_driver._assistant.get_host_from_connector( + self._connector)) + self.assertIsNone(ret) + + def test_instorage_mcs_fc_multi_host_maps(self): + # Create a volume to be used in mappings + ctxt = context.get_admin_context() + volume = self._generate_vol_info(None, None) + self.fc_driver.create_volume(volume) + + # Create volume types for protocols + types = {} + for protocol in ['FC']: + opts = {'storage_protocol': ' ' + protocol} + types[protocol] = volume_types.create(ctxt, protocol, opts) + + # Create a connector for the second 'host' + wwpns = ['1234567890123459', '6543210987654324'] + initiator = 'test.initiator.%s' % 123459 + conn2 = {'ip': '1.234.56.79', + 'host': 'instorage-mcs-test2', + 'wwpns': wwpns, + 'initiator': initiator} + + # Check protocols for FC + volume['volume_type_id'] = types[protocol]['id'] + + # Make sure that the volume has been created + self._assert_vol_exists(volume['name'], True) + + self.fc_driver.initialize_connection(volume, self._connector) + self.fc_driver.initialize_connection(volume, conn2) + + self.fc_driver.terminate_connection(volume, conn2) + self.fc_driver.terminate_connection(volume, self._connector) + + def test_add_vdisk_copy_fc(self): + # Ensure only FC is available + self.fc_driver._state['enabled_protocols'] = set(['FC']) + volume = self._generate_vol_info(None, None) + self.fc_driver.create_volume(volume) + self.fc_driver.add_vdisk_copy(volume['name'], 'fake-pool', None) diff --git a/cinder/volume/drivers/inspur/instorage/instorage_fc.py b/cinder/volume/drivers/inspur/instorage/instorage_fc.py new file mode 100644 index 00000000000..2a7102693e4 --- /dev/null +++ b/cinder/volume/drivers/inspur/instorage/instorage_fc.py @@ -0,0 +1,233 @@ +# Copyright 2017 Inspur Corp. +# 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. +# +""" +FC volume driver for Inspur InStorage family storage systems. +""" + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import excutils + +from cinder import coordination +from cinder import exception +from cinder.i18n import _ +from cinder import interface +from cinder import utils as cinder_utils +from cinder.volume import driver +from cinder.volume.drivers.inspur.instorage import instorage_common +from cinder.volume import utils +from cinder.zonemanager import utils as fczm_utils + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF + + +@interface.volumedriver +class InStorageMCSFCDriver(instorage_common.InStorageMCSCommonDriver, + driver.FibreChannelDriver): + """INSPUR InStorage MCS FC volume driver. + + Version history: + + .. code-block:: none + + 1.0 - Initial driver + """ + + VERSION = "1.0.0" + + # ThirdPartySystems wiki page + CI_WIKI_NAME = "INSPUR_CI" + + def __init__(self, *args, **kwargs): + super(InStorageMCSFCDriver, self).__init__(*args, **kwargs) + self.protocol = 'FC' + + @cinder_utils.trace + @fczm_utils.add_fc_zone + @coordination.synchronized('instorage-host' + '{self._state[system_id]}' + '{connector[host]}') + def initialize_connection(self, volume, connector): + """Perform necessary work to make a FC connection. + + To be able to create an FC connection from a given host to a + volume, we must: + 1. Translate the given WWNN to a host name + 2. Create new host on the storage system if it does not yet exist + 3. Map the volume to the host if it is not already done + 4. Return the connection information for relevant nodes (in the + proper I/O group) + + """ + volume_name = self._get_target_vol(volume) + + # Check if a host object is defined for this host name + host_name = self._assistant.get_host_from_connector(connector) + if host_name is None: + # Host does not exist - add a new host to InStorage/MCS + host_name = self._assistant.create_host(connector) + + volume_attributes = self._assistant.get_vdisk_attributes(volume_name) + if volume_attributes is None: + msg = (_('initialize_connection: Failed to get attributes' + ' for volume %s.') % volume_name) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + lun_id = self._assistant.map_vol_to_host(volume_name, + host_name, + True) + + try: + preferred_node = volume_attributes['preferred_node_id'] + IO_group = volume_attributes['IO_group_id'] + except KeyError as e: + LOG.error('Did not find expected column name in ' + 'lsvdisk: %s.', e) + raise exception.VolumeBackendAPIException( + data=_('initialize_connection: Missing volume attribute for ' + 'volume %s.') % volume_name) + + try: + # Get preferred node and other nodes in I/O group + preferred_node_entry = None + io_group_nodes = [] + for node in self._state['storage_nodes'].values(): + if node['id'] == preferred_node: + preferred_node_entry = node + if node['IO_group'] == IO_group: + io_group_nodes.append(node) + + if not len(io_group_nodes): + msg = (_('initialize_connection: No node found in ' + 'I/O group %(gid)s for volume %(vol)s.') % + {'gid': IO_group, 'vol': volume_name}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + if not preferred_node_entry: + # Get 1st node in I/O group + preferred_node_entry = io_group_nodes[0] + LOG.warning('initialize_connection: Did not find a ' + 'preferred node for volume %s.', volume_name) + + properties = {} + properties['target_discovered'] = False + properties['target_lun'] = lun_id + properties['volume_id'] = volume.id + + conn_wwpns = self._assistant.get_conn_fc_wwpns(host_name) + + # If conn_wwpns is empty, then that means that there were + # no target ports with visibility to any of the initiators + # so we return all target ports. + if len(conn_wwpns) == 0: + for node in self._state['storage_nodes'].values(): + conn_wwpns.extend(node['WWPN']) + + properties['target_wwn'] = conn_wwpns + + i_t_map = utils.make_initiator_target_all2all_map( + connector['wwpns'], conn_wwpns) + properties['initiator_target_map'] = i_t_map + + except Exception: + with excutils.save_and_reraise_exception(): + self._do_terminate_connection(volume, connector) + LOG.error('initialize_connection: Failed ' + 'to collect return ' + 'properties for volume %(vol)s and connector ' + '%(conn)s.\n', {'vol': volume, + 'conn': connector}) + + return {'driver_volume_type': 'fibre_channel', 'data': properties, } + + @fczm_utils.remove_fc_zone + def terminate_connection(self, volume, connector, **kwargs): + """Cleanup after an FC connection has been terminated.""" + # If a fake connector is generated by nova when the host + # is down, then the connector will not have a host property, + # In this case construct the lock without the host property + # so that all the fake connectors to an MCS are serialized + host = "" + if connector is not None and 'host' in connector: + host = connector['host'] + + @coordination.synchronized('instorage-host' + + self._state['system_id'] + host) + def _do_terminate_connection_locked(): + return self._do_terminate_connection(volume, connector, + **kwargs) + return _do_terminate_connection_locked() + + @cinder_utils.trace + def _do_terminate_connection(self, volume, connector, **kwargs): + """Cleanup after an FC connection has been terminated. + + When we clean up a terminated connection between a given connector + and volume, we: + 1. Translate the given connector to a host name + 2. Remove the volume-to-host mapping if it exists + 3. Delete the host if it has no more mappings (hosts are created + automatically by this driver when mappings are created) + """ + vol_name = self._get_target_vol(volume) + info = {} + if connector is not None and 'host' in connector: + # get host according to FC protocol + connector = connector.copy() + + connector.pop('initiator', None) + info = {'driver_volume_type': 'fibre_channel', + 'data': {}} + + host_name = self._assistant.get_host_from_connector( + connector, volume_name=vol_name) + if host_name is None: + msg = (_('terminate_connection: Failed to get host name from' + ' connector.')) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + else: + host_name = None + + # Unmap volumes, if hostname is None, need to get value from vdiskmap + host_name = self._assistant.unmap_vol_from_host(vol_name, host_name) + + # Host_name could be none + if host_name: + resp = self._assistant.check_host_mapped_vols(host_name) + if not len(resp): + LOG.info("Need to remove FC Zone, building initiator " + "target map.") + # Build info data structure for zone removing + if connector is not None and 'wwpns' in connector: + target_wwpns = [] + # Returning all target_wwpns in storage_nodes, since + # we cannot determine which wwpns are logged in during + # a VM deletion. + for node in self._state['storage_nodes'].values(): + target_wwpns.extend(node['WWPN']) + init_targ_map = (utils.make_initiator_target_all2all_map + (connector['wwpns'], + target_wwpns)) + info['data'] = {'initiator_target_map': init_targ_map} + # No volume mapped to the host, delete host from array + self._assistant.delete_host(host_name) + + return info diff --git a/cinder/volume/utils.py b/cinder/volume/utils.py index a5158c6c949..0d5e8950f84 100644 --- a/cinder/volume/utils.py +++ b/cinder/volume/utils.py @@ -1011,3 +1011,14 @@ def get_max_over_subscription_ratio(str_value, supports_auto=False): LOG.error(msg) raise exception.InvalidParameterValue(message=msg) return mosr + + +def make_initiator_target_all2all_map(initiator_wwpns, target_wwpns): + """Build a simplistic all-to-all mapping.""" + i_t_map = {} + for i_wwpn in initiator_wwpns: + i_t_map[str(i_wwpn)] = [] + for t_wwpn in target_wwpns: + i_t_map[i_wwpn].append(t_wwpn) + + return i_t_map diff --git a/doc/source/configuration/block-storage/drivers/inspur-instorage-driver.rst b/doc/source/configuration/block-storage/drivers/inspur-instorage-driver.rst new file mode 100644 index 00000000000..9ff44ba750a --- /dev/null +++ b/doc/source/configuration/block-storage/drivers/inspur-instorage-driver.rst @@ -0,0 +1,111 @@ +===================================== +Inspur InStorage family volume driver +===================================== + +Inspur InStorage family volume driver provides OpenStack Compute instances +with access to Inspur Instorage family storage system. + +Inspur InStorage storage system can be used with FC or iSCSI connection. + +This documentation explains how to configure and connect the block storage +nodes to Inspur InStorage family storage system. + +Supported operations +~~~~~~~~~~~~~~~~~~~~ + +- Create, list, delete, attach (map), and detach (unmap) volumes. +- Create, list and delete volume snapshots. +- Create a volume from a snapshot. +- Copy an image to a volume. +- Copy a volume to an image. +- Clone a volume. +- Extend a volume. +- Retype a volume. +- Manage and unmanage a volume. +- Create, list, and delete consistency group. +- Create, list, and delete consistency group snapshot. +- Modify consistency group (add or remove volumes). +- Create consistency group from source. +- Failover and Failback support. + +Configure Inspur InStorage iSCSI/FC backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This section details the steps required to configure the Inspur InStorage +Cinder Driver for single FC or iSCSI backend. + +#. In the ``cinder.conf`` configuration file under the ``[DEFAULT]`` + section, set the enabled_backends parameter + with the iSCSI or FC back-end group + + - For Fibre Channel: + + .. code-block:: ini + + [DEFAULT] + enabled_backends = instorage-fc-1 + + - For iSCSI: + + .. code-block:: ini + + [DEFAULT] + enabled_backends = instorage-iscsi-1 + + +#. Add a back-end group section for back-end group specified + in the enabled_backends parameter + +#. In the newly created back-end group section, set the + following configuration options: + + - For Fibre Channel: + + .. code-block:: ini + + [instorage-fc-1] + # Management IP of Inspur InStorage storage array + san_ip = 10.0.0.10 + # Management Port of Inspur InStorage storage array, by default set to 22 + san_ssh_port = 22 + # Management username of Inspur InStorage storage array + san_login = username + # Management password of Inspur InStorage storage array + san_password = password + # Private key for Inspur InStorage storage array + san_private_key = path/to/the/private/key + # The Pool used to allocated volumes + instorage_mcs_volpool_name = Pool0 + # The driver path + volume_driver = cinder.volume.drivers.inspur.instorage.instorage_fc.InStorageMCSFCDriver + # Backend name + volume_backend_name = instorage_fc + + - For iSCSI: + + .. code-block:: ini + + [instorage-iscsi-1] + # Management IP of Inspur InStorage storage array + san_ip = 10.0.0.10 + # Management Port of Inspur InStorage storage array, by default set to 22 + san_ssh_port = 22 + # Management username of Inspur InStorage storage array + san_login = username + # Management password of Inspur InStorage storage array + san_password = password + # Private key for Inspur InStorage storage array + san_private_key = path/to/the/private/key + # The Pool used to allocated volumes + instorage_mcs_volpool_name = Pool0 + # The driver path + volume_driver = cinder.volume.drivers.inspur.instorage.instorage_iscsi.InStorageMCSISCSIDriver + # Backend name + volume_backend_name = instorage_iscsi + + .. note:: + When both ``san_password`` and ``san_private_key`` are provide, the driver will use private key prefer to password. + + +#. Save the changes to the ``/etc/cinder/cinder.conf`` file and + restart the ``cinder-volume`` service. diff --git a/releasenotes/notes/inspur-instorage-fc-cinder-driver-70c13e4a64d785d5.yaml b/releasenotes/notes/inspur-instorage-fc-cinder-driver-70c13e4a64d785d5.yaml new file mode 100644 index 00000000000..1e7c5f57674 --- /dev/null +++ b/releasenotes/notes/inspur-instorage-fc-cinder-driver-70c13e4a64d785d5.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + New FC Cinder volume driver for Inspur Instorage.