From 73a5cb1f7584a0ad7dbf40889d06b1e8fd7c61d2 Mon Sep 17 00:00:00 2001 From: xiaolei hu Date: Thu, 25 Jun 2015 10:22:50 +0800 Subject: [PATCH] Separate FlashSystem FC and iSCSI common code The patch is mainly to split original FC driver code into common code and FC driver for IBM FlashSystem. iSCSI driver merged in L-1 has been inherited from original FC driver for common functions already. In this patch, * Separate FC driver into new driver and common code. * Modify iSCSI driver to inherite the common code. * Common function _get_node_data in iSCSI driver is moved into common code. * Add locks in initialize_connection and terminate_connection for iSCSI driver. * Remove flashsystem_multipath_enabled to use Cinder iSCSI multipath support. Implements: blueprint ibm-flashsystem-split-fc Change-Id: I166a14e3eef370a22f4c0a675d451a3a4a6989f1 --- cinder/tests/unit/test_ibm_flashsystem.py | 20 +- .../{flashsystem.py => flashsystem_common.py} | 486 ++++-------------- cinder/volume/drivers/ibm/flashsystem_fc.py | 379 ++++++++++++++ .../volume/drivers/ibm/flashsystem_iscsi.py | 110 +--- 4 files changed, 510 insertions(+), 485 deletions(-) rename cinder/volume/drivers/ibm/{flashsystem.py => flashsystem_common.py} (73%) create mode 100644 cinder/volume/drivers/ibm/flashsystem_fc.py diff --git a/cinder/tests/unit/test_ibm_flashsystem.py b/cinder/tests/unit/test_ibm_flashsystem.py index 3daf32b30..cdd9cec4c 100644 --- a/cinder/tests/unit/test_ibm_flashsystem.py +++ b/cinder/tests/unit/test_ibm_flashsystem.py @@ -34,7 +34,7 @@ from cinder import exception from cinder import test from cinder import utils from cinder.volume import configuration as conf -from cinder.volume.drivers.ibm import flashsystem +from cinder.volume.drivers.ibm import flashsystem_fc from cinder.volume import utils as volume_utils from cinder.volume import volume_types @@ -656,7 +656,7 @@ class FlashSystemManagementSimulator(object): self._next_cmd_error[cmd] = error -class FlashSystemFakeDriver(flashsystem.FlashSystemDriver): +class FlashSystemFakeDriver(flashsystem_fc.FlashSystemFCDriver): def __init__(self, *args, **kwargs): super(FlashSystemFakeDriver, self).__init__(*args, **kwargs) @@ -893,7 +893,7 @@ class FlashSystemDriverTestCase(test.TestCase): vol2, self.connector) # case 3: _get_vdisk_map_properties raises exception - with mock.patch.object(flashsystem.FlashSystemDriver, + with mock.patch.object(flashsystem_fc.FlashSystemFCDriver, '_get_vdisk_map_properties') as get_properties: get_properties.side_effect = exception.VolumeBackendAPIException self.assertRaises(exception.VolumeBackendAPIException, @@ -903,7 +903,7 @@ class FlashSystemDriverTestCase(test.TestCase): # clear environment self.driver.delete_volume(vol1) - @mock.patch.object(flashsystem.FlashSystemDriver, + @mock.patch.object(flashsystem_fc.FlashSystemFCDriver, '_create_and_copy_vdisk_data') def test_flashsystem_create_snapshot(self, _create_and_copy_vdisk_data): # case 1: good path @@ -923,7 +923,7 @@ class FlashSystemDriverTestCase(test.TestCase): self.assertRaises(exception.InvalidVolume, self.driver.create_snapshot, snap2) - @mock.patch.object(flashsystem.FlashSystemDriver, + @mock.patch.object(flashsystem_fc.FlashSystemFCDriver, '_delete_vdisk') def test_flashsystem_delete_snapshot(self, _delete_vdisk): vol1 = self._generate_vol_info(None) @@ -933,7 +933,7 @@ class FlashSystemDriverTestCase(test.TestCase): vol1['status']) self.driver.delete_snapshot(snap1) - @mock.patch.object(flashsystem.FlashSystemDriver, + @mock.patch.object(flashsystem_fc.FlashSystemFCDriver, '_create_and_copy_vdisk_data') def test_flashsystem_create_volume_from_snapshot( self, _create_and_copy_vdisk_data): @@ -966,7 +966,7 @@ class FlashSystemDriverTestCase(test.TestCase): self.driver.create_volume_from_snapshot, vol, snap) - @mock.patch.object(flashsystem.FlashSystemDriver, + @mock.patch.object(flashsystem_fc.FlashSystemFCDriver, '_create_and_copy_vdisk_data') def test_flashsystem_create_cloned_volume( self, _create_and_copy_vdisk_data): @@ -1002,7 +1002,7 @@ class FlashSystemDriverTestCase(test.TestCase): self.assertRaises(exception.VolumeBackendAPIException, self.driver.get_volume_stats, refresh=True) - @mock.patch.object(flashsystem.FlashSystemDriver, + @mock.patch.object(flashsystem_fc.FlashSystemFCDriver, '_copy_vdisk_data') def test_flashsystem_create_and_copy_vdisk_data(self, _copy_vdisk_data): # case 1: when volume does not exist @@ -1033,8 +1033,8 @@ class FlashSystemDriverTestCase(test.TestCase): self.driver.delete_volume(vol2) @mock.patch.object(volume_utils, 'copy_volume') - @mock.patch.object(flashsystem.FlashSystemDriver, '_scan_device') - @mock.patch.object(flashsystem.FlashSystemDriver, '_remove_device') + @mock.patch.object(flashsystem_fc.FlashSystemFCDriver, '_scan_device') + @mock.patch.object(flashsystem_fc.FlashSystemFCDriver, '_remove_device') @mock.patch.object(utils, 'brick_get_connector_properties') def test_flashsystem_copy_vdisk_data(self, _connector, diff --git a/cinder/volume/drivers/ibm/flashsystem.py b/cinder/volume/drivers/ibm/flashsystem_common.py similarity index 73% rename from cinder/volume/drivers/ibm/flashsystem.py rename to cinder/volume/drivers/ibm/flashsystem_common.py index 593ceeae4..9744dbbf3 100644 --- a/cinder/volume/drivers/ibm/flashsystem.py +++ b/cinder/volume/drivers/ibm/flashsystem_common.py @@ -1,4 +1,4 @@ -# Copyright 2014 - 2015 IBM Corporation. +# Copyright 2015 IBM Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -19,14 +19,11 @@ Volume driver for IBM FlashSystem storage systems. Limitations: 1. Cinder driver only works when open_access_enabled=off. -2. Cinder driver only works when connection protocol is FC. """ -import random import re import string -import threading from oslo_concurrency import processutils from oslo_config import cfg @@ -44,7 +41,6 @@ from cinder import utils from cinder.volume.drivers.san import san from cinder.volume import utils as volume_utils from cinder.volume import volume_types -from cinder.zonemanager import utils as fczm_utils LOG = logging.getLogger(__name__) @@ -54,13 +50,12 @@ FLASHSYSTEM_VOL_IOGRP = 0 flashsystem_opts = [ cfg.StrOpt('flashsystem_connection_protocol', default='FC', - help='Connection protocol should be FC.'), - cfg.BoolOpt('flashsystem_multipath_enabled', - default=False, - help='Connect with multipath (FC only).'), + help='Connection protocol should be FC. ' + '(Default is FC.)'), cfg.BoolOpt('flashsystem_multihostmap_enabled', default=True, - help='Allows vdisk to multi host mapping.') + help='Allows vdisk to multi host mapping. ' + '(Default is True)') ] CONF = cfg.CONF @@ -68,17 +63,19 @@ CONF.register_opts(flashsystem_opts) class FlashSystemDriver(san.SanDriver): - """IBM FlashSystem 840 FC volume driver. + """IBM FlashSystem volume driver. Version history: 1.0.0 - Initial driver 1.0.1 - Code clean up 1.0.2 - Add lock into vdisk map/unmap, connection initialize/terminate + 1.0.3 - Initial driver for iSCSI + 1.0.4 - Split Flashsystem driver into common and FC """ - VERSION = "1.0.1" + VERSION = "1.0.4" def __init__(self, *args, **kwargs): super(FlashSystemDriver, self).__init__(*args, **kwargs) @@ -134,16 +131,6 @@ class FlashSystemDriver(san.SanDriver): map[idx].append(t_wwpn) return map - def _check_vdisk_params(self, params): - # Check that the requested protocol is enabled - if params['protocol'] != self._protocol: - msg = (_("Illegal value '%(prot)s' specified for " - "flashsystem_connection_protocol: " - "valid value(s) are %(enabled)s.") - % {'prot': params['protocol'], - 'enabled': self._protocol}) - raise exception.InvalidInput(reason=msg) - def _connector_to_hostname_prefix(self, connector): """Translate connector info to storage system host name. @@ -172,8 +159,8 @@ class FlashSystemDriver(san.SanDriver): invalid_ch_in_host, '-' * len(invalid_ch_in_host)) host_name = host_name.translate(string_host_name_filter) else: - msg = (_('_create_host: Can not clean host name. Host name ' - 'is not unicode or string.')) + msg = _('_create_host: Can not translate host name. Host name ' + 'is not unicode or string.') LOG.error(msg) raise exception.NoValidHost(reason=msg) @@ -237,8 +224,7 @@ class FlashSystemDriver(san.SanDriver): self.configuration.volume_dd_blocksize) except Exception: with excutils.save_and_reraise_exception(): - LOG.error(_LE('_copy_vdisk_data: Failed to ' - 'copy %(src)s to %(dest)s.'), + LOG.error(_LE('Failed to copy %(src)s to %(dest)s.'), {'src': src_vdisk_name, 'dest': dest_vdisk_name}) finally: if not dest_map: @@ -275,50 +261,6 @@ class FlashSystemDriver(san.SanDriver): self._unset_vdisk_copy_in_progress( [src_vdisk_name, dest_vdisk_name]) - def _create_host(self, connector): - """Create a new host on the storage system. - - We create a host and associate it with the given connection - information. - - """ - - LOG.debug('enter: _create_host: host %s.', connector['host']) - - rand_id = six.text_type(random.randint(0, 99999999)).zfill(8) - host_name = '%s-%s' % (self._connector_to_hostname_prefix(connector), - rand_id) - - ports = [] - if 'FC' == self._protocol and 'wwpns' in connector: - for wwpn in connector['wwpns']: - ports.append('-hbawwpn %s' % wwpn) - - self._driver_assert(ports, - (_('_create_host: No connector ports.'))) - port1 = ports.pop(0) - arg_name, arg_val = port1.split() - ssh_cmd = ['svctask', 'mkhost', '-force', arg_name, arg_val, '-name', - '"%s"' % host_name] - out, err = self._ssh(ssh_cmd) - self._assert_ssh_return('successfully created' in out, - '_create_host', ssh_cmd, out, err) - - for port in ports: - arg_name, arg_val = port.split() - ssh_cmd = ['svctask', 'addhostport', '-force', - arg_name, arg_val, host_name] - out, err = self._ssh(ssh_cmd) - self._assert_ssh_return( - (not out.strip()), - '_create_host', ssh_cmd, out, err) - - LOG.debug( - 'leave: _create_host: host %(host)s - %(host_name)s.', - {'host': connector['host'], 'host_name': host_name}) - - return host_name - def _create_vdisk(self, name, size, unit, opts): """Create a new vdisk.""" @@ -405,8 +347,8 @@ class FlashSystemDriver(san.SanDriver): try: out, err = self._ssh(ssh_cmd) except processutils.ProcessExecutionError: - LOG.warning(_LW('_execute_command_and_parse_attributes: Failed to ' - 'run command: %s.'), ssh_cmd) + LOG.warning(_LW('Failed to run command: ' + '%s.'), ssh_cmd) # Does not raise exception when command encounters error. # Only return and the upper logic decides what to do. return None @@ -430,22 +372,6 @@ class FlashSystemDriver(san.SanDriver): return attributes - def _find_host_exhaustive(self, connector, hosts): - for host in hosts: - ssh_cmd = ['svcinfo', 'lshost', '-delim', '!', host] - out, err = self._ssh(ssh_cmd) - self._assert_ssh_return( - out.strip(), - '_find_host_exhaustive', ssh_cmd, out, err) - for attr_line in out.split('\n'): - # If '!' not found, return the string and two empty strings - attr_name, foo, attr_val = attr_line.partition('!') - if (attr_name == 'WWPN' and - 'wwpns' in connector and attr_val.lower() in - map(str.lower, map(str, connector['wwpns']))): - return host - return None - def _get_hdr_dic(self, header, row, delim): """Return CLI row data as a dictionary indexed by names from header. string. The strings are converted to columns using the delimiter in @@ -462,38 +388,6 @@ class FlashSystemDriver(san.SanDriver): dic = {a: v for a, v in map(None, attributes, values)} return dic - def _get_conn_fc_wwpns(self): - wwpns = [] - - cmd = ['svcinfo', 'lsportfc'] - - generator = self._port_conf_generator(cmd) - header = next(generator, None) - if not header: - return wwpns - - for port_data in generator: - try: - if port_data['status'] == 'active': - wwpns.append(port_data['WWPN']) - except KeyError: - self._handle_keyerror('lsportfc', header) - - return wwpns - - def _get_fc_wwpns(self): - for key in self._storage_nodes: - node = self._storage_nodes[key] - ssh_cmd = ['svcinfo', 'lsnode', '-delim', '!', node['id']] - attributes = self._execute_command_and_parse_attributes(ssh_cmd) - wwpns = set(node['WWPN']) - for i, s in zip(attributes['port_id'], attributes['port_status']): - if 'unconfigured' != s: - wwpns.add(i) - node['WWPN'] = list(wwpns) - LOG.info(_LI('WWPN on node %(node)s: %(wwpn)s.'), - {'node': node['id'], 'wwpn': node['WWPN']}) - def _get_host_from_connector(self, connector): """List the hosts defined in the storage. @@ -546,6 +440,78 @@ class FlashSystemDriver(san.SanDriver): return return_data + def _get_node_data(self): + """Get and verify node configuration.""" + + # Get storage system name and id + ssh_cmd = ['svcinfo', 'lssystem', '-delim', '!'] + attributes = self._execute_command_and_parse_attributes(ssh_cmd) + if not attributes or not ('name' in attributes): + msg = _('Could not get system name.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + self._system_name = attributes['name'] + self._system_id = attributes['id'] + + # Validate value of open_access_enabled flag, for now only + # support when open_access_enabled is off + if not attributes or not ('open_access_enabled' in attributes) or ( + attributes['open_access_enabled'] != 'off'): + msg = _('open_access_enabled is not off.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + # Validate that the array exists + pool = FLASHSYSTEM_VOLPOOL_NAME + ssh_cmd = ['svcinfo', 'lsmdiskgrp', '-bytes', '-delim', '!', pool] + attributes = self._execute_command_and_parse_attributes(ssh_cmd) + if not attributes: + msg = _('Unable to parse attributes.') + LOG.error(msg) + raise exception.InvalidInput(reason=msg) + if not ('status' in attributes) or ( + attributes['status'] == 'offline'): + msg = (_('Array does not exist or is offline. ' + 'Current status of array is %s.') + % attributes['status']) + LOG.error(msg) + raise exception.InvalidInput(reason=msg) + + # Get the iSCSI names of the FlashSystem nodes + ssh_cmd = ['svcinfo', 'lsnode', '-delim', '!'] + out, err = self._ssh(ssh_cmd) + self._assert_ssh_return( + out.strip(), '_get_config_data', ssh_cmd, out, err) + + nodes = out.strip().splitlines() + self._assert_ssh_return(nodes, '_get_node_data', ssh_cmd, out, err) + header = nodes.pop(0) + for node_line in nodes: + try: + node_data = self._get_hdr_dic(header, node_line, '!') + except exception.VolumeBackendAPIException: + with excutils.save_and_reraise_exception(): + self._log_cli_output_error('_get_node_data', + ssh_cmd, out, err) + try: + node = { + 'id': node_data['id'], + 'name': node_data['name'], + 'IO_group': node_data['IO_group_id'], + 'WWNN': node_data['WWNN'], + 'status': node_data['status'], + 'WWPN': [], + 'protocol': None, + 'iscsi_name': node_data['iscsi_name'], + 'config_node': node_data['config_node'], + 'ipv4': [], + 'ipv6': [], + } + if node['status'] == 'online': + self._storage_nodes[node['id']] = node + except KeyError: + self._handle_keyerror('lsnode', header) + def _get_vdisk_attributes(self, vdisk_name): """Return vdisk attributes @@ -574,70 +540,6 @@ class FlashSystemDriver(san.SanDriver): return return_data - def _get_vdisk_map_properties( - self, connector, lun_id, vdisk_name, vdisk_id, vdisk_params): - """Get the map properties of vdisk.""" - - LOG.debug( - 'enter: _get_vdisk_map_properties: vdisk ' - '%(vdisk_name)s.', {'vdisk_name': vdisk_name}) - - preferred_node = '0' - IO_group = '0' - - # Get preferred node and other nodes in I/O group - preferred_node_entry = None - io_group_nodes = [] - for k, node in self._storage_nodes.items(): - if vdisk_params['protocol'] != node['protocol']: - continue - if node['id'] == preferred_node: - preferred_node_entry = node - if node['IO_group'] == IO_group: - io_group_nodes.append(node) - - if not io_group_nodes: - msg = (_('_get_vdisk_map_properties: No node found in ' - 'I/O group %(gid)s for volume %(vol)s.') - % {'gid': IO_group, 'vol': vdisk_name}) - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - - if not preferred_node_entry and not vdisk_params['multipath']: - # Get 1st node in I/O group - preferred_node_entry = io_group_nodes[0] - LOG.warning(_LW('_get_vdisk_map_properties: Did not find a ' - 'preferred node for vdisk %s.'), vdisk_name) - properties = {} - properties['target_discovered'] = False - properties['target_lun'] = lun_id - properties['volume_id'] = vdisk_id - - type_str = 'fibre_channel' - conn_wwpns = self._get_conn_fc_wwpns() - - if not conn_wwpns: - msg = (_('_get_vdisk_map_properties: Could not get FC ' - 'connection information for the host-volume ' - 'connection. Is the host configured properly ' - 'for FC connections?')) - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - - properties['target_wwn'] = conn_wwpns - - if "zvm_fcp" in connector: - properties['zvm_fcp'] = connector['zvm_fcp'] - - properties['initiator_target_map'] = self._build_initiator_target_map( - connector['wwpns'], conn_wwpns) - - LOG.debug( - 'leave: _get_vdisk_map_properties: vdisk ' - '%(vdisk_name)s.', {'vdisk_name': vdisk_name}) - - return {'driver_volume_type': type_str, 'data': properties} - def _get_vdisk_params(self, type_id): params = self._build_default_params() if type_id: @@ -792,11 +694,11 @@ class FlashSystemDriver(san.SanDriver): out, err = self._ssh(ssh_cmd, check_exit_code=False) if err and err.startswith('CMMVC6071E'): if not self.configuration.flashsystem_multihostmap_enabled: - msg = (_('flashsystem_multihostmap_enabled is set ' - 'to False, not allow multi host mapping. ' - 'CMMVC6071E The VDisk-to-host mapping ' - 'was not created because the VDisk is ' - 'already mapped to a host.')) + msg = _('flashsystem_multihostmap_enabled is set ' + 'to False, not allow multi host mapping. ' + 'CMMVC6071E The VDisk-to-host mapping ' + 'was not created because the VDisk is ' + 'already mapped to a host.') LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) @@ -961,7 +863,7 @@ class FlashSystemDriver(san.SanDriver): ssh_cmd = ['svcinfo', 'lsmdiskgrp', '-bytes', '-delim', '!', pool] attributes = self._execute_command_and_parse_attributes(ssh_cmd) if not attributes: - msg = (_('_update_volume_stats: Could not get storage pool data.')) + msg = _('_update_volume_stats: Could not get storage pool data.') LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) @@ -1014,97 +916,6 @@ class FlashSystemDriver(san.SanDriver): self._is_vdisk_copy_in_progress, vdisk_name) timer.start(interval=self._check_lock_interval).wait() - def do_setup(self, ctxt): - """Check that we have all configuration details from the storage.""" - - LOG.debug('enter: do_setup') - - self._context = ctxt - - # Get storage system name and id - ssh_cmd = ['svcinfo', 'lssystem', '-delim', '!'] - attributes = self._execute_command_and_parse_attributes(ssh_cmd) - if not attributes or not ('name' in attributes): - msg = (_('do_setup: Could not get system name.')) - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - self._system_name = attributes['name'] - self._system_id = attributes['id'] - - # Validate value of open_access_enabled flag, for now only - # support when open_access_enabled is off - if not attributes or not ('open_access_enabled' in attributes) or ( - attributes['open_access_enabled'] != 'off'): - msg = (_('do_setup: open_access_enabled is not off.')) - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - - # Validate that the array exists - pool = FLASHSYSTEM_VOLPOOL_NAME - ssh_cmd = ['svcinfo', 'lsmdiskgrp', '-bytes', '-delim', '!', pool] - attributes = self._execute_command_and_parse_attributes(ssh_cmd) - if not attributes or not ('status' in attributes) or ( - attributes['status'] == 'offline'): - msg = (_('do_setup: Array does not exist or is offline.')) - LOG.error(msg) - raise exception.InvalidInput(reason=msg) - - # Get the FC names of the FlashSystem nodes - ssh_cmd = ['svcinfo', 'lsnode', '-delim', '!'] - out, err = self._ssh(ssh_cmd) - self._assert_ssh_return( - out.strip(), 'do_setup', ssh_cmd, out, err) - - nodes = out.strip().splitlines() - self._assert_ssh_return(nodes, 'do_setup', ssh_cmd, out, err) - header = nodes.pop(0) - for node_line in nodes: - try: - node_data = self._get_hdr_dic(header, node_line, '!') - except exception.VolumeBackendAPIException: - with excutils.save_and_reraise_exception(): - self._log_cli_output_error('do_setup', ssh_cmd, out, err) - node = {} - try: - node['id'] = node_data['id'] - node['name'] = node_data['name'] - node['IO_group'] = node_data['IO_group_id'] - node['WWNN'] = node_data['WWNN'] - node['status'] = node_data['status'] - node['WWPN'] = [] - node['protocol'] = None - if node['status'] == 'online': - self._storage_nodes[node['id']] = node - except KeyError: - self._handle_keyerror('lsnode', header) - - # Get the WWPNs of the FlashSystem nodes - self._get_fc_wwpns() - - # For each node, check what connection modes it supports. Delete any - # nodes that do not support any types (may be partially configured). - to_delete = [] - for k, node in self._storage_nodes.items(): - if not node['WWPN']: - to_delete.append(k) - - for delkey in to_delete: - del self._storage_nodes[delkey] - - # Make sure we have at least one node configured - self._driver_assert( - self._storage_nodes, - 'do_setup: No configured nodes.') - - self._protocol = node['protocol'] = 'FC' - - # Set for vdisk synchronization - self._vdisk_copy_in_progress = set() - self._vdisk_copy_lock = threading.Lock() - self._check_lock_interval = 5 - - LOG.debug('leave: do_setup') - def check_for_setup_error(self): """Ensure that the flags are set properly.""" LOG.debug('enter: check_for_setup_error') @@ -1115,7 +926,7 @@ class FlashSystemDriver(san.SanDriver): _('check_for_setup_error: Unable to determine system name.')) raise exception.VolumeBackendAPIException(data=msg) if self._system_id is None: - msg = (_('check_for_setup_error: Unable to determine system id.')) + msg = _('check_for_setup_error: Unable to determine system id.') raise exception.VolumeBackendAPIException(data=msg) required_flags = ['san_ip', 'san_ssh_port', 'san_login'] @@ -1127,9 +938,9 @@ class FlashSystemDriver(san.SanDriver): # Ensure that either password or keyfile were set if not (self.configuration.san_password or self.configuration.san_private_key): - msg = (_('check_for_setup_error: Password or SSH private key ' - 'is required for authentication: set either ' - 'san_password or san_private_key option.')) + msg = _('check_for_setup_error: Password or SSH private key ' + 'is required for authentication: set either ' + 'san_password or san_private_key option.') raise exception.InvalidInput(reason=msg) params = self._build_default_params() @@ -1137,13 +948,6 @@ class FlashSystemDriver(san.SanDriver): LOG.debug('leave: check_for_setup_error') - def validate_connector(self, connector): - """Check connector.""" - if 'FC' == self._protocol and 'wwpns' not in connector: - LOG.error(_LE('The connector does not contain the ' - 'required information: wwpns is missing')) - raise exception.InvalidConnectorException(missing='wwpns') - def create_volume(self, volume): """Create volume.""" vdisk_name = volume['name'] @@ -1175,96 +979,6 @@ class FlashSystemDriver(san.SanDriver): LOG.debug('leave: extend_volume: volume %s.', volume['name']) - @fczm_utils.AddFCZone - @utils.synchronized('flashsystem-init-conn', external=True) - def initialize_connection(self, volume, connector): - """Perform the necessary work so that a FC connection can - be made. - - To be able to create a FC connection from a given host to a - volume, we must: - 1. Translate the given WWNN to a host name - 2. Create new host on the storage system if it does not yet exist - 3. Map the volume to the host if it is not already done - 4. Return the connection information for relevant nodes (in the - proper I/O group) - - """ - - LOG.debug( - 'enter: initialize_connection: volume %(vol)s with ' - 'connector %(conn)s.', {'vol': volume, 'conn': connector}) - - vdisk_name = volume['name'] - vdisk_id = volume['id'] - vdisk_params = self._get_vdisk_params(volume['volume_type_id']) - - self._wait_vdisk_copy_completed(vdisk_name) - - self._driver_assert( - self._is_vdisk_defined(vdisk_name), - (_('initialize_connection: vdisk %s is not defined.') - % vdisk_name)) - - lun_id = self._map_vdisk_to_host(vdisk_name, connector) - - properties = {} - try: - properties = self._get_vdisk_map_properties( - connector, lun_id, vdisk_name, vdisk_id, vdisk_params) - except exception.VolumeBackendAPIException: - with excutils.save_and_reraise_exception(): - self.terminate_connection(volume, connector) - LOG.error(_LE('initialize_connection: Failed to collect ' - 'return properties for volume %(vol)s and ' - 'connector %(conn)s.'), - {'vol': volume, 'conn': connector}) - - LOG.debug( - 'leave: initialize_connection:\n volume: %(vol)s\n connector ' - '%(conn)s\n properties: %(prop)s.', - {'vol': volume, - 'conn': connector, - 'prop': properties}) - - return properties - - @fczm_utils.RemoveFCZone - @utils.synchronized('flashsystem-term-conn', external=True) - def terminate_connection(self, volume, connector, **kwargs): - """Cleanup after 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) - """ - LOG.debug( - 'enter: terminate_connection: volume %(vol)s with ' - 'connector %(conn)s.', - {'vol': volume, 'conn': connector}) - - vdisk_name = volume['name'] - self._wait_vdisk_copy_completed(vdisk_name) - self._unmap_vdisk_from_host(vdisk_name, connector) - - properties = {} - conn_wwpns = self._get_conn_fc_wwpns() - properties['target_wwn'] = conn_wwpns - properties['initiator_target_map'] = self._build_initiator_target_map( - connector['wwpns'], conn_wwpns) - - LOG.debug( - 'leave: terminate_connection: volume %(vol)s with ' - 'connector %(conn)s.', {'vol': volume, 'conn': connector}) - - return { - 'driver_volume_type': 'fibre_channel', - 'data': properties - } - def create_snapshot(self, snapshot): """Create snapshot from volume.""" @@ -1311,8 +1025,8 @@ class FlashSystemDriver(san.SanDriver): '%(snap)s.', {'vol': volume['name'], 'snap': snapshot['name']}) if volume['size'] != snapshot['volume_size']: - msg = (_('create_volume_from_snapshot: Volume size is different ' - 'from snapshot based volume.')) + msg = _('create_volume_from_snapshot: Volume size is different ' + 'from snapshot based volume.') LOG.error(msg) raise exception.VolumeDriverException(message=msg) @@ -1339,8 +1053,8 @@ class FlashSystemDriver(san.SanDriver): {'src': src_volume['name'], 'vol': volume['name']}) if src_volume['size'] != volume['size']: - msg = (_('create_cloned_volume: Source and destination ' - 'size differ.')) + msg = _('create_cloned_volume: Source and destination ' + 'size differ.') LOG.error(msg) raise exception.VolumeDriverException(message=msg) diff --git a/cinder/volume/drivers/ibm/flashsystem_fc.py b/cinder/volume/drivers/ibm/flashsystem_fc.py new file mode 100644 index 000000000..ba3ac951d --- /dev/null +++ b/cinder/volume/drivers/ibm/flashsystem_fc.py @@ -0,0 +1,379 @@ +# Copyright 2015 IBM 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. +# + + +""" +Volume driver for IBM FlashSystem storage systems with FC protocol. + +Limitations: +1. Cinder driver only works when open_access_enabled=off. + +""" + +import random +import threading + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import excutils +import six + +from cinder import exception +from cinder.i18n import _, _LE, _LI, _LW +from cinder import utils +import cinder.volume.driver +from cinder.volume.drivers.ibm import flashsystem_common as fscommon +from cinder.volume.drivers.san import san +from cinder.zonemanager import utils as fczm_utils + +LOG = logging.getLogger(__name__) + +flashsystem_fc_opts = [ + cfg.BoolOpt('flashsystem_multipath_enabled', + default=False, + help='Connect with multipath (FC only).' + '(Default is false.)') +] + +CONF = cfg.CONF +CONF.register_opts(flashsystem_fc_opts) + + +class FlashSystemFCDriver(fscommon.FlashSystemDriver, + cinder.volume.driver.FibreChannelDriver): + """IBM FlashSystem FC volume driver. + + Version history: + 1.0.0 - Initial driver + 1.0.1 - Code clean up + 1.0.2 - Add lock into vdisk map/unmap, connection + initialize/terminate + 1.0.3 - Initial driver for iSCSI + 1.0.4 - Split Flashsystem driver into common and FC + + """ + + VERSION = "1.0.4" + + def __init__(self, *args, **kwargs): + super(FlashSystemFCDriver, self).__init__(*args, **kwargs) + self.configuration.append_config_values(fscommon.flashsystem_opts) + self.configuration.append_config_values(flashsystem_fc_opts) + self.configuration.append_config_values(san.san_opts) + + def _check_vdisk_params(self, params): + # Check that the requested protocol is enabled + if params['protocol'] != self._protocol: + msg = (_("Illegal value '%(prot)s' specified for " + "flashsystem_connection_protocol: " + "valid value(s) are %(enabled)s.") + % {'prot': params['protocol'], + 'enabled': self._protocol}) + raise exception.InvalidInput(reason=msg) + + def _create_host(self, connector): + """Create a new host on the storage system. + + We create a host and associate it with the given connection + information. + + """ + + LOG.debug('enter: _create_host: host %s.', connector['host']) + + rand_id = six.text_type(random.randint(0, 99999999)).zfill(8) + host_name = '%s-%s' % (self._connector_to_hostname_prefix(connector), + rand_id) + + ports = [] + if 'FC' == self._protocol and 'wwpns' in connector: + for wwpn in connector['wwpns']: + ports.append('-hbawwpn %s' % wwpn) + + self._driver_assert(ports, + (_('_create_host: No connector ports.'))) + port1 = ports.pop(0) + arg_name, arg_val = port1.split() + ssh_cmd = ['svctask', 'mkhost', '-force', arg_name, arg_val, '-name', + '"%s"' % host_name] + out, err = self._ssh(ssh_cmd) + self._assert_ssh_return('successfully created' in out, + '_create_host', ssh_cmd, out, err) + + for port in ports: + arg_name, arg_val = port.split() + ssh_cmd = ['svctask', 'addhostport', '-force', + arg_name, arg_val, host_name] + out, err = self._ssh(ssh_cmd) + self._assert_ssh_return( + (not out.strip()), + '_create_host', ssh_cmd, out, err) + + LOG.debug( + 'leave: _create_host: host %(host)s - %(host_name)s.', + {'host': connector['host'], 'host_name': host_name}) + + return host_name + + def _find_host_exhaustive(self, connector, hosts): + for host in hosts: + ssh_cmd = ['svcinfo', 'lshost', '-delim', '!', host] + out, err = self._ssh(ssh_cmd) + self._assert_ssh_return( + out.strip(), + '_find_host_exhaustive', ssh_cmd, out, err) + for attr_line in out.split('\n'): + # If '!' not found, return the string and two empty strings + attr_name, foo, attr_val = attr_line.partition('!') + if (attr_name == 'WWPN' and + 'wwpns' in connector and attr_val.lower() in + map(str.lower, map(str, connector['wwpns']))): + return host + return None + + def _get_conn_fc_wwpns(self): + wwpns = [] + + cmd = ['svcinfo', 'lsportfc'] + + generator = self._port_conf_generator(cmd) + header = next(generator, None) + if not header: + return wwpns + + for port_data in generator: + try: + if port_data['status'] == 'active': + wwpns.append(port_data['WWPN']) + except KeyError: + self._handle_keyerror('lsportfc', header) + + return wwpns + + def _get_fc_wwpns(self): + for key in self._storage_nodes: + node = self._storage_nodes[key] + ssh_cmd = ['svcinfo', 'lsnode', '-delim', '!', node['id']] + attributes = self._execute_command_and_parse_attributes(ssh_cmd) + wwpns = set(node['WWPN']) + for i, s in zip(attributes['port_id'], attributes['port_status']): + if 'unconfigured' != s: + wwpns.add(i) + node['WWPN'] = list(wwpns) + LOG.info(_LI('WWPN on node %(node)s: %(wwpn)s.'), + {'node': node['id'], 'wwpn': node['WWPN']}) + + def _get_vdisk_map_properties( + self, connector, lun_id, vdisk_name, vdisk_id, vdisk_params): + """Get the map properties of vdisk.""" + + LOG.debug( + 'enter: _get_vdisk_map_properties: vdisk ' + '%(vdisk_name)s.', {'vdisk_name': vdisk_name}) + + preferred_node = '0' + IO_group = '0' + + # Get preferred node and other nodes in I/O group + preferred_node_entry = None + io_group_nodes = [] + for k, node in self._storage_nodes.items(): + if vdisk_params['protocol'] != node['protocol']: + continue + if node['id'] == preferred_node: + preferred_node_entry = node + if node['IO_group'] == IO_group: + io_group_nodes.append(node) + + if not io_group_nodes: + msg = (_('_get_vdisk_map_properties: No node found in ' + 'I/O group %(gid)s for volume %(vol)s.') + % {'gid': IO_group, 'vol': vdisk_name}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + if not preferred_node_entry and not vdisk_params['multipath']: + # Get 1st node in I/O group + preferred_node_entry = io_group_nodes[0] + LOG.warning(_LW('_get_vdisk_map_properties: Did not find a ' + 'preferred node for vdisk %s.'), vdisk_name) + properties = {} + properties['target_discovered'] = False + properties['target_lun'] = lun_id + properties['volume_id'] = vdisk_id + + type_str = 'fibre_channel' + conn_wwpns = self._get_conn_fc_wwpns() + + if not conn_wwpns: + msg = _('_get_vdisk_map_properties: Could not get FC ' + 'connection information for the host-volume ' + 'connection. Is the host configured properly ' + 'for FC connections?') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + properties['target_wwn'] = conn_wwpns + + if "zvm_fcp" in connector: + properties['zvm_fcp'] = connector['zvm_fcp'] + + properties['initiator_target_map'] = self._build_initiator_target_map( + connector['wwpns'], conn_wwpns) + + LOG.debug( + 'leave: _get_vdisk_map_properties: vdisk ' + '%(vdisk_name)s.', {'vdisk_name': vdisk_name}) + + return {'driver_volume_type': type_str, 'data': properties} + + @fczm_utils.AddFCZone + @utils.synchronized('flashsystem-init-conn', external=True) + def initialize_connection(self, volume, connector): + """Perform work so that an FC connection can be made. + + To be able to create a FC connection from a given host to a + volume, we must: + 1. Translate the given WWNN to a host name + 2. Create new host on the storage system if it does not yet exist + 3. Map the volume to the host if it is not already done + 4. Return the connection information for relevant nodes (in the + proper I/O group) + + """ + + LOG.debug( + 'enter: initialize_connection: volume %(vol)s with ' + 'connector %(conn)s.', {'vol': volume, 'conn': connector}) + + vdisk_name = volume['name'] + vdisk_id = volume['id'] + vdisk_params = self._get_vdisk_params(volume['volume_type_id']) + + # TODO(edwin): might fix it after vdisk copy function is + # ready in FlashSystem thin-provision layer. As this validation + # is to check the vdisk which is in copying, at present in firmware + # level vdisk doesn't allow to map host which it is copy. New + # vdisk clone and snapshot function will cover it. After that the + # _wait_vdisk_copy_completed need some modification. + self._wait_vdisk_copy_completed(vdisk_name) + + self._driver_assert( + self._is_vdisk_defined(vdisk_name), + (_('initialize_connection: vdisk %s is not defined.') + % vdisk_name)) + + lun_id = self._map_vdisk_to_host(vdisk_name, connector) + + properties = {} + try: + properties = self._get_vdisk_map_properties( + connector, lun_id, vdisk_name, vdisk_id, vdisk_params) + except exception.VolumeBackendAPIException: + with excutils.save_and_reraise_exception(): + self.terminate_connection(volume, connector) + LOG.error(_LE('initialize_connection: Failed to collect ' + 'return properties for volume %(vol)s and ' + 'connector %(conn)s.'), + {'vol': volume, 'conn': connector}) + + LOG.debug( + 'leave: initialize_connection:\n volume: %(vol)s\n connector ' + '%(conn)s\n properties: %(prop)s.', + {'vol': volume, + 'conn': connector, + 'prop': properties}) + + return properties + + @fczm_utils.RemoveFCZone + @utils.synchronized('flashsystem-term-conn', external=True) + def terminate_connection(self, volume, connector, **kwargs): + """Cleanup after 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) + """ + LOG.debug( + 'enter: terminate_connection: volume %(vol)s with ' + 'connector %(conn)s.', + {'vol': volume, 'conn': connector}) + + vdisk_name = volume['name'] + self._wait_vdisk_copy_completed(vdisk_name) + self._unmap_vdisk_from_host(vdisk_name, connector) + + properties = {} + conn_wwpns = self._get_conn_fc_wwpns() + properties['target_wwn'] = conn_wwpns + # TODO(edwin): add judgement here. No initiator_target_map within + # properties need if no more I/T connection. Otherwise the FCZone + # manager will remove the zoning between I/T. + properties['initiator_target_map'] = self._build_initiator_target_map( + connector['wwpns'], conn_wwpns) + + LOG.debug( + 'leave: terminate_connection: volume %(vol)s with ' + 'connector %(conn)s.', {'vol': volume, 'conn': connector}) + + return { + 'driver_volume_type': 'fibre_channel', + 'data': properties + } + + def do_setup(self, ctxt): + """Check that we have all configuration details from the storage.""" + + self._context = ctxt + + # Get data of configured node + self._get_node_data() + + # Get the WWPNs of the FlashSystem nodes + self._get_fc_wwpns() + + # For each node, check what connection modes it supports. Delete any + # nodes that do not support any types (may be partially configured). + to_delete = [] + for k, node in self._storage_nodes.items(): + if not node['WWPN']: + to_delete.append(k) + + for delkey in to_delete: + del self._storage_nodes[delkey] + + # Make sure we have at least one node configured + self._driver_assert(self._storage_nodes, + 'do_setup: No configured nodes.') + + self._protocol = node['protocol'] = 'FC' + + # Set for vdisk synchronization + self._vdisk_copy_in_progress = set() + self._vdisk_copy_lock = threading.Lock() + self._check_lock_interval = 5 + + def validate_connector(self, connector): + """Check connector.""" + if 'FC' == self._protocol and 'wwpns' not in connector: + msg = _LE('The connector does not contain the ' + 'required information: wwpns is missing') + LOG.error(msg) + raise exception.InvalidConnectorException(missing='wwpns') diff --git a/cinder/volume/drivers/ibm/flashsystem_iscsi.py b/cinder/volume/drivers/ibm/flashsystem_iscsi.py index e7f3185ae..b378a0812 100644 --- a/cinder/volume/drivers/ibm/flashsystem_iscsi.py +++ b/cinder/volume/drivers/ibm/flashsystem_iscsi.py @@ -33,8 +33,9 @@ import six from cinder import exception from cinder.i18n import _, _LE, _LW +from cinder import utils import cinder.volume.driver -from cinder.volume.drivers.ibm import flashsystem as fscommon +from cinder.volume.drivers.ibm import flashsystem_common as fscommon from cinder.volume.drivers.san import san LOG = logging.getLogger(__name__) @@ -52,14 +53,19 @@ CONF.register_opts(flashsystem_iscsi_opts) class FlashSystemISCSIDriver(fscommon.FlashSystemDriver, cinder.volume.driver.ISCSIDriver): - """IBM FlashSystem iSCSI volume driver + """IBM FlashSystem iSCSI volume driver. Version history: - 1.0.2 - Initial driver for iSCSI + 1.0.0 - Initial driver + 1.0.1 - Code clean up + 1.0.2 - Add lock into vdisk map/unmap, connection + initialize/terminate + 1.0.3 - Initial driver for iSCSI + 1.0.4 - Split Flashsystem driver into common and FC """ - VERSION = "1.0.2" + VERSION = "1.0.4" def __init__(self, *args, **kwargs): super(FlashSystemISCSIDriver, self).__init__(*args, **kwargs) @@ -171,11 +177,11 @@ class FlashSystemISCSIDriver(fscommon.FlashSystemDriver, LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) - if not preferred_node_entry and not vdisk_params['multipath']: + if not preferred_node_entry: # Get 1st node in I/O group preferred_node_entry = io_group_nodes[0] LOG.warning(_LW('_get_vdisk_map_properties: Did not find a ' - 'preferred node for vdisk %s.'), vdisk_name) + 'preferred node for vdisk %s.'), vdisk_name) properties = { 'target_discovered': False, 'target_lun': lun_id, @@ -197,6 +203,7 @@ class FlashSystemISCSIDriver(fscommon.FlashSystemDriver, return {'driver_volume_type': type_str, 'data': properties} + @utils.synchronized('flashsystem-init-conn', external=True) def initialize_connection(self, volume, connector): """Perform work so that an iSCSI connection can be made. @@ -222,7 +229,7 @@ class FlashSystemISCSIDriver(fscommon.FlashSystemDriver, self._driver_assert( self._is_vdisk_defined(vdisk_name), - (_('initialize_connection: vdisk %s is not defined.') + (_('vdisk %s is not defined.') % vdisk_name)) lun_id = self._map_vdisk_to_host(vdisk_name, connector) @@ -247,6 +254,7 @@ class FlashSystemISCSIDriver(fscommon.FlashSystemDriver, return properties + @utils.synchronized('flashsystem-term-conn', external=True) def terminate_connection(self, volume, connector, **kwargs): """Cleanup after connection has been terminated. @@ -350,92 +358,16 @@ class FlashSystemISCSIDriver(fscommon.FlashSystemDriver, LOG.debug('leave: do_setup') - def _get_node_data(self): - """Get and verify node configuration.""" - - LOG.debug('enter: _get_node_data') - - # Get storage system name and id - ssh_cmd = ['svcinfo', 'lssystem', '-delim', '!'] - attributes = self._execute_command_and_parse_attributes(ssh_cmd) - if not attributes or not ('name' in attributes): - msg = _('Could not get system name.') - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - self._system_name = attributes['name'] - self._system_id = attributes['id'] - - # Validate value of open_access_enabled flag, for now only - # support when open_access_enabled is off - if not attributes or not ('open_access_enabled' in attributes) or ( - attributes['open_access_enabled'] != 'off'): - msg = _('open_access_enabled is not off.') - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - - # Validate that the array exists - pool = fscommon.FLASHSYSTEM_VOLPOOL_NAME - ssh_cmd = ['svcinfo', 'lsmdiskgrp', '-bytes', '-delim', '!', pool] - attributes = self._execute_command_and_parse_attributes(ssh_cmd) - if not attributes: - LOG.debug('_get_node_data: lssystem attributes:', attributes) - msg = _('Unable to parse attributes') - LOG.error(msg) - raise exception.InvalidInput(reason=msg) - if not ('status' in attributes) or ( - attributes['status'] == 'offline'): - msg = (_('Array does not exist or is offline. ' - 'Current status of array is %ds.') - % attributes['status']) - LOG.error(msg) - raise exception.InvalidInput(reason=msg) - - # Get the iSCSI names of the FlashSystem nodes - ssh_cmd = ['svcinfo', 'lsnode', '-delim', '!'] - out, err = self._ssh(ssh_cmd) - self._assert_ssh_return( - out.strip(), '_get_config_data', ssh_cmd, out, err) - - nodes = out.strip().splitlines() - self._assert_ssh_return(nodes, '_get_node_data', ssh_cmd, out, err) - header = nodes.pop(0) - for node_line in nodes: - try: - node_data = self._get_hdr_dic(header, node_line, '!') - except exception.VolumeBackendAPIException: - with excutils.save_and_reraise_exception(): - self._log_cli_output_error('_get_node_data', - ssh_cmd, out, err) - try: - node = { - 'id': node_data['id'], - 'name': node_data['name'], - 'IO_group': node_data['IO_group_id'], - 'WWNN': node_data['WWNN'], - 'status': node_data['status'], - 'WWPN': [], - 'protocol': None, - 'iscsi_name': node_data['iscsi_name'], - 'config_node': node_data['config_node'], - 'ipv4': [], - 'ipv6': [], - } - if node['status'] == 'online': - self._storage_nodes[node['id']] = node - except KeyError: - self._handle_keyerror('lsnode', header) - - LOG.debug('leave: _get_iscsi_ip_addrs') - def _build_default_params(self): protocol = self.configuration.flashsystem_connection_protocol if protocol.lower() == 'iscsi': protocol = 'iSCSI' - return {'protocol': protocol, - 'multipath': self.configuration.flashsystem_multipath_enabled, - 'iscsi_ip': self.configuration.iscsi_ip_address, - 'iscsi_port': self.configuration.iscsi_port, - 'iscsi_ported': self.configuration.flashsystem_iscsi_portid} + return { + 'protocol': protocol, + 'iscsi_ip': self.configuration.iscsi_ip_address, + 'iscsi_port': self.configuration.iscsi_port, + 'iscsi_ported': self.configuration.flashsystem_iscsi_portid, + } def validate_connector(self, connector): """Check connector for enabled protocol.""" -- 2.45.2