cinder/cinder/volume/drivers/ibm/flashsystem_fc.py

401 lines
15 KiB
Python

# 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 _
from cinder import interface
from cinder import utils
from cinder.volume import configuration
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='This option no longer has any affect. It is deprecated '
'and will be removed in the next release.',
deprecated_for_removal=True)
]
CONF = cfg.CONF
CONF.register_opts(flashsystem_fc_opts, group=configuration.SHARED_CONF_GROUP)
@interface.volumedriver
class FlashSystemFCDriver(fscommon.FlashSystemDriver):
"""IBM FlashSystem FC volume driver.
Version history:
.. code-block:: none
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
1.0.5 - Report capability of volume multiattach
1.0.6 - Fix bug #1469581, add I/T mapping check in
terminate_connection
1.0.7 - Fix bug #1505477, add host name check in
_find_host_exhaustive for FC
1.0.8 - Fix bug #1572743, multi-attach attribute
should not be hardcoded, only in iSCSI
1.0.9 - Fix bug #1570574, Cleanup host resource
leaking, changes only in iSCSI
1.0.10 - Fix bug #1585085, add host name check in
_find_host_exhaustive for iSCSI
1.0.11 - Update driver to use ABC metaclasses
1.0.12 - Update driver to support Manage/Unmanage
existing volume
"""
VERSION = "1.0.12"
# ThirdPartySystems wiki page
CI_WIKI_NAME = "IBM_STORAGE_CI"
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):
hname = connector['host']
hnames = [ihost[0:ihost.rfind('-')] for ihost in hosts]
if hname in hnames:
host = hosts[hnames.index(hname)]
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)
attr_lines = [attr_line for attr_line in out.split('\n')]
attr_parm = {}
for attr_line in attr_lines:
attr_name, foo, attr_val = attr_line.partition('!')
attr_parm[attr_name] = attr_val
if ('WWPN' in attr_parm.keys() and 'wwpns' in connector and
attr_parm['WWPN'].lower() in
map(str.lower, map(str, connector['wwpns']))):
return host
else:
LOG.warning('Host %(host)s was not found on backend storage.',
{'host': hname})
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('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})
IO_group = '0'
io_group_nodes = []
for k, node in self._storage_nodes.items():
if vdisk_params['protocol'] != node['protocol']:
continue
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)
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}
@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('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})
fczm_utils.add_fc_zone(properties)
return properties
@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})
return_data = {
'driver_volume_type': 'fibre_channel',
'data': {},
}
vdisk_name = volume['name']
self._wait_vdisk_copy_completed(vdisk_name)
self._unmap_vdisk_from_host(vdisk_name, connector)
host_name = self._get_host_from_connector(connector)
if not host_name:
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))
return_data['data'] = properties
fczm_utils.remove_fc_zone(return_data)
LOG.debug(
'leave: terminate_connection: volume %(vol)s with '
'connector %(conn)s.', {'vol': volume, 'conn': connector})
return return_data
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:
LOG.error('The connector does not contain the '
'required information: wwpns is missing')
raise exception.InvalidConnectorException(missing='wwpns')