From 2bdc0867839ae7c2ca8a669a137c270413ec38d3 Mon Sep 17 00:00:00 2001 From: yangheng Date: Wed, 3 Nov 2021 17:41:29 +0800 Subject: [PATCH] Support Cinder FC driver for TOYOU NetStor * Supported Protocol - iSCSI - FC * Supported Feature - Volume Attach/Detach(FC) - Extend Attached Volume - Volume Manage/Unmanage - Revert to Snapshot - Multi-attach - Thin Provisioning ThirdPartySystems: TOYOU ACS5000 CI Change-Id: Id9bd2f880ea92e9f74ba286a1cb25aea174328c5 --- .../unit/volume/drivers/toyou/test_acs5000.py | 782 +++++++++++++++--- .../drivers/toyou/acs5000/acs5000_common.py | 364 +++++--- .../drivers/toyou/acs5000/acs5000_fc.py | 165 ++++ .../drivers/toyou/acs5000/acs5000_iscsi.py | 78 +- .../drivers/toyou-acs5000-driver.rst | 72 -- .../drivers/toyou-netstor-driver.rst | 106 +++ doc/source/reference/support-matrix.ini | 24 +- ...ge-acs5000-fc-driver-f0d7428924bfeda1.yaml | 4 + 8 files changed, 1266 insertions(+), 329 deletions(-) create mode 100644 cinder/volume/drivers/toyou/acs5000/acs5000_fc.py delete mode 100644 doc/source/configuration/block-storage/drivers/toyou-acs5000-driver.rst create mode 100644 doc/source/configuration/block-storage/drivers/toyou-netstor-driver.rst create mode 100644 releasenotes/notes/toyou-netstor-storage-acs5000-fc-driver-f0d7428924bfeda1.yaml diff --git a/cinder/tests/unit/volume/drivers/toyou/test_acs5000.py b/cinder/tests/unit/volume/drivers/toyou/test_acs5000.py index e2efb3d64ce..74d216d6e6a 100644 --- a/cinder/tests/unit/volume/drivers/toyou/test_acs5000.py +++ b/cinder/tests/unit/volume/drivers/toyou/test_acs5000.py @@ -33,12 +33,15 @@ import paramiko from cinder import context import cinder.db from cinder import exception +from cinder.objects import volume_attachment from cinder import ssh_utils +from cinder.tests.unit import fake_constants as fake from cinder.tests.unit import test from cinder.tests.unit import utils as testutils from cinder import utils as cinder_utils from cinder.volume import configuration as conf from cinder.volume.drivers.toyou.acs5000 import acs5000_common +from cinder.volume.drivers.toyou.acs5000 import acs5000_fc from cinder.volume.drivers.toyou.acs5000 import acs5000_iscsi POOLS_NAME = ['pool01', 'pool02'] @@ -91,38 +94,66 @@ class CommandSimulator(object): 'disk-array-000f12345:dev0.ctr2', 'WWNN': '200008CA45D33768', 'status': 'online'}] - self._ip_list = { - '0': [{ - 'ctrl_idx': 0, - 'id': 0, - 'name': 'lan1', - 'ip': '10.23.45.67', - 'mask': '255.255.255.0', - 'gw': '' - }, { - 'ctrl_idx': 0, - 'id': 1, - 'name': 'lan2', - 'ip': '10.23.45.68', - 'mask': '255.255.255.0', - 'gw': '' - }], - '1': [{ - 'ctrl_idx': 1, - 'id': 0, - 'name': 'lan1', - 'ip': '10.23.45.69', - 'mask': '255.255.255.0', - 'gw': '' - }, { - 'ctrl_idx': 1, - 'id': 1, - 'name': 'lan2', - 'ip': '10.23.45.70', - 'mask': '255.255.255.0', - 'gw': '' - }] - } + self._iscsi_list = [ + {'ctrl_idx': 0, + 'id': 0, + 'name': 'lan1', + 'ip': '10.23.45.67', + 'mask': '255.255.255.0', + 'gw': '', + 'link': '1 Gb/s'}, + {'ctrl_idx': 0, + 'id': 1, + 'name': 'lan2', + 'ip': '10.23.45.68', + 'mask': '255.255.255.0', + 'gw': '', + 'link': '1 Gb/s'}, + {'ctrl_idx': 0, + 'id': 2, + 'name': 'lan3', + 'ip': '10.23.45.69', + 'mask': '255.255.255.0', + 'gw': '', + 'link': '1 Gb/s'}, + {'ctrl_idx': 1, + 'id': 0, + 'name': 'lan1', + 'ip': '10.23.45.70', + 'mask': '255.255.255.0', + 'gw': '', + 'link': '1 Gb/s'}, + {'ctrl_idx': 1, + 'id': 1, + 'name': 'lan2', + 'ip': '10.23.45.71', + 'mask': '255.255.255.0', + 'gw': '', + 'link': 'Down'}, + {'ctrl_idx': 1, + 'id': 2, + 'name': 'lan3', + 'ip': '10.23.45.72', + 'mask': '255.255.255.0', + 'gw': '', + 'link': 'Down'}] + self._fc_list = [ + {'ctrl_idx': 0, + 'WWPN': str(random.randint(0, 9999999999999999)).zfill(16), + 'link': 'Up'}, + {'ctrl_idx': 0, + 'wwpn': str(random.randint(0, 9999999999999999)).zfill(16), + 'link': 'Up'}, + {'ctrl_idx': 0, + 'link': 'Up'}, + {'ctrl_idx': 1, + 'WWPN': str(random.randint(0, 9999999999999999)).zfill(16), + 'link': 'Up'}, + {'ctrl_idx': 1, + 'link': 'Up'}, + {'ctrl_idx': 1, + 'wwpn': str(random.randint(0, 9999999999999999)).zfill(16), + 'link': 'Down'}] self._system_info = {'version': '3.1.2.345678', 'vendor': 'TOYOU', 'system_name': 'Disk-Array', @@ -153,33 +184,27 @@ class CommandSimulator(object): 'already exists on the system.'), 'volume_extend_min': (321, 'A volume capacity shall not be' ' less than the current size'), - 'lun_not_exist': (401, 'The volume does not exist ' + 'volume_extend_size_equal': (322, 'A volume capacity shall not ' + 'be equal to than the ' + 'current size'), + 'lun_not_exist': (401, 'The volume does not exist ' 'on the system.'), 'not_available_lun': (402, 'The system have no available lun.'), + 'host_empty': (403, 'The host is empty.'), 'snap_over_system': (503, 'The system snapshots maximum quantity ' 'has been reached.'), 'snap_over_volume': (504, 'A volume snapshots maximum quantity ' 'has been reached.'), 'snap_not_exist': (505, 'The snapshot does not exist ' - 'on the system.') + 'on the system.'), + 'snap_not_latest': (506, 'The snapshot is not the latest one.'), + 'snapshot_not_belong_volume': (507, 'The snapshot does not ' + 'belong to the volume.'), + 'snap_name_existed': (508, 'A snapshot with same name ' + 'already exists on the system.') } self._command_function = { - 'sshGetSystem': 'get_system', - 'sshGetIpConnect': 'get_ip_connect', - 'sshGetPoolInfo': 'get_pool_info', - 'sshGetVolume': 'get_volume', - 'sshGetCtrInfo': 'ls_ctr_info', - 'sshCreateVolume': 'create_volume', - 'sshDeleteVolume': 'delete_volume', - 'sshCinderExtendVolume': 'extend_volume', - 'sshMkLocalClone': 'create_clone', - 'sshMkStartLocalClone': 'start_clone', - 'sshRemoveLocalClone': 'delete_clone', - 'sshMapVoltoHost': 'create_lun_map', - 'sshDeleteLunMap': 'delete_lun_map', - 'sshCreateSnapshot': 'create_snapshot', - 'sshDeleteSnapshot': 'delete_snapshot', - 'sshSetVolumeProperty': 'set_volume_property', + 'set_volume': 'set_volume_property', 'error_ssh': 'error_ssh' } self._volume_type = { @@ -242,12 +267,12 @@ class CommandSimulator(object): self._volumes_list[vol_name]['r'] = '' def execute_command(self, cmd_list, check_exit_code=True): - command = cmd_list[2] + command = cmd_list[1] if command in self._command_function: command = self._command_function[command] func = getattr(self, '_sim_' + command) kwargs = {} - for i in range(3, len(cmd_list)): + for i in range(2, len(cmd_list)): if cmd_list[i].startswith('--'): key = cmd_list[i][2:] value = '' @@ -276,11 +301,14 @@ class CommandSimulator(object): def _sim_get_system(self, **kwargs): return self._json_return(self._system_info) - def _sim_get_ip_connect(self, **kwargs): - return self._json_return(self._ip_list) + def _sim_ls_iscsi(self, **kwargs): + return self._json_return(self._iscsi_list) - def _sim_get_pool_info(self, **kwargs): - pool_name = kwargs['poolName'].strip('\'\"') + def _sim_ls_fc(self, **kwargs): + return self._json_return(self._fc_list) + + def _sim_get_pool(self, **kwargs): + pool_name = kwargs['pool'].strip('\'\"') if pool_name in self._all_pools_name['acs5000_volpool_name']: vol_len = 0 for vol in self._volumes_list.values(): @@ -294,27 +322,34 @@ class CommandSimulator(object): pool_data['total_volumes'] = str(vol_len) return self._json_return(pool_data) else: - return self._json_return() + return self._json_return({}) def _sim_get_volume(self, **kwargs): rows = [] - if isinstance(kwargs['name'], list): - volume_name = kwargs['name'] - else: - volume_name = [kwargs['name']] + if 'volume' not in kwargs: + volume_name = [] + elif isinstance(kwargs['volume'], list): + volume_name = kwargs['volume'] + elif isinstance(kwargs['volume'], str): + volume_name = [kwargs['volume']] for vol_name in volume_name: if vol_name in self._volumes_list.keys(): rows.append(self._volumes_list[vol_name]) return self._json_return(rows) - def _sim_ls_ctr_info(self, **kwargs): + def _sim_ls_controller(self, **kwargs): return self._json_return(self._controllers_list) def _sim_create_volume(self, **kwargs): - volume_name = kwargs['volumename'] - pool_name = kwargs['cinderPool'] - size = kwargs['volumesize'] + volume_name = kwargs['volume'] + pool_name = kwargs['pool'] + size = kwargs['size'] + type = kwargs['type'] + if pool_name not in self._pools_list: + return self._json_return( + msg=self._error['pool_not_exist'][1], + key=self._error['pool_not_exist'][0]) if volume_name in self._volumes_list: return self._json_return( msg=self._error['volume_name_exist'][1], @@ -334,7 +369,7 @@ class CommandSimulator(object): key=self._error['volume_limit_pool'][0]) avail_size = (int(self._pools_list[pool_name]['free_capacity']) / units.Gi) - if int(size) > avail_size: + if float(size) > avail_size: return self._json_return( msg=self._error['pool_exceeds_size'][1], key=self._error['pool_exceeds_size'][0]) @@ -342,6 +377,7 @@ class CommandSimulator(object): volume_info['id'] = self._create_id(self._volumes_list) volume_info['name'] = volume_name volume_info['size_gb'] = size + volume_info['size_mb'] = str(int(float(size) * 1024)) volume_info['status'] = 'Online' volume_info['health'] = 'Optimal' volume_info['r'] = '' @@ -349,7 +385,6 @@ class CommandSimulator(object): volume_info['has_clone'] = 0 volume_info['clone'] = 'N/A' volume_info['clone_snap'] = 'N/A' - type = kwargs['type'] if type not in ('0', '10'): type = '0' volume_info['type'] = self._volume_type[type] @@ -357,26 +392,30 @@ class CommandSimulator(object): return self._json_return() def _sim_delete_volume(self, **kwargs): - vol_name = kwargs['cinderVolume'] + vol_name = kwargs['volume'] if vol_name in self._volumes_list: del self._volumes_list[vol_name] return self._json_return() def _sim_extend_volume(self, **kwargs): - vol_name = kwargs['cinderVolume'] - size = int(kwargs['extendsize']) + vol_name = kwargs['volume'] + size = int(kwargs['size']) if vol_name not in self._volumes_list: return self._json_return( msg=self._error['volume_not_exist'][1], key=self._error['volume_not_exist'][0]) volume = self._volumes_list[vol_name] - curr_size = int(volume['size_gb']) + curr_size = int(volume['size_mb']) / 1024 pool = self._pools_list[volume['poolname']] avail_size = int(pool['free_capacity']) / units.Gi if curr_size > size: return self._json_return( msg=self._error['volume_extend_min'][1], key=self._error['volume_extend_min'][0]) + elif curr_size == size: + return self._json_return( + msg=self._error['volume_extend_size_equal'][1], + key=self._error['volume_extend_size_equal'][0]) elif (size - curr_size) > avail_size: return self._json_return( msg=self._error['pool_exceeds_size'][1], @@ -385,8 +424,8 @@ class CommandSimulator(object): return self._json_return() def _sim_create_clone(self, **kwargs): - src_name = kwargs['cinderVolume'] - tgt_name = kwargs['cloneVolume'] + src_name = kwargs['volume'] + tgt_name = kwargs['clone'] src_exist = False tgt_exist = False for vol in self._volumes_list.values(): @@ -418,7 +457,7 @@ class CommandSimulator(object): return self._json_return() def _sim_start_clone(self, **kwargs): - vol_name = kwargs['cinderVolume'] + vol_name = kwargs['volume'] snapshot = kwargs['snapshot'] if len(snapshot) > 0: snap_found = False @@ -441,7 +480,7 @@ class CommandSimulator(object): return self._json_return() def _sim_delete_clone(self, **kwargs): - vol_name = kwargs['name'] + vol_name = kwargs['volume'] snapshot = kwargs['snapshot'] if vol_name not in self._volumes_list: return self._json_return( @@ -461,14 +500,22 @@ class CommandSimulator(object): return self._json_return() def _sim_create_lun_map(self, **kwargs): - volume_name = kwargs['cinderVolume'] - protocol = kwargs['protocol'] - hosts = kwargs['host'] + volume_name = kwargs.get('volume', None) + protocol = kwargs.get('protocol', None) + hosts = kwargs.get('host', None) + if volume_name is None or protocol is None or hosts is None: + return self._json_return( + msg=self._error['unknown'][1], + key=self._error['unknown'][0]) if volume_name not in self._volumes_list: return self._json_return( msg=self._error['volume_not_exist'][1], key=self._error['volume_not_exist'][0]) if isinstance(hosts, str): + if hosts == '': + return self._json_return( + msg=self._error['host_empty'][1], + key=self._error['host_empty'][0]) hosts = [hosts] volume = self._volumes_list[volume_name] available_luns = LUN_NUMS_AVAILABLE @@ -483,7 +530,7 @@ class CommandSimulator(object): available_luns = [lun for lun in available_luns if lun != lun_row['lun']] if hosts and existed_lun > -1: - return self._json_return({'info': existed_lun}) + return self._json_return({'lun': existed_lun}) lun_info = {} lun_info['vd_id'] = volume['id'] lun_info['vd_name'] = volume['name'] @@ -500,25 +547,33 @@ class CommandSimulator(object): lun_info['id'] = self._create_id(self._lun_maps_list) lun_info['host'] = host self._lun_maps_list.append(copy.deepcopy(lun_info)) - ret = {'lun': [], - 'iscsi_name': [], - 'portal': []} - for ctr, ips in self._ip_list.items(): - for ip in ips: + ret = {} + if protocol == 'FC': + ret = {'lun': lun_info['lun']} + elif protocol == 'iSCSI': + ret = {'lun': [], + 'iscsi_name': [], + 'portal': []} + for iscsi in self._iscsi_list: + if iscsi['link'] == 'Down': + continue ret['lun'].append(lun_info['lun']) - ret['portal'].append('%s:3260' % ip['ip']) + ret['portal'].append('%s:3260' % iscsi['ip']) for control in self._controllers_list: - if ctr == control['id']: + if iscsi['ctrl_idx'] == int(control['id']): ret['iscsi_name'].append(control['iscsi_name']) break return self._json_return(ret) def _sim_delete_lun_map(self, **kwargs): map_exist = False - volume_name = kwargs['cinderVolume'] + volume_name = kwargs['volume'] protocol = kwargs['protocol'] - hosts = kwargs['cinderHost'] - if isinstance(hosts, str): + hosts = kwargs['host'] + all_host = False + if hosts == '-1': + all_host = True + elif isinstance(hosts, str): hosts = [hosts] if volume_name not in self._volumes_list: return self._json_return( @@ -530,7 +585,7 @@ class CommandSimulator(object): for row in lun_maps_list: if (row['vd_id'] == volume['id'] and row['protocol'] == protocol - and row['host'] in hosts): + and (all_host or row['host'] in hosts)): map_exist = True else: map_exist = False @@ -560,6 +615,10 @@ class CommandSimulator(object): volume_snap_count += 1 if int(snap['tag']) > tag: tag = int(snap['tag']) + if snap['name'] == snapshot_name: + return self._json_return( + msg=self._error['snap_name_existed'][1], + key=self._error['snap_name_existed'][0]) if volume_snap_count >= SNAPSHOTS_A_VOLUME: return self._json_return( msg=self._error['snap_over_volume'][1], @@ -595,6 +654,37 @@ class CommandSimulator(object): key=self._error['snap_not_exist'][0]) return self._json_return() + def _sim_rollback_snapshot(self, **kwargs): + volume_name = kwargs['volume'] + snapshot_name = kwargs['snapshot'] + if volume_name and volume_name not in self._volumes_list: + return self._json_return( + msg=self._error['volume_not_exist'][1], + key=self._error['volume_not_exist'][0]) + snapshot = [] + for snap in self._snapshots_list: + if snap['name'] == snapshot_name: + snapshot = snap + break + if not snapshot: + return self._json_return( + msg=self._error['snap_not_exist'][1], + key=self._error['snap_not_exist'][0]) + if volume_name and volume_name != snapshot['vd_name']: + return self._json_return( + msg=self._error['snapshot_not_belong_volume'][1], + key=self._error['snapshot_not_belong_volume'][0]) + elif not volume_name: + volume_name = snapshot['vd_name'] + for snap in self._snapshots_list: + if (snap['vd_name'] == volume_name + and snapshot_name != snap['name'] + and snap['tag'] > snapshot['tag']): + return self._json_return( + msg=self._error['snap_not_latest'][1], + key=self._error['snap_not_latest'][0]) + return self._json_return() + def _sim_set_volume_property(self, **kwargs): volume_name = kwargs['volume'] kwargs.pop('volume') @@ -654,6 +744,20 @@ class Acs5000ISCSIFakeDriver(acs5000_iscsi.Acs5000ISCSIDriver): return ret +class Acs5000FCFakeDriver(acs5000_fc.Acs5000FCDriver): + def __init__(self, *args, **kwargs): + super(Acs5000FCFakeDriver, self).__init__(*args, **kwargs) + + def set_fake_storage(self, fake): + self.fake_storage = fake + + def _run_ssh(self, cmd_list, check_exit_code=True): + cinder_utils.check_ssh_injection(cmd_list) + ret = self.fake_storage.execute_command(cmd_list, check_exit_code) + + return ret + + class Acs5000ISCSIDriverTestCase(test.TestCase): @mock.patch.object(time, 'sleep') def setUp(self, mock_sleep): @@ -664,6 +768,7 @@ class Acs5000ISCSIDriverTestCase(test.TestCase): self.configuration.san_login = 'cliuser' self.configuration.san_password = 'clipassword' self.configuration.acs5000_volpool_name = ['pool01'] + self.configuration.acs5000_multiattach = True self.iscsi_driver = Acs5000ISCSIFakeDriver( configuration=self.configuration) initiator = 'test.iqn.%s' % str(random.randint(10000, 99999)) @@ -727,10 +832,11 @@ class Acs5000ISCSIDriverTestCase(test.TestCase): volume = self._create_volume() result = self.iscsi_driver.initialize_connection(volume, self._connector) - ip_connect = self.sim._ip_list + ip_connect = self.sim._iscsi_list ip_count = 0 - for ips in ip_connect.values(): - ip_count += len(ips) + for iscsi in ip_connect: + if iscsi['link'] != 'Down': + ip_count += 1 self.assertEqual('iscsi', result['driver_volume_type']) self.assertEqual(ip_count, len(result['data']['target_iqns'])) @@ -751,7 +857,7 @@ class Acs5000ISCSIDriverTestCase(test.TestCase): vol, self._connector) self.db.volume_destroy(self.ctxt, vol['id']) - def test_initialize_connection_failure(self): + def test_initialize_connection_available_lun(self): volume_list = [] for i in LUN_NUMS_AVAILABLE: vol = self._create_volume() @@ -769,6 +875,15 @@ class Acs5000ISCSIDriverTestCase(test.TestCase): v, self._connector) self._delete_volume(v) + def test_initialize_connection_exception(self): + vol = self._create_volume() + connector = self._connector + connector['initiator'] = '' + self.assertRaises(exception.VolumeBackendAPIException, + self.iscsi_driver.initialize_connection, + vol, connector) + self._delete_volume(vol) + def test_initialize_connection_multi_host(self): connector = self._connector initiator1 = ('test.iqn.%s' @@ -790,6 +905,16 @@ class Acs5000ISCSIDriverTestCase(test.TestCase): self._assert_lun_exists(volume['id'], False) self._delete_volume(volume) + def test_initialize_connection_protocol(self): + volume = self._create_volume() + protocol = self.iscsi_driver.protocol + self.iscsi_driver.protocol = 'error_protocol' + self.assertRaises(exception.VolumeBackendAPIException, + self.iscsi_driver.initialize_connection, + volume, self._connector) + self.iscsi_driver.protocol = protocol + self._delete_volume(volume) + def test_terminate_connection(self): volume = self._create_volume() self.iscsi_driver.initialize_connection(volume, @@ -799,6 +924,208 @@ class Acs5000ISCSIDriverTestCase(test.TestCase): self._assert_lun_exists(volume['id'], False) self._delete_volume(volume) + def test_terminate_connection_multi_attached(self): + vol = self._create_volume() + connector = self._connector + connector['uuid'] = fake.UUID1 + self.iscsi_driver.initialize_connection(vol, connector) + self._assert_lun_exists(vol.id, True) + attachment1 = volume_attachment.VolumeAttachment() + attachment2 = volume_attachment.VolumeAttachment() + attachment1.connector = connector + attachment2.connector = connector + vol.volume_attachment.objects.append(attachment1) + vol.volume_attachment.objects.append(attachment2) + self.iscsi_driver.terminate_connection(vol, connector) + self._assert_lun_exists(vol.id, True) + vol.volume_attachment.objects = [attachment1] + self.iscsi_driver.terminate_connection(vol, connector) + self._assert_lun_exists(vol.id, False) + self.iscsi_driver.initialize_connection(vol, connector) + self._assert_lun_exists(vol.id, True) + self.iscsi_driver.terminate_connection(vol, None) + self._assert_lun_exists(vol.id, False) + self._delete_volume(vol) + + +class Acs5000FCDriverTestCase(test.TestCase): + @mock.patch.object(time, 'sleep') + def setUp(self, mock_sleep): + super(Acs5000FCDriverTestCase, self).setUp() + self.configuration = mock.Mock(conf.Configuration) + self.configuration.san_is_local = False + self.configuration.san_ip = '23.44.56.78' + self.configuration.san_login = 'cliuser' + self.configuration.san_password = 'clipassword' + self.configuration.acs5000_volpool_name = ['pool01'] + self.configuration.acs5000_multiattach = True + self.fc_driver = Acs5000FCFakeDriver( + configuration=self.configuration) + wwpns = [ + str(random.randint(0, 9999999999999999)).zfill(16), + str(random.randint(0, 9999999999999999)).zfill(16)] + initiator = 'test.iqn.%s' % str(random.randint(10000, 99999)) + self._connector = {'ip': '1.234.56.78', + 'host': 'stack', + 'wwpns': wwpns, + 'initiator': initiator} + self.sim = CommandSimulator(POOLS_NAME) + self.fc_driver.set_fake_storage(self.sim) + self.ctxt = context.get_admin_context() + + self.db = cinder.db + self.fc_driver.db = self.db + self.fc_driver.get_driver_options() + self.fc_driver.do_setup(None) + self.fc_driver.check_for_setup_error() + + def _create_volume(self, **kwargs): + prop = {'host': 'stack@ty1#%s' % POOLS_NAME[0], + 'size': 1, + 'volume_type_id': self.vt['id']} + for p in prop.keys(): + if p not in kwargs: + kwargs[p] = prop[p] + vol = testutils.create_volume(self.ctxt, **kwargs) + self.fc_driver.create_volume(vol) + return vol + + def _delete_volume(self, volume): + self.fc_driver.delete_volume(volume) + self.db.volume_destroy(self.ctxt, volume['id']) + + def _assert_lun_exists(self, vol_id, exists): + lun_maps = self.sim._lun_maps_list + is_lun_defined = False + luns = [] + volume_name = VOLUME_PRE + vol_id[-12:] + for lun in lun_maps: + if volume_name == lun['vd_name']: + luns.append(lun) + if len(luns): + is_lun_defined = True + self.assertEqual(exists, is_lun_defined) + return luns + + def test_validate_connector(self): + conn_neither = {'host': 'host'} + conn_iscsi = {'host': 'host', 'initiator': 'iqn.123'} + conn_fc = {'host': 'host', 'wwpns': 'fff123'} + conn_both = {'host': 'host', 'initiator': 'iqn.123', 'wwpns': 'fff123'} + + self.fc_driver.validate_connector(conn_fc) + self.fc_driver.validate_connector(conn_both) + self.assertRaises(exception.InvalidConnectorException, + self.fc_driver.validate_connector, conn_iscsi) + self.assertRaises(exception.InvalidConnectorException, + self.fc_driver.validate_connector, conn_neither) + + def test_initialize_connection(self): + volume = self._create_volume() + result = self.fc_driver.initialize_connection(volume, + self._connector) + fc_list = self.sim._fc_list + up_wwpns = [] + for port in fc_list: + if port['link'] == 'Up': + if 'WWPN' in port: + up_wwpns.append(port['WWPN']) + elif 'wwpn' in port: + up_wwpns.append(port['wwpn']) + self.assertEqual('fibre_channel', result['driver_volume_type']) + self.assertEqual(up_wwpns.sort(), result['data']['target_wwn'].sort()) + self.assertEqual(volume['id'], result['data']['volume_id']) + self.assertIsNotNone(result['data']['target_lun']) + self._delete_volume(volume) + + def test_initialize_connection_not_found(self): + prop = {'host': 'stack@ty1#%s' % POOLS_NAME[0], + 'size': 1, + 'volume_type_id': self.vt['id']} + vol = testutils.create_volume(self.ctxt, **prop) + self.assertRaises(exception.VolumeNotFound, + self.fc_driver.initialize_connection, + vol, self._connector) + self.db.volume_destroy(self.ctxt, vol['id']) + + def test_initialize_connection_failure(self): + volume_list = [] + for i in LUN_NUMS_AVAILABLE: + vol = self._create_volume() + self.fc_driver.initialize_connection( + vol, self._connector) + volume_list.append(vol) + + vol = self._create_volume() + self.assertRaises(exception.VolumeBackendAPIException, + self.fc_driver.initialize_connection, + vol, self._connector) + self._delete_volume(vol) + for v in volume_list: + self.fc_driver.terminate_connection( + v, self._connector) + self._delete_volume(v) + + def test_initialize_connection_exception(self): + vol = self._create_volume() + connector = self._connector + self.fc_driver.protocol = 'error_protocol' + self.assertRaises(exception.VolumeBackendAPIException, + self.fc_driver.initialize_connection, + vol, connector) + self.fc_driver.protocol = acs5000_fc.Acs5000FCDriver.PROTOCOL + fc_list = self.sim._fc_list + self.sim._fc_list = [] + self.assertRaises(exception.VolumeBackendAPIException, + self.fc_driver.initialize_connection, + vol, connector) + # _check_multi_attached then delete_lun_map + connector['uuid'] = fake.UUID1 + attachment1 = volume_attachment.VolumeAttachment() + attachment2 = volume_attachment.VolumeAttachment() + attachment1.connector = connector + attachment2.connector = connector + vol.volume_attachment.objects.append(attachment1) + vol.volume_attachment.objects.append(attachment2) + self.assertRaises(exception.VolumeBackendAPIException, + self.fc_driver.initialize_connection, + vol, connector) + self.sim._fc_list = fc_list + self._delete_volume(vol) + + def test_terminate_connection(self): + volume = self._create_volume() + self.fc_driver.initialize_connection(volume, + self._connector) + self.fc_driver.terminate_connection(volume, + self._connector) + self._assert_lun_exists(volume['id'], False) + self._delete_volume(volume) + + def test_terminate_connection_warn(self): + volume = self._create_volume() + connector = self._connector + connector['uuid'] = fake.UUID1 + self.fc_driver.initialize_connection(volume, connector) + self.fc_driver.terminate_connection(volume, None) + self._assert_lun_exists(volume.id, False) + self.fc_driver.initialize_connection(volume, connector) + attachment1 = volume_attachment.VolumeAttachment() + attachment2 = volume_attachment.VolumeAttachment() + attachment1.connector = connector + attachment2.connector = connector + volume.volume_attachment.objects.append(attachment1) + volume.volume_attachment.objects.append(attachment2) + self.fc_driver.terminate_connection(volume, connector) + self._assert_lun_exists(volume.id, True) + fc_list = self.sim._fc_list + self.sim._fc_list = [] + volume.volume_attachment.objects = [attachment1] + self.fc_driver.terminate_connection(volume, connector) + self.sim._fc_list = fc_list + self._assert_lun_exists(volume.id, False) + self._delete_volume(volume) + class Acs5000CommonDriverTestCase(test.TestCase): @mock.patch.object(time, 'sleep') @@ -808,21 +1135,25 @@ class Acs5000CommonDriverTestCase(test.TestCase): self.configuration = mock.Mock(conf.Configuration) self.configuration.san_is_local = False self.configuration.san_ip = '23.44.56.78' + self.configuration.san_ssh_port = '22' self.configuration.san_login = 'cliuser' self.configuration.san_password = 'clipassword' self.configuration.acs5000_volpool_name = POOLS_NAME self.configuration.acs5000_copy_interval = 0.01 + self.configuration.acs5000_multiattach = True self.configuration.reserved_percentage = 0 - self._driver = Acs5000ISCSIFakeDriver( - configuration=self.configuration) options = acs5000_iscsi.Acs5000ISCSIDriver.get_driver_options() config = conf.Configuration(options, conf.SHARED_CONF_GROUP) + self._driver = Acs5000ISCSIFakeDriver( + configuration=self.configuration) self.override_config('san_ip', '23.44.56.78', conf.SHARED_CONF_GROUP) + self.override_config('san_ssh_port', '22', conf.SHARED_CONF_GROUP) self.override_config('san_login', 'cliuser', conf.SHARED_CONF_GROUP) self.override_config('san_password', 'clipassword', conf.SHARED_CONF_GROUP) self.override_config('acs5000_volpool_name', POOLS_NAME, conf.SHARED_CONF_GROUP) + self._driver.configuration.safe_get = self._safe_get self._iscsi_driver = acs5000_iscsi.Acs5000ISCSIDriver( configuration=config) initiator = 'test.iqn.%s' % str(random.randint(10000, 99999)) @@ -839,6 +1170,12 @@ class Acs5000CommonDriverTestCase(test.TestCase): self._driver.do_setup(None) self._driver.check_for_setup_error() + def _safe_get(self, key): + try: + return getattr(self._driver.configuration, key) + except AttributeError: + return None + def _assert_vol_exists(self, name, exists): volume = self._driver._cmd.get_volume(VOLUME_PRE + name[-12:]) is_vol_defined = False @@ -897,23 +1234,23 @@ class Acs5000CommonDriverTestCase(test.TestCase): self.assertRaises(exception.VolumeBackendAPIException, self._driver._build_pool_stats, 'error_pool') - ssh_cmd = ['cinder', 'storage', 'error_ssh', '--error', 'json_error'] + ssh_cmd = ['error_ssh', '--error', 'json_error'] self.assertRaises(exception.VolumeBackendAPIException, self._driver._cmd.run_ssh_info, ssh_cmd) - ssh_cmd = ['cinder', 'storage', 'error_ssh', '--error', 'dict_error'] + ssh_cmd = ['error_ssh', '--error', 'dict_error'] self.assertRaises(exception.VolumeBackendAPIException, self._driver._cmd.run_ssh_info, ssh_cmd) - ssh_cmd = ['cinder', 'storage', 'error_ssh', '--error', 'keys_error'] + ssh_cmd = ['error_ssh', '--error', 'keys_error'] self.assertRaises(exception.VolumeBackendAPIException, self._driver._cmd.run_ssh_info, ssh_cmd) - ssh_cmd = ['cinder', 'storage', 'error_ssh', '--error', 'key_false'] + ssh_cmd = ['error_ssh', '--error', 'key_false'] self.assertRaises(exception.VolumeBackendAPIException, self._driver._cmd.run_ssh_info, ssh_cmd) @mock.patch.object(ssh_utils, 'SSHPool') @mock.patch.object(processutils, 'ssh_execute') def test_run_ssh_with_ip(self, mock_ssh_execute, mock_ssh_pool): - ssh_cmd = ['cinder', 'storage', 'run_ssh'] + ssh_cmd = ['run_ssh'] self._iscsi_driver._run_ssh(ssh_cmd) mock_ssh_pool.assert_called_once_with( self._iscsi_driver.configuration.san_ip, @@ -942,7 +1279,7 @@ class Acs5000CommonDriverTestCase(test.TestCase): mock.MagicMock()] self.override_config('acs5000_volpool_name', None, self._iscsi_driver.configuration.config_group) - ssh_cmd = ['cinder', 'storage', 'run_ssh'] + ssh_cmd = ['run_ssh'] self.assertRaises(processutils.ProcessExecutionError, self._iscsi_driver._run_ssh, ssh_cmd) @@ -950,7 +1287,15 @@ class Acs5000CommonDriverTestCase(test.TestCase): system_info = self.sim._system_info self.assertEqual(system_info['vendor'], self._driver._state['vendor']) self.assertIn('iSCSI', self._driver._state['enabled_protocols']) - self.assertEqual(2, len(self._driver._state['storage_nodes'])) + self.assertEqual(2, len(self._driver._state['controller'])) + iscsi_list = self.sim._iscsi_list + self.sim._iscsi_list = [] + self._driver._state['enabled_protocols'] = set() + self._driver.do_setup(None) + self.assertEqual(set(), self._driver._state['enabled_protocols']) + self.sim._iscsi_list = iscsi_list + self._driver.do_setup(None) + self.assertEqual({'iSCSI'}, self._driver._state['enabled_protocols']) def test_do_setup_no_pools(self): self._driver.pools = ['pool_error'] @@ -1015,6 +1360,19 @@ class Acs5000CommonDriverTestCase(test.TestCase): for v in volume_list: self._delete_volume(v) + def test_create_volume_pool_not_existed(self): + prop = { + 'host': 'stack@ty2#no_pool', + 'driver': False + } + self._driver.get_volume_stats() + vol = self._create_volume(**prop) + self._assert_vol_exists(vol['id'], False) + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.create_volume, + vol) + self._delete_volume(vol, False) + def test_delete_volume(self): vol = self._create_volume() self._assert_vol_exists(vol['id'], True) @@ -1064,6 +1422,16 @@ class Acs5000CommonDriverTestCase(test.TestCase): for vol in vol_list: self._delete_volume(vol) + def test_create_snapshot_name_existed(self): + vol = self._create_volume() + snap = self._create_snapshot(vol['id']) + self._assert_vol_exists(vol['id'], True) + self._assert_snap_exists(snap['id'], True) + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.create_snapshot, snap) + self._delete_snapshot(snap) + self._delete_volume(vol) + def test_delete_snapshot(self): vol = self._create_volume() self._assert_vol_exists(vol['id'], True) @@ -1084,6 +1452,18 @@ class Acs5000CommonDriverTestCase(test.TestCase): self._delete_snapshot(snap, False) self._delete_volume(vol) + def test_delete_snapshot_volume_not_found(self): + vol = self._create_volume() + snap = self._create_snapshot(vol['id']) + self._delete_volume(vol) + self._assert_vol_exists(vol['id'], False) + self._assert_snap_exists(snap['id'], True) + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.delete_snapshot, snap) + self._driver.create_volume(vol) + self._delete_snapshot(snap) + self._driver.delete_volume(vol) + def test_create_volume_from_snapshot(self): prop = {'size': 2} vol = self._create_volume(**prop) @@ -1257,6 +1637,15 @@ class Acs5000CommonDriverTestCase(test.TestCase): volume, '200') self._delete_volume(volume) + def test_extend_volume_size_equal(self): + volume = self._create_volume(size=10) + vol_info = self._assert_vol_exists(volume['id'], True) + self.assertEqual('10', vol_info[0]['size_gb']) + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.extend_volume, + volume, '10') + self._delete_volume(volume) + def test_migrate_volume_same_pool(self): host = 'stack@ty1#%s' % POOLS_NAME[0] volume = self._create_volume(host=host) @@ -1319,6 +1708,20 @@ class Acs5000CommonDriverTestCase(test.TestCase): self.assertEqual([], ret) ret = self._driver._cmd.get_volume('test_volume') self.assertEqual([], ret) + vol1 = self._create_volume() + vol2 = self._create_volume() + vol1_name = VOLUME_PRE + vol1['id'][-12:] + vol2_name = VOLUME_PRE + vol2['id'][-12:] + ret = self._driver._cmd.get_volume([vol1_name, vol2_name]) + self.assertEqual(2, len(ret)) + vol_name = [] + for vol in ret: + vol_name.append(vol['name']) + self.assertEqual(sorted([vol1_name, vol2_name]), sorted(vol_name)) + ret = self._driver._cmd.get_volume({'test_key': 'test_value'}) + self.assertEqual([], ret) + self._delete_volume(vol1) + self._delete_volume(vol2) def test_check_for_setup_error_failure(self): self._driver._state['system_name'] = None @@ -1329,7 +1732,7 @@ class Acs5000CommonDriverTestCase(test.TestCase): self.assertRaises(exception.VolumeBackendAPIException, self._driver.check_for_setup_error) self._driver.do_setup(None) - self._driver._state['storage_nodes'] = [] + self._driver._state['controller'] = [] self.assertRaises(exception.VolumeDriverException, self._driver.check_for_setup_error) self._driver.do_setup(None) @@ -1337,10 +1740,16 @@ class Acs5000CommonDriverTestCase(test.TestCase): self.assertRaises(exception.InvalidInput, self._driver.check_for_setup_error) self._driver.do_setup(None) + password = self._driver.configuration.san_password self._driver.configuration.san_password = None self.assertRaises(exception.InvalidInput, self._driver.check_for_setup_error) - self._driver.do_setup(None) + self._driver.configuration.san_password = password + san_ip = self._driver.configuration.san_ip + self._driver.configuration.san_ip = None + self.assertRaises(exception.InvalidInput, + self._driver.check_for_setup_error) + self._driver.configuration.san_ip = san_ip def test_build_pool_stats_no_pool(self): self.assertRaises(exception.VolumeBackendAPIException, @@ -1358,3 +1767,166 @@ class Acs5000CommonDriverTestCase(test.TestCase): self._driver._cmd.set_volume_property, volume_name, {}) self._delete_volume(volume) + + def test_snapshot_revert_use_temp_snapshot(self): + ret = self._driver.snapshot_revert_use_temp_snapshot() + self.assertFalse(ret) + + def test_revert_to_snapshot(self): + volume = self._create_volume() + snapshot = self._create_snapshot(volume.id) + self._driver.revert_to_snapshot(self.ctxt, volume, snapshot) + self._delete_snapshot(snapshot) + self._delete_volume(volume) + + def test_revert_to_snapshot_volume_not_found(self): + volume = self._create_volume(driver=False) + snapshot = self._create_snapshot(volume.id, driver=False) + self.assertRaises(exception.VolumeNotFound, + self._driver.revert_to_snapshot, + self.ctxt, volume, snapshot) + self._delete_snapshot(snapshot, driver=False) + self._delete_volume(volume, driver=False) + + def test_revert_to_snapshot_snapshot_not_found(self): + volume = self._create_volume() + snapshot = self._create_snapshot(volume.id, driver=False) + self.assertRaises(exception.SnapshotNotFound, + self._driver.revert_to_snapshot, + self.ctxt, volume, snapshot) + self._delete_snapshot(snapshot, driver=False) + self._delete_volume(volume) + + def test_revert_to_snapshot_not_latest_one(self): + volume = self._create_volume() + snapshot1 = self._create_snapshot(volume.id) + snapshot2 = self._create_snapshot(volume.id) + self.assertRaises(exception.InvalidSnapshot, + self._driver.revert_to_snapshot, + self.ctxt, volume, snapshot1) + self._delete_snapshot(snapshot2) + self._delete_snapshot(snapshot1) + self._delete_volume(volume) + + def test_revert_to_snapshot_not_belong(self): + volume1 = self._create_volume() + volume2 = self._create_volume() + snapshot = self._create_snapshot(volume2.id) + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.revert_to_snapshot, + self.ctxt, volume1, snapshot) + self._delete_snapshot(snapshot) + self._delete_volume(volume1) + self._delete_volume(volume2) + + def test_convert_name(self): + name = self._driver._convert_name('') + self.assertEqual(len(acs5000_common.VOLUME_PREFIX) + 12, + len(name)) + name = self._driver._convert_name('test_name') + self.assertEqual(len(acs5000_common.VOLUME_PREFIX) + 12, + len(name)) + name = self._driver._convert_name(fake.UUID1) + self.assertEqual(len(acs5000_common.VOLUME_PREFIX) + 12, + len(name)) + + def test_check_multi_attached(self): + connector = self._connector + volume = self._create_volume(driver=False) + connector['uuid'] = fake.UUID1 + attachment1 = volume_attachment.VolumeAttachment() + attachment2 = volume_attachment.VolumeAttachment() + volume.volume_attachment.objects.append(attachment1) + volume.volume_attachment.objects.append(attachment2) + count = self._driver._check_multi_attached(volume, connector) + self.assertEqual(0, count) + attachment1.connector = connector + attachment2.connector = connector + volume.volume_attachment.objects.append(attachment1) + volume.volume_attachment.objects.append(attachment2) + count = self._driver._check_multi_attached(volume, connector) + self.assertEqual(4, count) + self._delete_volume(volume, driver=False) + + def test_update_migrated_volume(self): + old_vol = self._create_volume(driver=False) + self._assert_vol_exists(old_vol.id, False) + new_vol = self._create_volume() + self._assert_vol_exists(new_vol.id, True) + ret = self._driver.update_migrated_volume( + self.ctxt, old_vol, new_vol, None) + self.assertEqual({'_name_id': None}, ret) + self._assert_vol_exists(old_vol.id, True) + self._assert_vol_exists(new_vol.id, False) + self._delete_volume(old_vol) + self._delete_volume(new_vol, False) + + def test_update_migrated_volume_existed(self): + old_vol = self._create_volume() + self._assert_vol_exists(old_vol.id, True) + new_vol = self._create_volume() + self._assert_vol_exists(new_vol.id, True) + ret = self._driver.update_migrated_volume( + self.ctxt, old_vol, new_vol, None) + self.assertEqual({'_name_id': new_vol.id}, ret) + self._delete_volume(old_vol) + self._delete_volume(new_vol) + + def test_manage_existing(self): + volume_name = fake.UUID1 + self._driver._cmd.create_volume(volume_name, '1', POOLS_NAME[0]) + volume = self._driver._cmd.get_volume(volume_name) + self.assertEqual(1, len(volume)) + self.assertEqual(volume_name, volume[0]['name']) + new_volume = self._create_volume(driver=False) + self._assert_vol_exists(new_volume.id, False) + self.assertRaises(exception.ManageExistingInvalidReference, + self._driver.manage_existing, + new_volume, {}) + self._driver.manage_existing(new_volume, + {'source-name': volume_name}) + self._assert_vol_exists(new_volume.id, True) + self._delete_volume(new_volume) + + def test_manage_existing_get_size(self): + volume_name = fake.UUID1 + self._driver._cmd.create_volume(volume_name, '1', POOLS_NAME[0]) + vol = self._create_volume(size=1, driver=False) + self._assert_vol_exists(vol.id, False) + ret = self._driver.manage_existing_get_size( + vol, {'source-name': volume_name}) + self.assertEqual(1, ret) + self._driver._cmd.delete_volume(volume_name) + self._delete_volume(vol, False) + + def test_manage_existing_get_size_extend(self): + volume_name = fake.UUID1 + size_str = '1.2' + size_gb = 2 + self._driver._cmd.create_volume(volume_name, size_str, POOLS_NAME[0]) + volume = self._driver._cmd.get_volume(volume_name) + self.assertEqual(1, len(volume)) + self.assertEqual(volume_name, volume[0]['name']) + vol = self._create_volume(driver=False) + self._assert_vol_exists(vol.id, False) + ret = self._driver.manage_existing_get_size( + vol, {'source-name': volume_name}) + self.assertEqual(size_gb, ret) + self._driver._cmd.delete_volume(volume_name) + self._delete_volume(vol, driver=False) + + def test_manage_get_volume(self): + vol = self._create_volume() + vol_backend = self._assert_vol_exists(vol.id, True) + vol_name = vol_backend[0]['name'] + ret = self._driver._manage_get_volume({'source-name': vol_name}) + self.assertEqual(vol_backend[0], ret) + self.assertRaises(exception.ManageExistingInvalidReference, + self._driver._manage_get_volume, {}) + self.assertRaises(exception.ManageExistingInvalidReference, + self._driver._manage_get_volume, + {'source-name': 'error_volume'}) + self.assertRaises(exception.ManageExistingInvalidReference, + self._driver._manage_get_volume, + {'source-name': vol_name}, 'error_pool') + self._delete_volume(vol) diff --git a/cinder/volume/drivers/toyou/acs5000/acs5000_common.py b/cinder/volume/drivers/toyou/acs5000/acs5000_common.py index c0fee1e4d68..551a313cee5 100644 --- a/cinder/volume/drivers/toyou/acs5000/acs5000_common.py +++ b/cinder/volume/drivers/toyou/acs5000/acs5000_common.py @@ -19,6 +19,7 @@ It will be called by iSCSI driver """ import json +import math import random from eventlet import greenthread @@ -53,7 +54,12 @@ acs5000c_opts = [ min=3, max=100, help='When volume copy task is going on,refresh volume ' - 'status interval') + 'status interval'), + cfg.BoolOpt( + 'acs5000_multiattach', + default=False, + help='Enable to allow volumes attaching to multiple ' + 'hosts with no limit.'), ] CONF = cfg.CONF CONF.register_opts(acs5000c_opts) @@ -78,6 +84,7 @@ class Command(object): def run_ssh_info(self, ssh_cmd, key=False): """Run an SSH command and return parsed output.""" + ssh_cmd.insert(0, 'cinder') out, err = self._run_ssh(ssh_cmd) if len(err): msg = (_('Execute command %(cmd)s failed, ' @@ -134,35 +141,43 @@ class Command(object): return info['arr'] def get_system(self): - ssh_cmd = ['cinder', 'Storage', 'sshGetSystem'] + ssh_cmd = ['get_system'] return self.run_ssh_info(ssh_cmd) - def get_ip_connect(self): - ssh_cmd = ['cinder', - 'Storage', - 'sshGetIpConnect'] - return self.run_ssh_info(ssh_cmd) + def ls_iscsi(self): + ssh_cmd = ['ls_iscsi'] + ports = self.run_ssh_info(ssh_cmd) + up_ports = [] + for port in ports: + if 'link' in port and port['link'] != 'Down': + up_ports.append(up_ports) + return up_ports - def get_pool_info(self, pool): - ssh_cmd = ['cinder', - 'Storage', - 'sshGetPoolInfo', - '--poolName', + def ls_fc(self): + ssh_cmd = ['ls_fc'] + ports = self.run_ssh_info(ssh_cmd) + up_ports = [] + for port in ports: + if 'link' in port and port['link'] == 'Up': + up_ports.append(port) + return up_ports + + def get_pool(self, pool): + ssh_cmd = ['get_pool', + '--pool', pool] return self.run_ssh_info(ssh_cmd) def get_volume(self, volume): - ssh_cmd = ['cinder', - 'Storage', - 'sshGetVolume'] + ssh_cmd = ['get_volume'] if not volume: return [] elif isinstance(volume, str): - ssh_cmd.append('--name') + ssh_cmd.append('--volume') ssh_cmd.append(volume) elif isinstance(volume, list): for vol in volume: - ssh_cmd.append('--name') + ssh_cmd.append('--volume') ssh_cmd.append(vol) result = self.run_ssh_info(ssh_cmd) if not result: @@ -170,83 +185,63 @@ class Command(object): else: return result - def ls_ctr_info(self): - ssh_cmd = ['cinder', 'Storage', 'sshGetCtrInfo'] + def ls_controller(self): + ssh_cmd = ['ls_controller'] ctrs = self.run_ssh_info(ssh_cmd) nodes = {} for node_data in ctrs: nodes[node_data['id']] = { - 'id': node_data['id'], + 'id': int(node_data['id']), 'name': node_data['name'], - 'iscsi_name': node_data['iscsi_name'], - 'WWNN': node_data['WWNN'], - 'WWPN': [], - 'status': node_data['status'], - 'ipv4': [], - 'ipv6': [], - 'enabled_protocols': [] + 'status': node_data['status'] } return nodes def create_volume(self, name, size, pool_name, type='0'): - ssh_cmd = ['cinder', - 'Storage', - 'sshCreateVolume', - '--volumesize', + ssh_cmd = ['create_volume', + '--size', size, - '--volumename', + '--volume', name, - '--cinderPool', + '--pool', pool_name, '--type', type] return self.run_ssh_info(ssh_cmd, key=True) def delete_volume(self, volume): - ssh_cmd = ['cinder', - 'Storage', - 'sshDeleteVolume', - '--cinderVolume', + ssh_cmd = ['delete_volume', + '--volume', volume] return self.run_ssh_info(ssh_cmd) def extend_volume(self, volume, size): - ssh_cmd = ['cinder', - 'Storage', - 'sshCinderExtendVolume', - '--cinderVolume', + ssh_cmd = ['extend_volume', + '--volume', volume, - '--extendunit', - 'gb', - '--extendsize', + '--size', str(size)] return self.run_ssh_info(ssh_cmd, key=True) def create_clone(self, volume_name, clone_name): - ssh_cmd = ['cinder', - 'Storage', - 'sshMkLocalClone', - '--cinderVolume', + ssh_cmd = ['create_clone', + '--volume', volume_name, - '--cloneVolume', + '--clone', clone_name] return self.run_ssh_info(ssh_cmd, key=True) def start_clone(self, volume_name, snapshot=''): - ssh_cmd = ['cinder', - 'Storage', - 'sshMkStartLocalClone', - '--cinderVolume', + ssh_cmd = ['start_clone', + '--volume', volume_name, '--snapshot', snapshot] return self.run_ssh_info(ssh_cmd, key=True) def delete_clone(self, volume_name, snapshot=''): - ssh_cmd = ['cinder', - 'Storage', - 'sshRemoveLocalClone', - '--name', + ssh_cmd = ['delete_clone', + '--volume', volume_name, '--snapshot', snapshot] @@ -255,10 +250,8 @@ class Command(object): def create_lun_map(self, volume_name, protocol, host): """Map volume to host.""" LOG.debug('enter: create_lun_map volume %s.', volume_name) - ssh_cmd = ['cinder', - 'Storage', - 'sshMapVoltoHost', - '--cinderVolume', + ssh_cmd = ['create_lun_map', + '--volume', volume_name, '--protocol', protocol] @@ -272,26 +265,22 @@ class Command(object): return self.run_ssh_info(ssh_cmd, key=True) def delete_lun_map(self, volume_name, protocol, host): - ssh_cmd = ['cinder', - 'Storage', - 'sshDeleteLunMap', - '--cinderVolume', + ssh_cmd = ['delete_lun_map', + '--volume', volume_name, '--protocol', protocol] if isinstance(host, list): for ht in host: - ssh_cmd.append('--cinderHost') + ssh_cmd.append('--host') ssh_cmd.append(ht) else: - ssh_cmd.append('--cinderHost') + ssh_cmd.append('--host') ssh_cmd.append(str(host)) return self.run_ssh_info(ssh_cmd, key=True) def create_snapshot(self, volume_name, snapshot_name): - ssh_cmd = ['cinder', - 'Storage', - 'sshCreateSnapshot', + ssh_cmd = ['create_snapshot', '--volume', volume_name, '--snapshot', @@ -299,19 +288,23 @@ class Command(object): return self.run_ssh_info(ssh_cmd, key=True) def delete_snapshot(self, volume_name, snapshot_name): - ssh_cmd = ['cinder', - 'Storage', - 'sshDeleteSnapshot', + ssh_cmd = ['delete_snapshot', '--volume', volume_name, '--snapshot', snapshot_name] return self.run_ssh_info(ssh_cmd, key=True) + def rollback_snapshot(self, snapshot_name, volume_name=''): + ssh_cmd = ['rollback_snapshot', + '--snapshot', + snapshot_name, + '--volume', + volume_name] + return self.run_ssh_info(ssh_cmd, key=True) + def set_volume_property(self, name, setting): - ssh_cmd = ['cinder', - 'Storage', - 'sshSetVolumeProperty', + ssh_cmd = ['set_volume', '--volume', name] for key, value in setting.items(): @@ -340,7 +333,7 @@ class Acs5000CommonDriver(san.SanDriver, self.pools = self.configuration.acs5000_volpool_name self._cmd = Command(self._run_ssh) self.protocol = None - self._state = {'storage_nodes': {}, + self._state = {'controller': {}, 'enabled_protocols': set(), 'system_name': None, 'system_id': None, @@ -361,27 +354,53 @@ class Acs5000CommonDriver(san.SanDriver, self._state.update(self._cmd.get_system()) - self._state['storage_nodes'] = self._cmd.ls_ctr_info() - ports = self._cmd.get_ip_connect() + self._state['controller'] = self._cmd.ls_controller() + if self.protocol == 'FC': + ports = self._cmd.ls_fc() + else: + ports = self._cmd.ls_iscsi() if len(ports) > 0: - self._state['enabled_protocols'].add('iSCSI') - for node in self._state['storage_nodes'].values(): - if node['id'] in ports.keys(): - node['enabled_protocols'].append('iSCSI') - for port in ports[node['id']]: - node['ipv4'].append(port['ip']) - return + self._state['enabled_protocols'].add(self.protocol) def _validate_pools_exist(self): LOG.debug('_validate_pools_exist. ' 'pools: %s', ' '.join(self.pools)) for pool in self.pools: - pool_data = self._cmd.get_pool_info(pool) + pool_data = self._cmd.get_pool(pool) if not pool_data: msg = _('Failed getting details for pool %s.') % pool raise exception.InvalidInput(reason=msg) return True + @staticmethod + def _convert_name(name): + if len(name) >= 12: + suffix = name[-12:] + elif len(name) > 0: + suffix = str(name).zfill(12) + else: + suffix = str(random.randint(0, 999999)).zfill(12) + return VOLUME_PREFIX + suffix + + @staticmethod + def _check_multi_attached(volume, connector): + # In the case of multi-attach, these VMs belong to the same host. + # The mapping action only happens once. + # If the only mapping relationship is cancelled, + # volume on other VMs cannot be read or written. + if not connector or 'uuid' not in connector: + return 0 + attached_count = 0 + uuid = connector['uuid'] + for ref in volume.volume_attachment: + ref_connector = {} + if 'connector' in ref and ref.connector: + # ref.connector may be None + ref_connector = ref.connector + if 'uuid' in ref_connector and uuid == ref_connector['uuid']: + attached_count += 1 + return attached_count + @volume_utils.trace_method def check_for_setup_error(self): """Ensure that the params are set properly.""" @@ -391,8 +410,8 @@ class Acs5000CommonDriver(san.SanDriver, if self._state['system_id'] is None: exception_msg = _('Unable to determine system id.') raise exception.VolumeBackendAPIException(data=exception_msg) - if len(self._state['storage_nodes']) != 2: - msg = _('do_setup: No configured nodes.') + if len(self._state['controller']) != 2: + msg = _('do_setup: The dual controller status is incorrect.') LOG.error(msg) raise exception.VolumeDriverException(message=msg) if self.protocol not in self._state['enabled_protocols']: @@ -468,7 +487,7 @@ class Acs5000CommonDriver(san.SanDriver, def create_volume(self, volume): LOG.debug('create_volume, volume %s.', volume['id']) - volume_name = VOLUME_PREFIX + volume['id'][-12:] + volume_name = self._convert_name(volume.name) pool_name = volume_utils.extract_host(volume['host'], 'pool') ret = self._cmd.create_volume( volume_name, @@ -492,16 +511,22 @@ class Acs5000CommonDriver(san.SanDriver, elif ret['key'] == 308: raise exception.VolumeLimitExceeded(allowed=4096, name=volume_name) - model_update = None - return model_update + elif ret['key'] != 0: + msg = (_('Failed to create_volume %(vol)s on pool %(pool)s, ' + 'code=%(ret)s, error=%(msg)s.') % {'vol': volume_name, + 'pool': pool_name, + 'ret': ret['key'], + 'msg': ret['msg']}) + raise exception.VolumeBackendAPIException(data=msg) + return None def delete_volume(self, volume): - volume_name = VOLUME_PREFIX + volume['id'][-12:] + volume_name = self._convert_name(volume.name) self._cmd.delete_volume(volume_name) def create_snapshot(self, snapshot): - volume_name = VOLUME_PREFIX + snapshot['volume_name'][-12:] - snapshot_name = VOLUME_PREFIX + snapshot['name'][-12:] + volume_name = self._convert_name(snapshot.volume_name) + snapshot_name = self._convert_name(snapshot.name) ret = self._cmd.create_snapshot(volume_name, snapshot_name) if ret['key'] == 303: raise exception.VolumeNotFound(volume_id=volume_name) @@ -509,18 +534,32 @@ class Acs5000CommonDriver(san.SanDriver, raise exception.SnapshotLimitExceeded(allowed=4096) elif ret['key'] == 504: raise exception.SnapshotLimitExceeded(allowed=64) + elif ret['key'] != 0: + msg = (_('Failed to create_snapshot %(snap)s on volume %(vol)s ' + 'code=%(ret)s, error=%(msg)s.') % {'vol': volume_name, + 'snap': snapshot_name, + 'ret': ret['key'], + 'msg': ret['msg']}) + raise exception.VolumeBackendAPIException(data=msg) def delete_snapshot(self, snapshot): - volume_name = VOLUME_PREFIX + snapshot['volume_name'][-12:] - snapshot_name = VOLUME_PREFIX + snapshot['name'][-12:] + volume_name = self._convert_name(snapshot.volume_name) + snapshot_name = self._convert_name(snapshot.name) ret = self._cmd.delete_snapshot(volume_name, snapshot_name) if ret['key'] == 505: raise exception.SnapshotNotFound(snapshot_id=snapshot['id']) + elif ret['key'] != 0: + msg = (_('Failed to delete_snapshot %(snap)s on volume %(vol)s ' + 'code=%(ret)s, error=%(msg)s.') % {'vol': volume_name, + 'snap': snapshot_name, + 'ret': ret['key'], + 'msg': ret['msg']}) + raise exception.VolumeBackendAPIException(data=msg) def create_volume_from_snapshot(self, volume, snapshot): - snapshot_name = VOLUME_PREFIX + snapshot['name'][-12:] - volume_name = VOLUME_PREFIX + volume['id'][-12:] - source_volume = VOLUME_PREFIX + snapshot['volume_name'][-12:] + snapshot_name = self._convert_name(snapshot.name) + volume_name = self._convert_name(volume.name) + source_volume = self._convert_name(snapshot.volume_name) pool = volume_utils.extract_host(volume['host'], 'pool') self._cmd.create_volume(volume_name, str(volume['size']), @@ -530,9 +569,32 @@ class Acs5000CommonDriver(san.SanDriver, 'create_volume_from_snapshot', snapshot_name) + def snapshot_revert_use_temp_snapshot(self): + return False + + @volume_utils.trace + def revert_to_snapshot(self, context, volume, snapshot): + volume_name = self._convert_name(volume.name) + snapshot_name = self._convert_name(snapshot.name) + ret = self._cmd.rollback_snapshot(snapshot_name, volume_name) + if ret['key'] == 303: + raise exception.VolumeNotFound(volume_id=volume_name) + elif ret['key'] == 505: + raise exception.SnapshotNotFound(snapshot_id=snapshot_name) + elif ret['key'] == 506: + msg = (_('Snapshot %s is not the latest one.') % snapshot_name) + raise exception.InvalidSnapshot(reason=msg) + elif ret['key'] != 0: + msg = (_('Failed to revert volume %(vol)s to snapshot %(snap)s, ' + 'code=%(ret)s, error=%(msg)s.') % {'vol': volume_name, + 'snap': snapshot_name, + 'ret': ret['key'], + 'msg': ret['msg']}) + raise exception.VolumeBackendAPIException(data=msg) + def create_cloned_volume(self, tgt_volume, src_volume): - clone_name = VOLUME_PREFIX + tgt_volume['id'][-12:] - volume_name = VOLUME_PREFIX + src_volume['id'][-12:] + clone_name = self._convert_name(tgt_volume.name) + volume_name = self._convert_name(src_volume.name) tgt_pool = volume_utils.extract_host(tgt_volume['host'], 'pool') try: self._cmd.create_volume(clone_name, str( @@ -545,7 +607,7 @@ class Acs5000CommonDriver(san.SanDriver, data='create_cloned_volume failed.') def extend_volume(self, volume, new_size): - volume_name = VOLUME_PREFIX + volume['id'][-12:] + volume_name = self._convert_name(volume.name) ret = self._cmd.extend_volume(volume_name, int(new_size)) if ret['key'] == 303: raise exception.VolumeNotFound(volume_id=volume_name) @@ -562,6 +624,33 @@ class Acs5000CommonDriver(san.SanDriver, break raise exception.VolumeSizeExceedsLimit(size=int(new_size), limit=allow_size) + elif ret['key'] != 0: + msg = (_('Failed to extend_volume %(vol)s to size %(size)s, ' + 'code=%(ret)s, error=%(msg)s.') % {'vol': volume_name, + 'size': new_size, + 'ret': ret['key'], + 'msg': ret['msg']}) + raise exception.VolumeBackendAPIException(data=msg) + + def update_migrated_volume(self, ctxt, volume, new_volume, + original_volume_status): + """Only for host copy.""" + existing_name = self._convert_name(new_volume.name) + wanted_name = self._convert_name(volume.name) + LOG.debug('enter: update_migrated_volume: rename of %(new)s ' + 'to original name %(wanted)s.', {'new': existing_name, + 'wanted': wanted_name}) + is_existed = self._cmd.get_volume(wanted_name) + if len(is_existed) == 1: + LOG.warn('volume name %(wanted)s is existed, The two volumes ' + '%(wanted)s and %(new)s may be on the same system.', + {'new': existing_name, + 'wanted': wanted_name}) + return {'_name_id': new_volume['_name_id'] or new_volume['id']} + else: + self._cmd.set_volume_property(existing_name, + {'new_name': wanted_name}) + return {'_name_id': None} def migrate_volume(self, ctxt, volume, host): LOG.debug('enter: migrate_volume id %(id)s, host %(host)s', @@ -581,7 +670,7 @@ class Acs5000CommonDriver(san.SanDriver, LOG.info('The target host belongs to the same storage system ' 'as the current but to a different pool. ' 'The same storage system will clone volume into the new pool') - volume_name = VOLUME_PREFIX + volume['id'][-12:] + volume_name = self._convert_name(volume.name) tmp_name = VOLUME_PREFIX + 'tmp' tmp_name += str(random.randint(0, 999999)).zfill(8) self._cmd.create_volume(tmp_name, @@ -596,6 +685,58 @@ class Acs5000CommonDriver(san.SanDriver, 'new_name': volume_name}) return (True, None) + def _manage_get_volume(self, ref, pool_name=None): + if 'source-name' in ref: + manage_source = ref['source-name'] + volumes = self._cmd.get_volume(manage_source) + else: + reason = _('Reference must contain source-name element ' + 'and only support source-name.') + raise exception.ManageExistingInvalidReference(existing_ref=ref, + reason=reason) + + if not volumes: + reason = (_('No volume by ref %s.') + % manage_source) + raise exception.ManageExistingInvalidReference(existing_ref=ref, + reason=reason) + volume = volumes[0] + if pool_name and pool_name != volume['poolname']: + reason = (_('Volume %(volume)s does not belong to pool name ' + '%(pool)s.') % {'volume': manage_source, + 'pool': pool_name}) + raise exception.ManageExistingInvalidReference(existing_ref=ref, + reason=reason) + return volume + + @volume_utils.trace_method + def manage_existing(self, volume, ref): + """Manages an existing volume.""" + volume_name = ref.get('source-name') + if not volume_name: + reason = _('Reference must contain source-name element ' + 'and only support source-name.') + raise exception.ManageExistingInvalidReference(existing_ref=ref, + reason=reason) + new_name = self._convert_name(volume.name) + self._cmd.set_volume_property(volume_name, {'type': '2', + 'new_name': new_name}) + + @volume_utils.trace_method + def manage_existing_get_size(self, volume, ref): + """Return size of an existing volume for manage_existing.""" + pool_name = volume_utils.extract_host(volume['host'], 'pool') + vol_backend = self._manage_get_volume(ref, pool_name) + size = int(vol_backend.get('size_mb', 0)) + size_gb = int(math.ceil(size / 1024)) + if (size_gb * 1024) > size: + LOG.warn('Volume %(vol)s capacity is %(mb)s MB, ' + 'extend to %(gb)s GB.', {'vol': ref['source-name'], + 'mb': size, + 'gb': size_gb}) + self._cmd.extend_volume(ref['source-name'], size_gb) + return size_gb + def get_volume_stats(self, refresh=False): """Get volume stats. @@ -628,15 +769,20 @@ class Acs5000CommonDriver(san.SanDriver, """Build pool status""" pool_stats = {} try: - pool_data = self._cmd.get_pool_info(pool) + pool_data = self._cmd.get_pool(pool) if pool_data: - total_capacity_gb = float(pool_data['capacity']) / units.Gi - free_capacity_gb = float(pool_data['free_capacity']) / units.Gi + total_capacity_gb = float( + pool_data['capacity']) / units.Gi + free_capacity_gb = float( + pool_data['free_capacity']) / units.Gi allocated_capacity_gb = float( pool_data['used_capacity']) / units.Gi total_volumes = None if 'total_volumes' in pool_data.keys(): total_volumes = int(pool_data['total_volumes']) + thin_provisioning = False + if 'thin' in pool_data and pool_data['thin'] == 'Enabled': + thin_provisioning = True pool_stats = { 'pool_name': pool_data['name'], 'total_capacity_gb': total_capacity_gb, @@ -647,8 +793,8 @@ class Acs5000CommonDriver(san.SanDriver, self.configuration.reserved_percentage, 'QoS_support': False, 'consistencygroup_support': False, - 'multiattach': False, - 'easytier_support': False, + 'multiattach': self.configuration.acs5000_multiattach, + 'thin_provisioning_support': thin_provisioning, 'total_volumes': total_volumes, 'system_id': self._state['system_id']} else: diff --git a/cinder/volume/drivers/toyou/acs5000/acs5000_fc.py b/cinder/volume/drivers/toyou/acs5000/acs5000_fc.py new file mode 100644 index 00000000000..49892c792ce --- /dev/null +++ b/cinder/volume/drivers/toyou/acs5000/acs5000_fc.py @@ -0,0 +1,165 @@ +# Copyright 2021 toyou 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. + +""" +acs5000 FC driver +""" + +from oslo_log import log as logging + +from cinder import exception +from cinder.i18n import _ +from cinder import interface +from cinder import utils +from cinder.volume.drivers.toyou.acs5000 import acs5000_common +from cinder.zonemanager import utils as zone_utils + +LOG = logging.getLogger(__name__) + + +@interface.volumedriver +class Acs5000FCDriver(acs5000_common.Acs5000CommonDriver): + """TOYOU ACS5000 storage FC volume driver. + + .. code-block:: none + + Version history: + 1.0.0 - Initial driver + + """ + + VENDOR = 'TOYOU' + VERSION = '1.0.0' + PROTOCOL = 'FC' + + # ThirdPartySystems wiki page + CI_WIKI_NAME = 'TOYOU_ACS5000_CI' + + def __init__(self, *args, **kwargs): + super(Acs5000FCDriver, self).__init__(*args, **kwargs) + self.protocol = self.PROTOCOL + + @staticmethod + def get_driver_options(): + return acs5000_common.Acs5000CommonDriver.get_driver_options() + + def _get_connected_wwpns(self): + fc_ports = self._cmd.ls_fc() + connected_wwpns = [] + for port in fc_ports: + if 'wwpn' in port: + connected_wwpns.append(port['wwpn']) + elif 'WWPN' in port: + connected_wwpns.append(port['WWPN']) + return connected_wwpns + + def validate_connector(self, connector): + """Check connector for at least one enabled FC protocol.""" + if 'wwpns' not in connector: + LOG.error('The connector does not ' + 'contain the required information.') + raise exception.InvalidConnectorException( + missing='wwpns') + + @utils.synchronized('acs5000A-host', external=True) + def initialize_connection(self, volume, connector): + LOG.debug('enter: initialize_connection: volume ' + '%(vol)s with connector %(conn)s', + {'vol': volume.id, 'conn': connector}) + volume_name = self._convert_name(volume.name) + ret = self._cmd.create_lun_map(volume_name, + self.protocol, + connector['wwpns']) + if ret['key'] == 0: + if 'lun' in ret['arr']: + lun_id = int(ret['arr']['lun']) + else: + msg = (_('_create_fc_lun: Lun id did not find ' + 'when volume %s create lun map.') % volume['id']) + raise exception.VolumeBackendAPIException(data=msg) + + target_wwpns = self._get_connected_wwpns() + if len(target_wwpns) == 0: + if self._check_multi_attached(volume, connector) < 1: + self._cmd.delete_lun_map(volume_name, + self.protocol, + connector['wwpns']) + msg = (_('_create_fc_lun: Did not find ' + 'available fc wwpns when volume %s ' + 'create lun map.') % volume['id']) + raise exception.VolumeBackendAPIException(data=msg) + + initiator_target = {} + for initiator_wwpn in connector['wwpns']: + initiator_target[str(initiator_wwpn)] = target_wwpns + properties = {'driver_volume_type': 'fibre_channel', + 'data': {'target_wwn': target_wwpns, + 'target_discovered': False, + 'target_lun': lun_id, + 'volume_id': volume['id']}} + properties['data']['initiator_target_map'] = initiator_target + elif ret['key'] == 303: + raise exception.VolumeNotFound(volume_id=volume_name) + else: + msg = (_('failed to map the volume %(vol)s to ' + 'connector %(conn)s.') % + {'vol': volume['id'], 'conn': connector}) + raise exception.VolumeBackendAPIException(data=msg) + + zone_utils.add_fc_zone(properties) + LOG.debug('leave: initialize_connection: volume ' + '%(vol)s with connector %(conn)s', + {'vol': volume.id, 'conn': connector}) + return properties + + @utils.synchronized('acs5000A-host', external=True) + def terminate_connection(self, volume, connector, **kwargs): + LOG.debug('enter: terminate_connection: volume ' + '%(vol)s with connector %(conn)s', + {'vol': volume.id, 'conn': connector}) + volume_name = self._convert_name(volume.name) + properties = {'driver_volume_type': 'fibre_channel', + 'data': {}} + initiator_wwpns = [] + target_wwpns = [] + if connector and 'wwpns' in connector: + initiator_wwpns = connector['wwpns'] + target_wwpns = self._get_connected_wwpns() + if len(target_wwpns) == 0: + target_wwpns = [] + LOG.warn('terminate_connection: Did not find ' + 'available fc wwpns when volume %s ' + 'delete lun map.', volume.id) + + initiator_target = {} + for i_wwpn in initiator_wwpns: + initiator_target[str(i_wwpn)] = target_wwpns + properties['data'] = {'initiator_target_map': initiator_target} + if self._check_multi_attached(volume, connector) < 2: + if not initiator_wwpns: + # -1 means all lun maps of this volume + initiator_wwpns = -1 + self._cmd.delete_lun_map(volume_name, + self.protocol, + initiator_wwpns) + else: + LOG.warn('volume %s has been mapped to multi VMs, and these VMs ' + 'belong to the same host. The mapping cancellation ' + 'request is aborted.', volume.id) + zone_utils.remove_fc_zone(properties) + LOG.debug('leave: terminate_connection: volume ' + '%(vol)s with connector %(conn)s', + {'vol': volume.id, 'conn': connector}) + return properties diff --git a/cinder/volume/drivers/toyou/acs5000/acs5000_iscsi.py b/cinder/volume/drivers/toyou/acs5000/acs5000_iscsi.py index eb6d984411d..55883ef8d7c 100644 --- a/cinder/volume/drivers/toyou/acs5000/acs5000_iscsi.py +++ b/cinder/volume/drivers/toyou/acs5000/acs5000_iscsi.py @@ -20,6 +20,7 @@ acs5000 iSCSI driver from oslo_log import log as logging from cinder import exception +from cinder.i18n import _ from cinder import interface from cinder import utils from cinder.volume.drivers.toyou.acs5000 import acs5000_common @@ -61,48 +62,63 @@ class Acs5000ISCSIDriver(acs5000_common.Acs5000CommonDriver): raise exception.InvalidConnectorException( missing='initiator') - @utils.synchronized('Acs5000A-host', external=True) + @utils.synchronized('acs5000A-host', external=True) def initialize_connection(self, volume, connector): - LOG.debug('initialize_connection: volume %(vol)s with connector ' - '%(conn)s', {'vol': volume['id'], 'conn': connector}) - volume_name = acs5000_common.VOLUME_PREFIX + volume['name'][-12:] + LOG.debug('enter: initialize_connection: volume ' + '%(vol)s with connector %(conn)s', + {'vol': volume.id, 'conn': connector}) + volume_name = self._convert_name(volume.name) ret = self._cmd.create_lun_map(volume_name, - 'WITH_ISCSI', + self.protocol, connector['initiator']) + if ret['key'] == 0: + lun_required = ['iscsi_name', 'portal', 'lun'] + lun_info = ret['arr'] + for param in lun_required: + if param not in lun_info: + msg = (_('initialize_connection: Param %(param)s ' + 'was not returned correctly when volume ' + '%(vol)s mapping.') % {'param': param, + 'vol': volume.id}) + raise exception.VolumeBackendAPIException(data=msg) + data = {'target_discovered': False, + 'target_iqns': lun_info['iscsi_name'], + 'target_portals': lun_info['portal'], + 'target_luns': lun_info['lun'], + 'volume_id': volume.id} + LOG.debug('leave: initialize_connection: volume ' + '%(vol)s with connector %(conn)s', + {'vol': volume.id, 'conn': connector}) + return {'driver_volume_type': 'iscsi', 'data': data} if ret['key'] == 303: raise exception.VolumeNotFound(volume_id=volume_name) elif ret['key'] == 402: raise exception.ISCSITargetAttachFailed(volume_id=volume_name) else: - lun_info = ret['arr'] - properties = {} - properties['target_discovered'] = False - properties['target_iqns'] = lun_info['iscsi_name'] - properties['target_portals'] = lun_info['portal'] - properties['target_luns'] = lun_info['lun'] - properties['volume_id'] = volume['id'] - properties['auth_method'] = '' - properties['auth_username'] = '' - properties['auth_password'] = '' - properties['discovery_auth_method'] = '' - properties['discovery_auth_username'] = '' - properties['discovery_auth_password'] = '' - return {'driver_volume_type': 'iscsi', 'data': properties} + msg = (_('failed to map the volume %(vol)s to ' + 'connector %(conn)s.') % + {'vol': volume['id'], 'conn': connector}) + raise exception.VolumeBackendAPIException(data=msg) - @utils.synchronized('Acs5000A-host', external=True) + @utils.synchronized('acs5000A-host', external=True) def terminate_connection(self, volume, connector, **kwargs): - LOG.debug('terminate_connection: volume %(vol)s with connector ' - '%(conn)s', {'vol': volume['id'], 'conn': connector}) - info = {'driver_volume_type': 'iscsi', 'data': {}} - name = acs5000_common.VOLUME_PREFIX + volume['name'][-12:] + LOG.debug('enter: terminate_connection: volume ' + '%(vol)s with connector %(conn)s', + {'vol': volume.id, 'conn': connector}) + name = self._convert_name(volume.name) # -1 means all lun maps initiator = '-1' if connector and connector['initiator']: initiator = connector['initiator'] - self._cmd.delete_lun_map(name, - 'WITH_ISCSI', - initiator) - LOG.debug('leave: terminate_connection: volume %(vol)s with ' - 'connector %(conn)s', {'vol': volume['id'], - 'conn': connector}) - return info + if self._check_multi_attached(volume, connector) < 2: + self._cmd.delete_lun_map(name, + self.protocol, + initiator) + else: + LOG.warn('volume %s has been mapped to multi VMs, and these VMs ' + 'belong to the same host. The mapping cancellation ' + 'request is aborted.', volume.id) + LOG.debug('leave: terminate_connection: volume ' + '%(vol)s with connector %(conn)s', + {'vol': volume.id, 'conn': connector}) + return {'driver_volume_type': 'iscsi', 'data': {}} diff --git a/doc/source/configuration/block-storage/drivers/toyou-acs5000-driver.rst b/doc/source/configuration/block-storage/drivers/toyou-acs5000-driver.rst deleted file mode 100644 index b659f785b71..00000000000 --- a/doc/source/configuration/block-storage/drivers/toyou-acs5000-driver.rst +++ /dev/null @@ -1,72 +0,0 @@ -========================== -TOYOU ACS5000 iSCSI driver -========================== - -TOYOU ACS5000 series volume driver provides OpenStack Compute instances -with access to TOYOU ACS5000 series storage systems. - -TOYOU ACS5000 storage can be used with iSCSI connection. - -This documentation explains how to configure and connect the block storage -nodes to TOYOU ACS5000 series storage. - -Driver options -~~~~~~~~~~~~~~ - -The following table contains the configuration options supported by the -TOYOU ACS5000 iSCSI driver. - -.. config-table:: - :config-target: TOYOU ACS5000 - - cinder.volume.drivers.toyou.acs5000.acs5000_iscsi - cinder.volume.drivers.toyou.acs5000.acs5000_common - -Supported operations -~~~~~~~~~~~~~~~~~~~~ - -- Create, list, delete, attach (map), and detach (unmap) volumes. -- Create, list and delete volume snapshots. -- Create a volume from a snapshot. -- Copy an image to a volume. -- Copy a volume to an image. -- Clone a volume. -- Extend a volume. -- Migrate a volume. - -Configure TOYOU ACS5000 iSCSI backend -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This section details the steps required to configure the TOYOU ACS5000 -storage cinder driver. - -#. In the ``cinder.conf`` configuration file under the ``[DEFAULT]`` - section, set the enabled_backends parameter. - - .. code-block:: ini - - [DEFAULT] - enabled_backends = ACS5000-1 - - -#. Add a backend group section for the backend group specified - in the enabled_backends parameter. - -#. In the newly created backend group section, set the - following configuration options: - - .. code-block:: ini - - [ACS5000-1] - # The driver path - volume_driver = cinder.volume.drivers.toyou.acs5000.acs5000_iscsi.Acs5000ISCSIDriver - # Management IP of TOYOU ACS5000 storage array - san_ip = 10.0.0.10 - # Management username of TOYOU ACS5000 storage array - san_login = cliuser - # Management password of TOYOU ACS5000 storage array - san_password = clipassword - # The Pool used to allocated volumes - acs5000_volpool_name = pool01 - # Backend name - volume_backend_name = ACS5000 diff --git a/doc/source/configuration/block-storage/drivers/toyou-netstor-driver.rst b/doc/source/configuration/block-storage/drivers/toyou-netstor-driver.rst new file mode 100644 index 00000000000..456c00b2000 --- /dev/null +++ b/doc/source/configuration/block-storage/drivers/toyou-netstor-driver.rst @@ -0,0 +1,106 @@ +=========================== +TOYOU NetStor Cinder driver +=========================== + +TOYOU NetStor series volume driver provides OpenStack Compute instances +with access to TOYOU NetStor series storage systems. + +TOYOU NetStor storage can be used with iSCSI or FC connection. + +This documentation explains how to configure and connect the block storage +nodes to TOYOU NetStor series storage. + +Driver options +~~~~~~~~~~~~~~ + +The following table contains the configuration options supported by the +TOYOU NetStor iSCSI/FC driver. + +.. config-table:: + :config-target: TOYOU NetStor + + cinder.volume.drivers.toyou.acs5000.acs5000_common + +Supported operations +~~~~~~~~~~~~~~~~~~~~ + +- Create, list, delete, attach (map), and detach (unmap) volumes. +- Create, list and delete volume snapshots. +- Create a volume from a snapshot. +- Copy an image to a volume. +- Copy a volume to an image. +- Clone a volume. +- Extend a volume. +- Migrate a volume. +- Manage/Unmanage volume. +- Revert to Snapshot. +- Multi-attach. +- Thin Provisioning. +- Extend Attached Volume. + +Configure TOYOU NetStor iSCSI/FC backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This section details the steps required to configure the TOYOU NetStor +storage cinder driver. + +#. In the ``cinder.conf`` configuration file under the ``[DEFAULT]`` + section, set the enabled_backends parameter + with the iSCSI or FC back-end group. + + - For Fibre Channel: + + .. code-block:: ini + + [DEFAULT] + enabled_backends = toyou-fc-1 + + - For iSCSI: + + .. code-block:: ini + + [DEFAULT] + enabled_backends = toyou-iscsi-1 + + +#. Add a backend group section for the backend group specified + in the enabled_backends parameter. + +#. In the newly created backend group section, set the + following configuration options: + + - For Fibre Channel: + + .. code-block:: ini + + [toyou-fc-1] + # The TOYOU NetStor driver path + volume_driver = cinder.volume.drivers.toyou.acs5000.acs5000_fc.Acs5000FCDriver + # Management IP of TOYOU NetStor storage array + san_ip = 10.0.0.10 + # Management username of TOYOU NetStor storage array + san_login = cliuser + # Management password of TOYOU NetStor storage array + san_password = clipassword + # The Pool used to allocated volumes + acs5000_volpool_name = pool01 + # Backend name + volume_backend_name = toyou-fc + + - For iSCSI: + + .. code-block:: ini + + [toyou-iscsi-1] + # The TOYOU NetStor driver path + volume_driver = cinder.volume.drivers.toyou.acs5000.acs5000_iscsi.Acs5000ISCSIDriver + # Management IP of TOYOU NetStor storage array + san_ip = 10.0.0.10 + # Management username of TOYOU NetStor storage array + san_login = cliuser + # Management password of TOYOU NetStor storage array + san_password = clipassword + # The Pool used to allocated volumes + acs5000_volpool_name = pool01 + # Backend name + volume_backend_name = toyou-iscsi diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index abae577969b..cb7c5d08218 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -183,8 +183,8 @@ title=StorPool Storage Driver (storpool) [driver.synology] title=Synology Storage Driver (iSCSI) -[driver.toyou] -title=TOYOU ACS5000 Storage Driver (iSCSI) +[driver.toyou_netstor] +title=TOYOU NetStor Storage Driver (iSCSI, FC) [driver.vrtsaccess] title=Veritas Access iSCSI Driver (iSCSI) @@ -273,7 +273,7 @@ driver.sandstone=complete driver.seagate=complete driver.storpool=complete driver.synology=complete -driver.toyou=complete +driver.toyou_netstor=complete driver.vrtsaccess=missing driver.vrtscnfs=missing driver.vzstorage=missing @@ -343,7 +343,7 @@ driver.sandstone=complete driver.seagate=complete driver.storpool=complete driver.synology=complete -driver.toyou=missing +driver.toyou_netstor=complete driver.vrtsaccess=complete driver.vrtscnfs=complete driver.vzstorage=complete @@ -416,7 +416,7 @@ driver.sandstone=complete driver.seagate=missing driver.storpool=missing driver.synology=missing -driver.toyou=missing +driver.toyou_netstor=missing driver.vrtsaccess=missing driver.vrtscnfs=missing driver.vzstorage=missing @@ -488,7 +488,7 @@ driver.sandstone=complete driver.seagate=missing driver.storpool=complete driver.synology=missing -driver.toyou=missing +driver.toyou_netstor=missing driver.vrtsaccess=missing driver.vrtscnfs=missing driver.vzstorage=missing @@ -561,7 +561,7 @@ driver.sandstone=missing driver.seagate=missing driver.storpool=missing driver.synology=missing -driver.toyou=missing +driver.toyou_netstor=missing driver.vrtsaccess=missing driver.vrtscnfs=missing driver.vzstorage=missing @@ -633,7 +633,7 @@ driver.sandstone=complete driver.seagate=missing driver.storpool=complete driver.synology=missing -driver.toyou=missing +driver.toyou_netstor=complete driver.vrtsaccess=missing driver.vrtscnfs=missing driver.vzstorage=missing @@ -706,7 +706,7 @@ driver.sandstone=missing driver.seagate=missing driver.storpool=complete driver.synology=missing -driver.toyou=complete +driver.toyou_netstor=complete driver.vrtsaccess=missing driver.vrtscnfs=missing driver.vzstorage=missing @@ -779,7 +779,7 @@ driver.sandstone=complete driver.seagate=complete driver.storpool=complete driver.synology=missing -driver.toyou=missing +driver.toyou_netstor=complete driver.vrtsaccess=missing driver.vrtscnfs=missing driver.vzstorage=missing @@ -849,7 +849,7 @@ driver.sandstone=complete driver.seagate=missing driver.storpool=missing driver.synology=missing -driver.toyou=missing +driver.toyou_netstor=complete driver.vrtsaccess=missing driver.vrtscnfs=missing driver.vzstorage=missing @@ -923,7 +923,7 @@ driver.sandstone=complete driver.seagate=missing driver.storpool=missing driver.synology=missing -driver.toyou=missing +driver.toyou_netstor=missing driver.vrtsaccess=missing driver.vrtscnfs=missing driver.vzstorage=missing diff --git a/releasenotes/notes/toyou-netstor-storage-acs5000-fc-driver-f0d7428924bfeda1.yaml b/releasenotes/notes/toyou-netstor-storage-acs5000-fc-driver-f0d7428924bfeda1.yaml new file mode 100644 index 00000000000..dbf71b20c7d --- /dev/null +++ b/releasenotes/notes/toyou-netstor-storage-acs5000-fc-driver-f0d7428924bfeda1.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + New FC cinder volume driver for TOYOU NetStor Storage.