Merge "Implementation of Cinder driver over FC for Inspur InStorage"
This commit is contained in:
commit
e071cb271c
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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': '<in> 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': '<in> 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': '<in> 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': '<in> 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': '<in> 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': '<in> 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': '<in> 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': '<in> ' + 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)
|
233
cinder/volume/drivers/inspur/instorage/instorage_fc.py
Normal file
233
cinder/volume/drivers/inspur/instorage/instorage_fc.py
Normal file
@ -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
|
@ -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
|
||||
|
@ -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.
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
New FC Cinder volume driver for Inspur Instorage.
|
Loading…
x
Reference in New Issue
Block a user