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 \
|
from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_iscsi as \
|
||||||
cinder_volume_drivers_ibm_storwize_svc_storwizesvciscsi
|
cinder_volume_drivers_ibm_storwize_svc_storwizesvciscsi
|
||||||
from cinder.volume.drivers import infinidat as cinder_volume_drivers_infinidat
|
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 \
|
from cinder.volume.drivers.kaminario import kaminario_common as \
|
||||||
cinder_volume_drivers_kaminario_kaminariocommon
|
cinder_volume_drivers_kaminario_kaminariocommon
|
||||||
from cinder.volume.drivers.lenovo import lenovo_common as \
|
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_api.az_cache_time_opt],
|
||||||
cinder_volume_driver.volume_opts,
|
cinder_volume_driver.volume_opts,
|
||||||
cinder_volume_driver.iser_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_volume_manager.volume_manager_opts,
|
||||||
cinder_wsgi_eventletserver.socket_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