Inspur Cinder iSCSI driver

Features that Inspur Driver support:
Create, list, delete, attach (map), and detach (unmap) volumes
Create, list, and delete volume snapshots
Copy an image to a volume
Copy a volume to an image
Clone a volume
Extend a volume
Retype a volume
Create a volume from a snapshot
Manage an existing volume
Consistency group create,update,delete
Consistency group snapshot create,delete
Group create,update,delete
Group snapshot create,delete
Replication V2.1

ThirdPartySystems: INSPUR CI

Implements: Blueprint inspur-instorage-driver

Change-Id: I06a8eb38f35ccff125282c8886458bfe99fe196e
This commit is contained in:
wang yong 2017-10-07 22:06:45 +08:00
parent 04fe6f10a6
commit e7362103c6
15 changed files with 9877 additions and 0 deletions

View File

@ -123,6 +123,10 @@ from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_fc as \
from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_iscsi as \
cinder_volume_drivers_ibm_storwize_svc_storwizesvciscsi
from cinder.volume.drivers import infinidat as cinder_volume_drivers_infinidat
from cinder.volume.drivers.inspur.instorage import instorage_common as \
cinder_volume_drivers_inspur_instorage_instoragecommon
from cinder.volume.drivers.inspur.instorage import instorage_iscsi as \
cinder_volume_drivers_inspur_instorage_instorageiscsi
from cinder.volume.drivers.kaminario import kaminario_common as \
cinder_volume_drivers_kaminario_kaminariocommon
from cinder.volume.drivers.lenovo import lenovo_common as \
@ -241,6 +245,10 @@ def list_opts():
[cinder_volume_api.az_cache_time_opt],
cinder_volume_driver.volume_opts,
cinder_volume_driver.iser_opts,
cinder_volume_drivers_inspur_instorage_instoragecommon.
instorage_mcs_opts,
cinder_volume_drivers_inspur_instorage_instorageiscsi.
instorage_mcs_iscsi_opts,
cinder_volume_manager.volume_manager_opts,
cinder_wsgi_eventletserver.socket_opts,
)),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,256 @@
# 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.
"""
import ddt
import mock
from cinder import exception
from cinder import test
from cinder.volume import configuration as conf
from cinder.volume.drivers.inspur.instorage import instorage_common
from cinder.tests.unit.volume.drivers.inspur.instorage import fakes
class CLIParserTestCase(test.TestCase):
def test_empty(self):
self.assertEqual(0, len(
instorage_common.CLIParser('')))
self.assertEqual(0, len(
instorage_common.CLIParser(('', 'stderr'))))
def test_header(self):
raw = r'''id!name
1!node1
2!node2
'''
resp = instorage_common.CLIParser(raw, with_header=True)
self.assertEqual(2, len(resp))
self.assertEqual('1', resp[0]['id'])
self.assertEqual('2', resp[1]['id'])
def test_select(self):
raw = r'''id!123
name!Bill
name!Bill2
age!30
home address!s1
home address!s2
id! 7
name!John
name!John2
age!40
home address!s3
home address!s4
'''
resp = instorage_common.CLIParser(raw, with_header=False)
self.assertEqual([('s1', 'Bill', 's1'), ('s2', 'Bill2', 's2'),
('s3', 'John', 's3'), ('s4', 'John2', 's4')],
list(resp.select('home address', 'name',
'home address')))
def test_lsnode_all(self):
raw = r'''id!name!UPS_serial_number!WWNN!status
1!node1!!500507680200C744!online
2!node2!!500507680200C745!online
'''
resp = instorage_common.CLIParser(raw)
self.assertEqual(2, len(resp))
self.assertEqual('1', resp[0]['id'])
self.assertEqual('500507680200C744', resp[0]['WWNN'])
self.assertEqual('2', resp[1]['id'])
self.assertEqual('500507680200C745', resp[1]['WWNN'])
def test_lsnode_single(self):
raw = r'''id!1
port_id!500507680210C744
port_status!active
port_speed!8Gb
port_id!500507680240C744
port_status!inactive
port_speed!8Gb
'''
resp = instorage_common.CLIParser(raw, with_header=False)
self.assertEqual(1, len(resp))
self.assertEqual('1', resp[0]['id'])
self.assertEqual([('500507680210C744', 'active'),
('500507680240C744', 'inactive')],
list(resp.select('port_id', 'port_status')))
class InStorageAssistantTestCase(test.TestCase):
def setUp(self):
super(InStorageAssistantTestCase, self).setUp()
self.instorage_mcs_common = instorage_common.InStorageAssistant(None)
self.mock_wait_time = mock.patch.object(
instorage_common.InStorageAssistant, "WAIT_TIME", 0)
@mock.patch.object(instorage_common.InStorageSSH, 'lslicense')
@mock.patch.object(instorage_common.InStorageSSH, 'lsguicapabilities')
def test_compression_enabled(self, lsguicapabilities, lslicense):
fake_license_without_keys = {}
fake_license = {
'license_compression_enclosures': '1',
'license_compression_capacity': '1'
}
fake_license_scheme = {
'compression': 'yes'
}
fake_license_invalid_scheme = {
'compression': 'no'
}
lslicense.side_effect = [fake_license_without_keys,
fake_license_without_keys,
fake_license,
fake_license_without_keys]
lsguicapabilities.side_effect = [fake_license_without_keys,
fake_license_invalid_scheme,
fake_license_scheme]
self.assertFalse(self.instorage_mcs_common.compression_enabled())
self.assertFalse(self.instorage_mcs_common.compression_enabled())
self.assertTrue(self.instorage_mcs_common.compression_enabled())
self.assertTrue(self.instorage_mcs_common.compression_enabled())
@mock.patch.object(instorage_common.InStorageAssistant,
'get_vdisk_count_by_io_group')
def test_select_io_group(self, get_vdisk_count_by_io_group):
# given io groups
opts = {}
# system io groups
state = {}
fake_iog_vdc1 = {0: 100, 1: 50, 2: 50, 3: 300}
fake_iog_vdc2 = {0: 2, 1: 1, 2: 200}
fake_iog_vdc3 = {0: 2, 2: 200}
fake_iog_vdc4 = {0: 100, 1: 100, 2: 100, 3: 100}
fake_iog_vdc5 = {0: 10, 1: 1, 2: 200, 3: 300}
get_vdisk_count_by_io_group.side_effect = [fake_iog_vdc1,
fake_iog_vdc2,
fake_iog_vdc3,
fake_iog_vdc4,
fake_iog_vdc5]
opts['iogrp'] = '0,2'
state['available_iogrps'] = [0, 1, 2, 3]
iog = self.instorage_mcs_common.select_io_group(state, opts)
self.assertTrue(iog in state['available_iogrps'])
self.assertEqual(2, iog)
opts['iogrp'] = '0'
state['available_iogrps'] = [0, 1, 2]
iog = self.instorage_mcs_common.select_io_group(state, opts)
self.assertTrue(iog in state['available_iogrps'])
self.assertEqual(0, iog)
opts['iogrp'] = '1,2'
state['available_iogrps'] = [0, 2]
iog = self.instorage_mcs_common.select_io_group(state, opts)
self.assertTrue(iog in state['available_iogrps'])
self.assertEqual(2, iog)
opts['iogrp'] = ' 0, 1, 2 '
state['available_iogrps'] = [0, 1, 2, 3]
iog = self.instorage_mcs_common.select_io_group(state, opts)
self.assertTrue(iog in state['available_iogrps'])
# since vdisk count in all iogroups is same, it will pick the first
self.assertEqual(0, iog)
opts['iogrp'] = '0,1,2, 3'
state['available_iogrps'] = [0, 1, 2, 3]
iog = self.instorage_mcs_common.select_io_group(state, opts)
self.assertTrue(iog in state['available_iogrps'])
self.assertEqual(1, iog)
@ddt.ddt
class InStorageSSHTestCase(test.TestCase):
def setUp(self):
super(InStorageSSHTestCase, self).setUp()
self.fake_driver = fakes.FakeInStorageMCSISCSIDriver(
configuration=conf.Configuration(None))
sim = fakes.FakeInStorage(['openstack'])
self.fake_driver.set_fake_storage(sim)
self.instorage_ssh = instorage_common.InStorageSSH(
self.fake_driver._run_ssh)
def test_mkvdiskhostmap(self):
# mkvdiskhostmap should not be returning anything
self.fake_driver.fake_storage._volumes_list['9999'] = {
'name': ' 9999', 'id': '0', 'uid': '0',
'IO_group_id': '0', 'IO_group_name': 'fakepool'}
self.fake_driver.fake_storage._hosts_list['HOST1'] = {
'name': 'HOST1', 'id': '0', 'host_name': 'HOST1'}
self.fake_driver.fake_storage._hosts_list['HOST2'] = {
'name': 'HOST2', 'id': '1', 'host_name': 'HOST2'}
self.fake_driver.fake_storage._hosts_list['HOST3'] = {
'name': 'HOST3', 'id': '2', 'host_name': 'HOST3'}
ret = self.instorage_ssh.mkvdiskhostmap('HOST1', '9999', '511', False)
self.assertEqual('511', ret)
ret = self.instorage_ssh.mkvdiskhostmap('HOST2', '9999', '512', True)
self.assertEqual('512', ret)
ret = self.instorage_ssh.mkvdiskhostmap('HOST3', '9999', None, True)
self.assertIsNotNone(ret)
with mock.patch.object(
instorage_common.InStorageSSH,
'run_ssh_check_created') as run_ssh_check_created:
ex = exception.VolumeBackendAPIException(data='CMMVC6071E')
run_ssh_check_created.side_effect = ex
self.assertRaises(exception.VolumeBackendAPIException,
self.instorage_ssh.mkvdiskhostmap,
'HOST3', '9999', 511, True)
@ddt.data((exception.VolumeBackendAPIException(data='CMMVC6372W'), None),
(exception.VolumeBackendAPIException(data='CMMVC6372W'),
{'name': 'fakevol', 'id': '0', 'uid': '0', 'IO_group_id': '0',
'IO_group_name': 'fakepool'}),
(exception.VolumeBackendAPIException(data='error'), None))
@ddt.unpack
def test_mkvdisk_with_warning(self, run_ssh_check, lsvol):
opt = {'iogrp': 0}
with mock.patch.object(instorage_common.InStorageSSH,
'run_ssh_check_created',
side_effect=run_ssh_check):
with mock.patch.object(instorage_common.InStorageSSH, 'lsvdisk',
return_value=lsvol):
if lsvol:
ret = self.instorage_ssh.mkvdisk('fakevol', '1', 'gb',
'fakepool', opt, [])
self.assertEqual('0', ret)
else:
self.assertRaises(exception.VolumeBackendAPIException,
self.instorage_ssh.mkvdisk,
'fakevol', '1', 'gb', 'fakepool',
opt, [])

View File

@ -0,0 +1,430 @@
# 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
import six
from cinder import context
from cinder import exception
from cinder import test
from cinder.tests.unit import utils as testutils
from cinder.volume import configuration as conf
from cinder.volume.drivers.inspur.instorage import instorage_iscsi
from cinder.volume import volume_types
from cinder.tests.unit.volume.drivers.inspur.instorage import fakes
class InStorageMCSISCSIDriverTestCase(test.TestCase):
@mock.patch.object(greenthread, 'sleep')
def setUp(self, mock_sleep):
super(InStorageMCSISCSIDriverTestCase, self).setUp()
self.iscsi_driver = fakes.FakeInStorageMCSISCSIDriver(
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 = ['1234567890123456', '6543210987654321']
initiator = 'test.initiator.%s' % 123456
self._connector = {'ip': '1.234.56.78',
'host': 'instorage-mcs-test',
'wwpns': wwpns,
'initiator': initiator}
self.sim = fakes.FakeInStorage(['openstack'])
self.iscsi_driver.set_fake_storage(self.sim)
self.ctxt = context.get_admin_context()
self._reset_flags()
self.ctxt = context.get_admin_context()
db_driver = self.iscsi_driver.configuration.db_driver
self.db = importutils.import_module(db_driver)
self.iscsi_driver.db = self.db
self.iscsi_driver.do_setup(None)
self.iscsi_driver.check_for_setup_error()
self.iscsi_driver._assistant.check_lcmapping_interval = 0
def _set_flag(self, flag, value):
group = self.iscsi_driver.configuration.config_group
self.iscsi_driver.configuration.set_override(flag, value, group)
def _reset_flags(self):
self.iscsi_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.iscsi_driver.create_volume(vol)
return vol
def _delete_volume(self, volume):
self.iscsi_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.iscsi_driver._assistant.is_vdisk_defined(name)
self.assertEqual(exists, is_vol_defined)
def test_instorage_mcs_iscsi_validate_connector(self):
conn_neither = {'host': 'host'}
conn_iscsi = {'host': 'host', 'initiator': 'foo'}
conn_fc = {'host': 'host', 'wwpns': 'bar'}
conn_both = {'host': 'host', 'initiator': 'foo', 'wwpns': 'bar'}
self.iscsi_driver._state['enabled_protocols'] = set(['iSCSI'])
self.iscsi_driver.validate_connector(conn_iscsi)
self.iscsi_driver.validate_connector(conn_both)
self.assertRaises(exception.InvalidConnectorException,
self.iscsi_driver.validate_connector, conn_fc)
self.assertRaises(exception.InvalidConnectorException,
self.iscsi_driver.validate_connector, conn_neither)
self.iscsi_driver._state['enabled_protocols'] = set(['iSCSI', 'FC'])
self.iscsi_driver.validate_connector(conn_iscsi)
self.iscsi_driver.validate_connector(conn_both)
self.assertRaises(exception.InvalidConnectorException,
self.iscsi_driver.validate_connector, conn_neither)
def test_instorage_terminate_iscsi_connection(self):
# create a iSCSI volume
volume_iSCSI = self._create_volume()
extra_spec = {'capabilities:storage_protocol': '<in> iSCSI'}
vol_type_iSCSI = volume_types.create(self.ctxt, 'iSCSI', extra_spec)
volume_iSCSI['volume_type_id'] = vol_type_iSCSI['id']
connector = {'host': 'instorage-mcs-host',
'wwnns': ['20000090fa17311e', '20000090fa17311f'],
'wwpns': ['ff00000000000000', 'ff00000000000001'],
'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1aaa'}
self.iscsi_driver.initialize_connection(volume_iSCSI, connector)
self.iscsi_driver.terminate_connection(volume_iSCSI, connector)
@mock.patch.object(instorage_iscsi.InStorageMCSISCSIDriver,
'_do_terminate_connection')
def test_instorage_initialize_iscsi_connection_failure(self, term_conn):
# create a iSCSI volume
volume_iSCSI = self._create_volume()
extra_spec = {'capabilities:storage_protocol': '<in> iSCSI'}
vol_type_iSCSI = volume_types.create(self.ctxt, 'iSCSI', extra_spec)
volume_iSCSI['volume_type_id'] = vol_type_iSCSI['id']
connector = {'host': 'instorage-mcs-host',
'wwnns': ['20000090fa17311e', '20000090fa17311f'],
'wwpns': ['ff00000000000000', 'ff00000000000001'],
'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1aaa'}
self.iscsi_driver._state['storage_nodes'] = {}
self.assertRaises(exception.VolumeBackendAPIException,
self.iscsi_driver.initialize_connection,
volume_iSCSI, connector)
term_conn.assert_called_once_with(volume_iSCSI, connector)
def test_instorage_initialize_iscsi_connection_single_path(self):
# Test the return value for _get_iscsi_properties
connector = {'host': 'instorage-mcs-host',
'wwnns': ['20000090fa17311e', '20000090fa17311f'],
'wwpns': ['ff00000000000000', 'ff00000000000001'],
'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1aaa'}
# Expected single path host-volume map return value
exp_s_path = {'driver_volume_type': 'iscsi',
'data': {'target_discovered': False,
'target_iqn':
'iqn.1982-01.com.inspur:1234.sim.node1',
'target_portal': '1.234.56.78:3260',
'target_lun': 0,
'auth_method': 'CHAP',
'discovery_auth_method': 'CHAP'}}
volume_iSCSI = self._create_volume()
extra_spec = {'capabilities:storage_protocol': '<in> iSCSI'}
vol_type_iSCSI = volume_types.create(self.ctxt, 'iSCSI', extra_spec)
volume_iSCSI['volume_type_id'] = vol_type_iSCSI['id']
# Make sure that the volumes have been created
self._assert_vol_exists(volume_iSCSI['name'], True)
# Check case where no hosts exist
ret = self.iscsi_driver._assistant.get_host_from_connector(
connector)
self.assertIsNone(ret)
# Initialize connection to map volume to a host
ret = self.iscsi_driver.initialize_connection(
volume_iSCSI, connector)
self.assertEqual(exp_s_path['driver_volume_type'],
ret['driver_volume_type'])
# Check the single path host-volume map return value
for k, v in exp_s_path['data'].items():
self.assertEqual(v, ret['data'][k])
ret = self.iscsi_driver._assistant.get_host_from_connector(
connector)
self.assertIsNotNone(ret)
def test_instorage_initialize_iscsi_connection_multipath(self):
# Test the return value for _get_iscsi_properties
connector = {'host': 'instorage-mcs-host',
'wwnns': ['20000090fa17311e', '20000090fa17311f'],
'wwpns': ['ff00000000000000', 'ff00000000000001'],
'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1aaa',
'multipath': True}
# Expected multipath host-volume map return value
exp_m_path = {'driver_volume_type': 'iscsi',
'data': {'target_discovered': False,
'target_iqn':
'iqn.1982-01.com.inspur:1234.sim.node1',
'target_portal': '1.234.56.78:3260',
'target_lun': 0,
'target_iqns': [
'iqn.1982-01.com.inspur:1234.sim.node1',
'iqn.1982-01.com.inspur:1234.sim.node1',
'iqn.1982-01.com.inspur:1234.sim.node2'],
'target_portals':
['1.234.56.78:3260',
'1.234.56.80:3260',
'1.234.56.79:3260'],
'target_luns': [0, 0, 0],
'auth_method': 'CHAP',
'discovery_auth_method': 'CHAP'}}
volume_iSCSI = self._create_volume()
extra_spec = {'capabilities:storage_protocol': '<in> iSCSI'}
vol_type_iSCSI = volume_types.create(self.ctxt, 'iSCSI', extra_spec)
volume_iSCSI['volume_type_id'] = vol_type_iSCSI['id']
# Check case where no hosts exist
ret = self.iscsi_driver._assistant.get_host_from_connector(
connector)
self.assertIsNone(ret)
# Initialize connection to map volume to a host
ret = self.iscsi_driver.initialize_connection(
volume_iSCSI, connector)
self.assertEqual(exp_m_path['driver_volume_type'],
ret['driver_volume_type'])
# Check the multipath host-volume map return value
for k, v in exp_m_path['data'].items():
self.assertEqual(v, ret['data'][k])
ret = self.iscsi_driver._assistant.get_host_from_connector(
connector)
self.assertIsNotNone(ret)
def test_instorage_mcs_iscsi_host_maps(self):
# Create two volumes to be used in mappings
ctxt = context.get_admin_context()
volume1 = self._generate_vol_info(None, None)
self.iscsi_driver.create_volume(volume1)
volume2 = self._generate_vol_info(None, None)
self.iscsi_driver.create_volume(volume2)
# Create volume types that we created
types = {}
for protocol in ['iSCSI']:
opts = {'storage_protocol': '<in> ' + protocol}
types[protocol] = volume_types.create(ctxt, protocol, opts)
expected = {'iSCSI': {'driver_volume_type': 'iscsi',
'data': {'target_discovered': False,
'target_iqn':
'iqn.1982-01.com.inspur:1234.sim.node1',
'target_portal': '1.234.56.78:3260',
'target_lun': 0,
'auth_method': 'CHAP',
'discovery_auth_method': 'CHAP'}}}
volume1['volume_type_id'] = types[protocol]['id']
volume2['volume_type_id'] = types[protocol]['id']
# Check case where no hosts exist
ret = self.iscsi_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.iscsi_driver.initialize_connection(
volume1, self._connector)
self.assertEqual(expected[protocol]['driver_volume_type'],
ret['driver_volume_type'])
for k, v in expected[protocol]['data'].items():
self.assertEqual(v, ret['data'][k])
# Initialize again, should notice it and do nothing
ret = self.iscsi_driver.initialize_connection(
volume1, self._connector)
self.assertEqual(expected[protocol]['driver_volume_type'],
ret['driver_volume_type'])
for k, v in expected[protocol]['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.iscsi_driver.delete_volume,
volume1)
ret = self.iscsi_driver.terminate_connection(volume1,
self._connector)
ret = self.iscsi_driver._assistant.get_host_from_connector(
self._connector)
self.assertIsNone(ret)
# Check cases with no auth set for host
for auth_enabled in [True, False]:
for host_exists in ['yes-auth', 'yes-noauth', 'no']:
self._set_flag('instorage_mcs_iscsi_chap_enabled',
auth_enabled)
case = 'en' + six.text_type(
auth_enabled) + 'ex' + six.text_type(host_exists)
conn_na = {'initiator': 'test:init:%s' % 56789,
'ip': '11.11.11.11',
'host': 'host-%s' % case}
if host_exists.startswith('yes'):
self.sim._add_host_to_list(conn_na)
if host_exists == 'yes-auth':
kwargs = {'chapsecret': 'foo',
'obj': conn_na['host']}
self.sim._cmd_chhost(**kwargs)
volume1['volume_type_id'] = types['iSCSI']['id']
init_ret = self.iscsi_driver.initialize_connection(volume1,
conn_na)
host_name = self.sim._host_in_list(conn_na['host'])
chap_ret = (
self.iscsi_driver._assistant.get_chap_secret_for_host(
host_name))
if auth_enabled or host_exists == 'yes-auth':
self.assertIn('auth_password', init_ret['data'])
self.assertIsNotNone(chap_ret)
else:
self.assertNotIn('auth_password', init_ret['data'])
self.assertIsNone(chap_ret)
self.iscsi_driver.terminate_connection(volume1, conn_na)
self._set_flag('instorage_mcs_iscsi_chap_enabled', True)
# Test no preferred node
self.sim.error_injection('lsvdisk', 'no_pref_node')
self.assertRaises(exception.VolumeBackendAPIException,
self.iscsi_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.iscsi_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.iscsi_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.iscsi_driver.create_volume(unmapped_vol)
self.iscsi_driver.terminate_connection(unmapped_vol, self._connector)
self.iscsi_driver.delete_volume(unmapped_vol)
# Remove the mapping from the 1st volume and delete it
self.iscsi_driver.terminate_connection(volume1, self._connector)
self.iscsi_driver.delete_volume(volume1)
self._assert_vol_exists(volume1['name'], False)
# Make sure our host still exists
host_name = self.iscsi_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.iscsi_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.iscsi_driver.initialize_connection(volume2, self._connector)
host_name = self.iscsi_driver._assistant.get_host_from_connector(
self._connector)
self.assertIsNotNone(host_name)
self.iscsi_driver.terminate_connection(volume2, fake_conn)
host_name = self.iscsi_driver._assistant.get_host_from_connector(
self._connector)
self.assertIsNone(host_name)
self.iscsi_driver.delete_volume(volume2)
self._assert_vol_exists(volume2['name'], False)
# Delete volume types that we created
for protocol in ['iSCSI']:
volume_types.destroy(ctxt, types[protocol]['id'])
# Check if our host still exists (it should not)
ret = (self.iscsi_driver._assistant.get_host_from_connector(
self._connector))
self.assertIsNone(ret)
def test_add_vdisk_copy_iscsi(self):
# Ensure only iSCSI is available
self.iscsi_driver._state['enabled_protocols'] = set(['iSCSI'])
volume = self._generate_vol_info(None, None)
self.iscsi_driver.create_volume(volume)
self.iscsi_driver.add_vdisk_copy(volume['name'], 'fake-pool', None)

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
# 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.
#
DEV_MODEL_INSTORAGE = '1813'
DEV_MODEL_INSTORAGE_AS5X00 = '2076'
REP_CAP_DEVS = (DEV_MODEL_INSTORAGE, DEV_MODEL_INSTORAGE_AS5X00)
# constants used for replication
ASYNC = 'async'
SYNC = 'sync'
VALID_REP_TYPES = (ASYNC, SYNC)
FAILBACK_VALUE = 'default'
DEFAULT_RC_TIMEOUT = 3600 * 24 * 7
DEFAULT_RC_INTERVAL = 5
REPLICA_AUX_VOL_PREFIX = 'aux_'
# remote mirror copy status
REP_CONSIS_SYNC = 'consistent_synchronized'
REP_CONSIS_STOP = 'consistent_stopped'
REP_SYNC = 'synchronized'
REP_IDL = 'idling'
REP_IDL_DISC = 'idling_disconnected'
REP_STATUS_ON_LINE = 'online'

View File

@ -0,0 +1,298 @@
# 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.
#
"""
ISCSI volume driver for Inspur InStorage family and MCS storage systems.
Notes:
1. Make sure you config the password or key file. If you specify both
a password and a key file, this driver will use the key file only.
2. When a key file is used for authentication, the private key is stored
in a secure manner by the user or system administrator.
3. The defaults for creating volumes are
"-rsize 2% -autoexpand -grainsize 256 -warning 0".
These can be changed in the configuration file
or by using volume types(recommended only for advanced users).
Limitations:
1. The driver expects CLI output in English,
but the error messages may be in a localized format.
2. when you clone or create volumes from snapshots,
it not support that the source and target_rep are different size.
Perform necessary work to make an iSCSI connection:
To be able to create an iSCSI connection from a given host to a volume,
we must:
1. Translate the given iSCSI name 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)
"""
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
import six
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
LOG = logging.getLogger(__name__)
instorage_mcs_iscsi_opts = [
cfg.BoolOpt('instorage_mcs_iscsi_chap_enabled',
default=True,
help='Configure CHAP authentication for iSCSI connections '
'(Default: Enabled)'),
]
CONF = cfg.CONF
CONF.register_opts(instorage_mcs_iscsi_opts)
@interface.volumedriver
class InStorageMCSISCSIDriver(instorage_common.InStorageMCSCommonDriver,
driver.ISCSIDriver):
"""Inspur InStorage iSCSI 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(InStorageMCSISCSIDriver, self).__init__(*args, **kwargs)
self.protocol = 'iSCSI'
self.configuration.append_config_values(
instorage_mcs_iscsi_opts)
@cinder_utils.trace
@coordination.synchronized('instorage-host'
'{self._state[system_id]}'
'{connector[host]}')
def initialize_connection(self, volume, connector):
"""Perform necessary work to make an iSCSI connection."""
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)
chap_secret = self._assistant.get_chap_secret_for_host(host_name)
chap_enabled = self.configuration.instorage_mcs_iscsi_chap_enabled
if chap_enabled and chap_secret is None:
chap_secret = self._assistant.add_chap_secret_to_host(host_name)
elif not chap_enabled and chap_secret:
LOG.warning('CHAP secret exists for host but CHAP is disabled.')
lun_id = self._assistant.map_vol_to_host(volume_name,
host_name,
False)
try:
properties = self._get_single_iscsi_data(volume, connector,
lun_id, chap_secret)
multipath = connector.get('multipath', False)
if multipath:
properties = self._get_multi_iscsi_data(volume, connector,
lun_id, properties)
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': 'iscsi', 'data': properties}
@cinder_utils.trace
def _get_single_iscsi_data(self, volume, connector, lun_id, chap_secret):
volume_name = self._get_target_vol(volume)
volume_attributes = self._assistant.get_vdisk_attributes(volume_name)
if volume_attributes is None:
msg = (_('_get_single_iscsi_data: Failed to get attributes'
' for volume %s.') % volume_name)
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
try:
preferred_node = volume_attributes['preferred_node_id']
IO_group = volume_attributes['IO_group_id']
except KeyError as e:
msg = (_('_get_single_iscsi_data: Did not find expected column'
' name in %(volume)s: %(key)s %(error)s.'),
{'volume': volume_name, 'key': e.args[0],
'error': e})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# 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 self.protocol not in node['enabled_protocols']:
continue
if node['IO_group'] != IO_group:
continue
io_group_nodes.append(node)
if node['id'] == preferred_node:
preferred_node_entry = node
if not len(io_group_nodes):
msg = (_('_get_single_iscsi_data: 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('_get_single_iscsi_data: Did not find a '
'preferred node for volume %s.', volume_name)
properties = {
'target_discovered': False,
'target_lun': lun_id,
'volume_id': volume.id}
if preferred_node_entry['ipv4']:
ipaddr = preferred_node_entry['ipv4'][0]
else:
ipaddr = '[%s]' % preferred_node_entry['ipv6'][0]
# ipv6 need surround with brackets when it use port
properties['target_portal'] = '%s:%s' % (ipaddr, '3260')
properties['target_iqn'] = preferred_node_entry['iscsi_name']
if chap_secret:
properties.update(auth_method='CHAP',
auth_username=connector['initiator'],
auth_password=chap_secret,
discovery_auth_method='CHAP',
discovery_auth_username=connector['initiator'],
discovery_auth_password=chap_secret)
return properties
@cinder_utils.trace
def _get_multi_iscsi_data(self, volume, connector, lun_id, properties):
try:
resp = self._assistant.ssh.lsportip()
except Exception as ex:
msg = (_('_get_multi_iscsi_data: Failed to '
'get port ip because of exception: '
'%s.') % six.text_type(ex))
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
properties['target_iqns'] = []
properties['target_portals'] = []
properties['target_luns'] = []
for node in self._state['storage_nodes'].values():
for ip_data in resp:
if ip_data['node_id'] != node['id']:
continue
link_state = ip_data.get('link_state', None)
valid_port = ''
if ((ip_data['state'] == 'configured' and
link_state == 'active') or
ip_data['state'] == 'online'):
valid_port = (ip_data['IP_address'] or
ip_data['IP_address_6'])
if valid_port:
properties['target_portals'].append(
'%s:%s' % (valid_port, '3260'))
properties['target_iqns'].append(
node['iscsi_name'])
properties['target_luns'].append(lun_id)
if not len(properties['target_portals']):
msg = (_('_get_multi_iscsi_data: Failed to find valid port '
'for volume %s.') % volume.name)
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return properties
def terminate_connection(self, volume, connector, **kwargs):
"""Cleanup after an iSCSI 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 iSCSI 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 iSCSI protocol
info = {'driver_volume_type': 'iscsi',
'data': {}}
host_name = self._assistant.get_host_from_connector(connector)
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):
self._assistant.delete_host(host_name)
return info

View File

@ -0,0 +1,240 @@
# 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.
#
import random
from eventlet import greenthread
from oslo_concurrency import processutils
from oslo_log import log as logging
from oslo_utils import excutils
import six
from cinder import exception
from cinder.i18n import _
from cinder.objects import fields
from cinder import ssh_utils
from cinder import utils as cinder_utils
from cinder.volume.drivers.inspur.instorage import instorage_const
LOG = logging.getLogger(__name__)
class InStorageMCSReplicationManager(object):
def __init__(self, driver, replication_target=None, target_assistant=None):
self.sshpool = None
self.driver = driver
self.target = replication_target
self.target_assistant = target_assistant(self._run_ssh)
self._local_assistant = self.driver._local_backend_assistant
self.async_m = InStorageMCSReplicationAsyncCopy(
self.driver, replication_target, self.target_assistant)
self.sync_m = InStorageMCSReplicationSyncCopy(
self.driver, replication_target, self.target_assistant)
def _run_ssh(self, cmd_list, check_exit_code=True, attempts=1):
cinder_utils.check_ssh_injection(cmd_list)
command = ' '. join(cmd_list)
if not self.sshpool:
self.sshpool = ssh_utils.SSHPool(
self.target.get('san_ip'),
self.target.get('san_ssh_port', 22),
self.target.get('ssh_conn_timeout', 30),
self.target.get('san_login'),
password=self.target.get('san_password'),
privatekey=self.target.get('san_private_key', ''),
min_size=self.target.get('ssh_min_pool_conn', 1),
max_size=self.target.get('ssh_max_pool_conn', 5),)
last_exception = None
try:
with self.sshpool.item() as ssh:
while attempts > 0:
attempts -= 1
try:
return processutils.ssh_execute(
ssh, command, check_exit_code=check_exit_code)
except Exception as e:
LOG.error(e)
last_exception = e
greenthread.sleep(random.randint(20, 500) / 100.0)
try:
raise processutils.ProcessExecutionError(
exit_code=last_exception.exit_code,
stdout=last_exception.stdout,
stderr=last_exception.stderr,
cmd=last_exception.cmd)
except AttributeError:
raise processutils.ProcessExecutionError(
exit_code=-1, stdout="",
stderr="Error running SSH command",
cmd=command)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error("Error running SSH command: %s", command)
def get_target_assistant(self):
return self.target_assistant
def get_replica_obj(self, rep_type):
if rep_type == instorage_const.ASYNC:
return self.async_m
elif rep_type == instorage_const.SYNC:
return self.sync_m
else:
return None
def _partnership_validate_create(self, client, remote_name, remote_ip):
try:
partnership_info = client.get_partnership_info(remote_name)
if not partnership_info:
candidate_info = client.get_partnershipcandidate_info(
remote_name)
if candidate_info:
client.mkfcpartnership(remote_name)
else:
client.mkippartnership(remote_ip)
partnership_info = client.get_partnership_info(remote_name)
if partnership_info['partnership'] != 'fully_configured':
client.chpartnership(partnership_info['id'])
except Exception:
msg = (_('Unable to establish the partnership with '
'the InStorage cluster %s.') % remote_name)
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
def establish_target_partnership(self):
local_system_info = self._local_assistant.get_system_info()
target_system_info = self.target_assistant.get_system_info()
local_system_name = local_system_info['system_name']
target_system_name = target_system_info['system_name']
local_ip = self.driver.configuration.safe_get('san_ip')
target_ip = self.target.get('san_ip')
# Establish partnership only when the local system and the replication
# target system is different.
if target_system_name != local_system_name:
self._partnership_validate_create(self._local_assistant,
target_system_name, target_ip)
self._partnership_validate_create(self.target_assistant,
local_system_name, local_ip)
class InStorageMCSReplication(object):
def __init__(self, asynccopy, driver,
replication_target=None, target_assistant=None):
self.asynccopy = asynccopy
self.driver = driver
self.target = replication_target or {}
self.target_assistant = target_assistant
@cinder_utils.trace
def volume_replication_setup(self, context, vref):
target_vol_name = instorage_const.REPLICA_AUX_VOL_PREFIX + vref.name
try:
attr = self.target_assistant.get_vdisk_attributes(target_vol_name)
if not attr:
opts = self.driver._get_vdisk_params(vref.volume_type_id)
pool = self.target.get('pool_name')
src_attr = self.driver._assistant.get_vdisk_attributes(
vref.name)
opts['iogrp'] = src_attr['IO_group_id']
self.target_assistant.create_vdisk(target_vol_name,
six.text_type(vref['size']),
'gb', pool, opts)
system_info = self.target_assistant.get_system_info()
self.driver._assistant.create_relationship(
vref.name, target_vol_name, system_info.get('system_name'),
self.asynccopy)
except Exception as e:
msg = (_("Unable to set up copy mode replication for %(vol)s. "
"Exception: %(err)s.") % {'vol': vref.id, 'err': e})
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
@cinder_utils.trace
def failover_volume_host(self, context, vref):
target_vol = instorage_const.REPLICA_AUX_VOL_PREFIX + vref.name
try:
rel_info = self.target_assistant.get_relationship_info(target_vol)
# Reverse the role of the primary and secondary volumes
self.target_assistant.switch_relationship(rel_info['name'])
return {'replication_status': fields.ReplicationStatus.FAILED_OVER}
except Exception as e:
LOG.exception('Unable to fail-over the volume %(id)s to the '
'secondary back-end by switchrcrelationship '
'command.', {"id": vref.id})
# If the switch command fail, try to make the aux volume
# writeable again.
try:
self.target_assistant.stop_relationship(target_vol,
access=True)
return {
'replication_status': fields.ReplicationStatus.FAILED_OVER}
except Exception as e:
msg = (_('Unable to fail-over the volume %(id)s to the '
'secondary back-end, error: %(error)s') %
{"id": vref.id, "error": six.text_type(e)})
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
def replication_failback(self, volume):
tgt_volume = instorage_const.REPLICA_AUX_VOL_PREFIX + volume.name
rel_info = self.target_assistant.get_relationship_info(tgt_volume)
if rel_info:
try:
self.target_assistant.switch_relationship(rel_info['name'],
aux=False)
return {'replication_status': fields.ReplicationStatus.ENABLED,
'status': 'available'}
except Exception as e:
msg = (_('Unable to fail-back the volume:%(vol)s to the '
'master back-end, error:%(error)s') %
{"vol": volume.name, "error": six.text_type(e)})
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
class InStorageMCSReplicationAsyncCopy(InStorageMCSReplication):
"""Support for InStorage/MCS async copy mode replication.
Async Copy establishes a Async Copy relationship between
two volumes of equal size. The volumes in a Async Copy relationship
are referred to as the master (source) volume and the auxiliary
(target) volume. This mode is dedicated to the asynchronous volume
replication.
"""
def __init__(self, driver, replication_target=None, target_assistant=None):
super(InStorageMCSReplicationAsyncCopy, self).__init__(
True, driver, replication_target, target_assistant)
class InStorageMCSReplicationSyncCopy(InStorageMCSReplication):
"""Support for InStorage/MCS sync copy mode replication.
Sync Copy establishes a Sync Copy relationship between
two volumes of equal size. The volumes in a Sync Copy relationship
are referred to as the master (source) volume and the auxiliary
(target) volume.
"""
def __init__(self, driver, replication_target=None, target_assistant=None):
super(InStorageMCSReplicationSyncCopy, self).__init__(
False, driver, replication_target, target_assistant)

View File

@ -0,0 +1,5 @@
---
features:
- |
New Cinder volume driver for Inspur InStorage.
The new driver supports iSCSI.