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:
parent
04fe6f10a6
commit
e7362103c6
@ -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,
|
||||
)),
|
||||
|
0
cinder/tests/unit/volume/drivers/inspur/__init__.py
Normal file
0
cinder/tests/unit/volume/drivers/inspur/__init__.py
Normal file
2194
cinder/tests/unit/volume/drivers/inspur/instorage/fakes.py
Normal file
2194
cinder/tests/unit/volume/drivers/inspur/instorage/fakes.py
Normal file
File diff suppressed because it is too large
Load Diff
1775
cinder/tests/unit/volume/drivers/inspur/instorage/test_common.py
Normal file
1775
cinder/tests/unit/volume/drivers/inspur/instorage/test_common.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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, [])
|
@ -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
0
cinder/volume/drivers/inspur/__init__.py
Normal file
0
cinder/volume/drivers/inspur/__init__.py
Normal file
0
cinder/volume/drivers/inspur/instorage/__init__.py
Normal file
0
cinder/volume/drivers/inspur/instorage/__init__.py
Normal file
3629
cinder/volume/drivers/inspur/instorage/instorage_common.py
Normal file
3629
cinder/volume/drivers/inspur/instorage/instorage_common.py
Normal file
File diff suppressed because it is too large
Load Diff
40
cinder/volume/drivers/inspur/instorage/instorage_const.py
Normal file
40
cinder/volume/drivers/inspur/instorage/instorage_const.py
Normal 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'
|
298
cinder/volume/drivers/inspur/instorage/instorage_iscsi.py
Normal file
298
cinder/volume/drivers/inspur/instorage/instorage_iscsi.py
Normal 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
|
240
cinder/volume/drivers/inspur/instorage/replication.py
Normal file
240
cinder/volume/drivers/inspur/instorage/replication.py
Normal 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)
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
New Cinder volume driver for Inspur InStorage.
|
||||
The new driver supports iSCSI.
|
Loading…
Reference in New Issue
Block a user