383 lines
13 KiB
Python
383 lines
13 KiB
Python
# Copyright (c) 2016 EMC Corporation.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import six
|
|
import time
|
|
|
|
from oslo_log import log as logging
|
|
from oslo_service import loopingcall
|
|
from oslo_utils import excutils
|
|
from oslo_utils import importutils
|
|
from oslo_utils import uuidutils
|
|
|
|
from cinder.i18n import _
|
|
from cinder.volume.drivers.dell_emc.vnx import common
|
|
from cinder.volume.drivers.san.san import san_opts
|
|
from cinder.volume import utils as vol_utils
|
|
|
|
storops = importutils.try_import('storops')
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def init_ops(configuration):
|
|
configuration.append_config_values(common.VNX_OPTS)
|
|
configuration.append_config_values(san_opts)
|
|
|
|
|
|
def get_metadata(volume):
|
|
# Since versionedobjects is partially merged, metadata
|
|
# may come from 'volume_metadata' or 'metadata', here
|
|
# we need to take care both of them.
|
|
volume_metadata = {}
|
|
if 'volume_metadata' in volume:
|
|
for metadata in volume['volume_metadata']:
|
|
volume_metadata[metadata['key']] = metadata['value']
|
|
return volume_metadata
|
|
return volume['metadata'] if 'metadata' in volume else {}
|
|
|
|
|
|
def dump_provider_location(location_dict):
|
|
return '|'.join([k + '^' + v for k, v in location_dict.items()])
|
|
|
|
|
|
def build_provider_location(system, lun_type, lun_id, base_lun_name, version):
|
|
"""Builds provider_location for volume or snapshot.
|
|
|
|
:param system: VNX serial number
|
|
:param lun_id: LUN ID in VNX
|
|
:param lun_type: 'lun' or 'smp'
|
|
:param base_lun_name: primary LUN name,
|
|
it will be used when creating snap lun
|
|
:param version: driver version
|
|
"""
|
|
location_dict = {'system': system,
|
|
'type': lun_type,
|
|
'id': six.text_type(lun_id),
|
|
'base_lun_name': six.text_type(base_lun_name),
|
|
'version': version}
|
|
return dump_provider_location(location_dict)
|
|
|
|
|
|
def extract_provider_location(provider_location, key):
|
|
"""Extracts value of the specified field from provider_location string.
|
|
|
|
:param provider_location: provider_location string
|
|
:param key: field name of the value that to be extracted
|
|
:return: value of the specified field if it exists, otherwise,
|
|
None is returned
|
|
"""
|
|
if not provider_location:
|
|
return None
|
|
|
|
kvps = provider_location.split('|')
|
|
for kvp in kvps:
|
|
fields = kvp.split('^')
|
|
if len(fields) == 2 and fields[0] == key:
|
|
return fields[1]
|
|
|
|
|
|
def update_provider_location(provider_location, items):
|
|
"""Updates provider_location with new dict items.
|
|
|
|
:param provider_location: volume's provider_location.
|
|
:param items: dict items for updating.
|
|
"""
|
|
location_dict = {tp.split('^')[0]: tp.split('^')[1]
|
|
for tp in provider_location.split('|')}
|
|
for key, value in items.items():
|
|
location_dict[key] = value
|
|
return dump_provider_location(location_dict)
|
|
|
|
|
|
def get_pool_from_host(host):
|
|
return vol_utils.extract_host(host, 'pool')
|
|
|
|
|
|
def wait_until(condition, timeout=None, interval=common.INTERVAL_5_SEC,
|
|
reraise_arbiter=lambda ex: True, *args, **kwargs):
|
|
start_time = time.time()
|
|
if not timeout:
|
|
timeout = common.DEFAULT_TIMEOUT
|
|
|
|
def _inner():
|
|
try:
|
|
test_value = condition(*args, **kwargs)
|
|
except Exception as ex:
|
|
test_value = False
|
|
with excutils.save_and_reraise_exception(
|
|
reraise=reraise_arbiter(ex)):
|
|
LOG.debug('Exception raised when executing %(condition_name)s'
|
|
'in wait_until. Message: %(msg)s',
|
|
{'condition_name': condition.__name__,
|
|
'msg': ex.message})
|
|
if test_value:
|
|
raise loopingcall.LoopingCallDone()
|
|
|
|
if int(time.time()) - start_time > timeout:
|
|
msg = (_('Timeout waiting for %(condition_name)s in wait_until.')
|
|
% {'condition_name': condition.__name__})
|
|
LOG.error(msg)
|
|
raise common.WaitUtilTimeoutException(msg)
|
|
|
|
timer = loopingcall.FixedIntervalLoopingCall(_inner)
|
|
timer.start(interval=interval).wait()
|
|
|
|
|
|
def validate_storage_migration(volume, target_host, src_serial, src_protocol):
|
|
if 'location_info' not in target_host['capabilities']:
|
|
LOG.warning("Failed to get pool name and "
|
|
"serial number. 'location_info' "
|
|
"from %s.", target_host['host'])
|
|
return False
|
|
info = target_host['capabilities']['location_info']
|
|
LOG.debug("Host for migration is %s.", info)
|
|
try:
|
|
serial_number = info.split('|')[1]
|
|
except AttributeError:
|
|
LOG.warning('Error on getting serial number '
|
|
'from %s.', target_host['host'])
|
|
return False
|
|
if serial_number != src_serial:
|
|
LOG.debug('Skip storage-assisted migration because '
|
|
'target and source backend are not managing'
|
|
'the same array.')
|
|
return False
|
|
if (target_host['capabilities']['storage_protocol'] != src_protocol
|
|
and get_original_status(volume) == 'in-use'):
|
|
LOG.debug('Skip storage-assisted migration because '
|
|
'in-use volume can not be '
|
|
'migrate between different protocols.')
|
|
return False
|
|
return True
|
|
|
|
|
|
def retype_need_migration(volume, old_provision, new_provision, host):
|
|
if volume['host'] != host['host']:
|
|
return True
|
|
|
|
lun_type = extract_provider_location(volume['provider_location'], 'type')
|
|
if lun_type == 'smp':
|
|
return True
|
|
|
|
if old_provision != new_provision:
|
|
if retype_need_turn_on_compression(old_provision, new_provision):
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def retype_need_turn_on_compression(old_provision, new_provision):
|
|
return (old_provision in [storops.VNXProvisionEnum.THIN,
|
|
storops.VNXProvisionEnum.THICK]
|
|
and new_provision == storops.VNXProvisionEnum.COMPRESSED)
|
|
|
|
|
|
def retype_need_change_tier(old_tier, new_tier):
|
|
return new_tier is not None and old_tier != new_tier
|
|
|
|
|
|
def get_original_status(volume):
|
|
if not volume['volume_attachment']:
|
|
return 'available'
|
|
else:
|
|
return 'in-use'
|
|
|
|
|
|
def construct_snap_name(volume):
|
|
"""Return snapshot name."""
|
|
if is_snapcopy_enabled(volume):
|
|
return 'snap-as-vol-' + six.text_type(volume.name_id)
|
|
else:
|
|
return 'tmp-snap-' + six.text_type(volume.name_id)
|
|
|
|
|
|
def construct_mirror_name(volume):
|
|
"""Constructs MirrorView name for volume."""
|
|
return 'mirror_' + six.text_type(volume.id)
|
|
|
|
|
|
def construct_tmp_cg_snap_name(cg_name):
|
|
"""Return CG snapshot name."""
|
|
return 'tmp-snap-' + six.text_type(cg_name)
|
|
|
|
|
|
def construct_tmp_lun_name(lun_name):
|
|
"""Constructs a time-based temporary LUN name."""
|
|
return '%(src)s-%(ts)s' % {'src': lun_name,
|
|
'ts': int(time.time())}
|
|
|
|
|
|
def construct_smp_name(snap_id):
|
|
return 'tmp-smp-' + six.text_type(snap_id)
|
|
|
|
|
|
def is_snapcopy_enabled(volume):
|
|
meta = get_metadata(volume)
|
|
return 'snapcopy' in meta and meta['snapcopy'].lower() == 'true'
|
|
|
|
|
|
def is_async_migrate_enabled(volume):
|
|
extra_specs = common.ExtraSpecs.from_volume(volume)
|
|
if extra_specs.is_replication_enabled:
|
|
# For replication-enabled volume, we should not use the async-cloned
|
|
# volume, or setup replication would fail with
|
|
# VNXMirrorLunNotAvailableError
|
|
return False
|
|
meta = get_metadata(volume)
|
|
if 'async_migrate' not in meta:
|
|
# Asynchronous migration is the default behavior now
|
|
return True
|
|
return 'async_migrate' in meta and meta['async_migrate'].lower() == 'true'
|
|
|
|
|
|
def get_migration_rate(volume):
|
|
metadata = get_metadata(volume)
|
|
rate = metadata.get('migrate_rate', None)
|
|
if rate:
|
|
if rate.lower() in storops.VNXMigrationRate.values():
|
|
return storops.VNXMigrationRate.parse(rate.lower())
|
|
else:
|
|
LOG.warning('Unknown migration rate specified, '
|
|
'using [high] as migration rate.')
|
|
|
|
return storops.VNXMigrationRate.HIGH
|
|
|
|
|
|
def update_res_without_poll(res):
|
|
with res.with_no_poll():
|
|
res.update()
|
|
|
|
|
|
def update_res_with_poll(res):
|
|
with res.with_poll():
|
|
res.update()
|
|
|
|
|
|
def get_base_lun_name(volume):
|
|
"""Returns base LUN name for LUN/snapcopy LUN."""
|
|
base_name = extract_provider_location(
|
|
volume.provider_location, 'base_lun_name')
|
|
if base_name is None or base_name == 'None':
|
|
return volume.name
|
|
return base_name
|
|
|
|
|
|
def sift_port_white_list(port_white_list, registered_io_ports):
|
|
"""Filters out the unregistered ports.
|
|
|
|
Goes through the `port_white_list`, and filters out the ones not
|
|
registered (that is not in `registered_io_ports`).
|
|
"""
|
|
valid_port_list = []
|
|
LOG.debug('Filter ports in [%(white)s}] but not in [%(reg_ports)s].',
|
|
{'white': ','.join(
|
|
[port.display_name for port in port_white_list]),
|
|
'reg_ports': ','.join(
|
|
[port.display_name for port in registered_io_ports])})
|
|
for io_port in port_white_list:
|
|
if io_port not in registered_io_ports:
|
|
LOG.debug('Skipped SP port %(port)s due to it is not registered. '
|
|
'The registered IO ports: %(reg_ports)s.',
|
|
{'port': io_port, 'reg_ports': registered_io_ports})
|
|
else:
|
|
valid_port_list.append(io_port)
|
|
|
|
return valid_port_list
|
|
|
|
|
|
def convert_to_tgt_list_and_itor_tgt_map(zone_mapping):
|
|
"""Function to process data from lookup service.
|
|
|
|
:param zone_mapping: mapping is the data from the zone lookup service
|
|
with below format
|
|
{
|
|
<San name>: {
|
|
'initiator_port_wwn_list':
|
|
('200000051e55a100', '200000051e55a121'..)
|
|
'target_port_wwn_list':
|
|
('100000051e55a100', '100000051e55a121'..)
|
|
}
|
|
}
|
|
"""
|
|
target_wwns = []
|
|
itor_tgt_map = {}
|
|
for san_name in zone_mapping:
|
|
one_map = zone_mapping[san_name]
|
|
for target in one_map['target_port_wwn_list']:
|
|
if target not in target_wwns:
|
|
target_wwns.append(target)
|
|
for initiator in one_map['initiator_port_wwn_list']:
|
|
itor_tgt_map[initiator] = one_map['target_port_wwn_list']
|
|
LOG.debug("target_wwns: %(tgt_wwns)s\n init_targ_map: %(itor_tgt_map)s",
|
|
{'tgt_wwns': target_wwns,
|
|
'itor_tgt_map': itor_tgt_map})
|
|
return target_wwns, itor_tgt_map
|
|
|
|
|
|
def truncate_fc_port_wwn(wwn):
|
|
return wwn.replace(':', '')[16:]
|
|
|
|
|
|
def is_volume_smp(volume):
|
|
return 'smp' == extract_provider_location(volume.provider_location, 'type')
|
|
|
|
|
|
def require_consistent_group_snapshot_enabled(func):
|
|
@six.wraps(func)
|
|
def inner(self, *args, **kwargs):
|
|
if not vol_utils.is_group_a_cg_snapshot_type(args[1]):
|
|
raise NotImplementedError
|
|
return func(self, *args, **kwargs)
|
|
return inner
|
|
|
|
|
|
def get_remote_pool(config, volume):
|
|
"""Select remote pool name for replication.
|
|
|
|
Prefer configured remote pool name, or same pool name
|
|
as the source volume.
|
|
"""
|
|
pool_name = get_pool_from_host(volume.host)
|
|
rep_list = common.ReplicationDeviceList(config)
|
|
remote_pool_name = rep_list[0].pool_name
|
|
return remote_pool_name if remote_pool_name else pool_name
|
|
|
|
|
|
def is_image_cache_volume(volume):
|
|
display_name = volume.display_name
|
|
if (display_name.startswith('image-')
|
|
and uuidutils.is_uuid_like(display_name[6:])):
|
|
LOG.debug('Volume: %s is for image cache. Use sync migration and '
|
|
'thin provisioning.', volume.name)
|
|
return True
|
|
return False
|
|
|
|
|
|
def calc_migrate_and_provision(volume):
|
|
"""Returns a tuple of async migrate and provision type.
|
|
|
|
The first element is the flag whether to enable async migrate,
|
|
the second is the provision type (thin or thick).
|
|
"""
|
|
if is_image_cache_volume(volume):
|
|
return False, storops.VNXProvisionEnum.THIN
|
|
else:
|
|
specs = common.ExtraSpecs.from_volume(volume)
|
|
return is_async_migrate_enabled(volume), specs.provision
|