1185 lines
47 KiB
Python
1185 lines
47 KiB
Python
# Copyright (c) 2016 - 2018 Dell Inc. or its subsidiaries.
|
|
# 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 contextlib
|
|
import copy
|
|
import functools
|
|
import os
|
|
import random
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import excutils
|
|
from oslo_utils import importutils
|
|
|
|
from cinder import exception
|
|
from cinder.i18n import _
|
|
from cinder.objects import fields
|
|
from cinder import utils as cinder_utils
|
|
from cinder.volume.drivers.dell_emc.unity import client
|
|
from cinder.volume.drivers.dell_emc.unity import utils
|
|
from cinder.volume import volume_utils
|
|
|
|
storops = importutils.try_import('storops')
|
|
if storops:
|
|
from storops import exception as storops_ex
|
|
from storops.unity import enums
|
|
else:
|
|
# Set storops_ex to be None for unit test
|
|
storops_ex = None
|
|
enums = None
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
PROTOCOL_FC = 'FC'
|
|
PROTOCOL_ISCSI = 'iSCSI'
|
|
|
|
|
|
class VolumeParams(object):
|
|
def __init__(self, adapter, volume):
|
|
self._adapter = adapter
|
|
self._volume = volume
|
|
|
|
self._volume_id = volume.id
|
|
self._name = volume.name
|
|
self._size = volume.size
|
|
self._description = (volume.display_description
|
|
if volume.display_description
|
|
else volume.display_name)
|
|
self._pool = None
|
|
self._io_limit_policy = None
|
|
self._is_thick = None
|
|
self._is_compressed = None
|
|
self._is_in_cg = None
|
|
self._is_replication_enabled = None
|
|
self._tiering_policy = None
|
|
|
|
@property
|
|
def volume_id(self):
|
|
return self._volume_id
|
|
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
|
|
@name.setter
|
|
def name(self, value):
|
|
self._name = value
|
|
|
|
@property
|
|
def size(self):
|
|
return self._size
|
|
|
|
@size.setter
|
|
def size(self, value):
|
|
self._size = value
|
|
|
|
@property
|
|
def description(self):
|
|
return self._description
|
|
|
|
@description.setter
|
|
def description(self, value):
|
|
self._description = value
|
|
|
|
@property
|
|
def pool(self):
|
|
if self._pool is None:
|
|
self._pool = self._adapter._get_target_pool(self._volume)
|
|
return self._pool
|
|
|
|
@pool.setter
|
|
def pool(self, value):
|
|
self._pool = value
|
|
|
|
@property
|
|
def io_limit_policy(self):
|
|
if self._io_limit_policy is None:
|
|
qos_specs = utils.get_backend_qos_specs(self._volume)
|
|
self._io_limit_policy = self._adapter.client.get_io_limit_policy(
|
|
qos_specs)
|
|
return self._io_limit_policy
|
|
|
|
@io_limit_policy.setter
|
|
def io_limit_policy(self, value):
|
|
self._io_limit_policy = value
|
|
|
|
@property
|
|
def is_thick(self):
|
|
if self._is_thick is None:
|
|
provision = utils.get_extra_spec(self._volume, 'provisioning:type')
|
|
support = utils.get_extra_spec(self._volume,
|
|
'thick_provisioning_support')
|
|
self._is_thick = (provision == 'thick' and support == '<is> True')
|
|
return self._is_thick
|
|
|
|
@property
|
|
def is_compressed(self):
|
|
if self._is_compressed is None:
|
|
provision = utils.get_extra_spec(self._volume, 'provisioning:type')
|
|
compression = utils.get_extra_spec(self._volume,
|
|
'compression_support')
|
|
if provision == 'compressed' and compression == '<is> True':
|
|
self._is_compressed = True
|
|
return self._is_compressed
|
|
|
|
@is_compressed.setter
|
|
def is_compressed(self, value):
|
|
self._is_compressed = value
|
|
|
|
@property
|
|
def is_in_cg(self):
|
|
if self._is_in_cg is None:
|
|
self._is_in_cg = (self._volume.group and
|
|
volume_utils.is_group_a_cg_snapshot_type(
|
|
self._volume.group))
|
|
return self._is_in_cg
|
|
|
|
@property
|
|
def tiering_policy(self):
|
|
tiering_policy_map = {'StartHighThenAuto':
|
|
enums.TieringPolicyEnum.AUTOTIER_HIGH,
|
|
'Auto':
|
|
enums.TieringPolicyEnum.AUTOTIER,
|
|
'HighestAvailable':
|
|
enums.TieringPolicyEnum.HIGHEST,
|
|
'LowestAvailable':
|
|
enums.TieringPolicyEnum.LOWEST}
|
|
if not self._tiering_policy:
|
|
tiering_value = utils.get_extra_spec(self._volume,
|
|
'storagetype:tiering')
|
|
support = utils.get_extra_spec(self._volume,
|
|
'fast_support') == '<is> True'
|
|
|
|
if tiering_value and support:
|
|
self._tiering_policy = tiering_policy_map.get(tiering_value)
|
|
# if no value, unity sets StartHighThenAuto as default
|
|
return self._tiering_policy
|
|
|
|
@property
|
|
def cg_id(self):
|
|
if self.is_in_cg:
|
|
return self._volume.group_id
|
|
return None
|
|
|
|
@property
|
|
def is_replication_enabled(self):
|
|
if self._is_replication_enabled is None:
|
|
value = utils.get_extra_spec(self._volume, 'replication_enabled')
|
|
self._is_replication_enabled = value == '<is> True'
|
|
return self._is_replication_enabled
|
|
|
|
def __eq__(self, other):
|
|
return (self.volume_id == other.volume_id and
|
|
self.name == other.name and
|
|
self.size == other.size and
|
|
self.io_limit_policy == other.io_limit_policy and
|
|
self.is_thick == other.is_thick and
|
|
self.is_compressed == other.is_compressed and
|
|
self.is_in_cg == other.is_in_cg and
|
|
self.cg_id == other.cg_id and
|
|
self.tiering_policy == other.tiering_policy and
|
|
self.is_replication_enabled == other.is_replication_enabled)
|
|
|
|
|
|
class CommonAdapter(object):
|
|
protocol = 'unknown'
|
|
driver_name = 'UnityAbstractDriver'
|
|
driver_volume_type = 'unknown'
|
|
|
|
def __init__(self, version=None):
|
|
self.is_setup = False
|
|
self.version = version
|
|
self.driver = None
|
|
self.config = None
|
|
self.configured_pool_names = None
|
|
self.reserved_percentage = None
|
|
self.max_over_subscription_ratio = None
|
|
self.volume_backend_name = None
|
|
self.ip = None
|
|
self.username = None
|
|
self.password = None
|
|
self.array_cert_verify = None
|
|
self.array_ca_cert_path = None
|
|
|
|
self._serial_number = None
|
|
self.storage_pools_map = None
|
|
self._client = None
|
|
self.allowed_ports = None
|
|
self.remove_empty_host = False
|
|
self.to_lock_host = False
|
|
self.replication_manager = None
|
|
|
|
def do_setup(self, driver, conf):
|
|
"""Sets up the attributes of adapter.
|
|
|
|
:param driver: the unity driver.
|
|
:param conf: the driver configurations.
|
|
"""
|
|
self.driver = driver
|
|
self.config = self.normalize_config(conf)
|
|
self.replication_manager = driver.replication_manager
|
|
self.configured_pool_names = self.config.unity_storage_pool_names
|
|
self.reserved_percentage = self.config.reserved_percentage
|
|
self.max_over_subscription_ratio = (
|
|
self.config.max_over_subscription_ratio)
|
|
self.volume_backend_name = (self.config.safe_get('volume_backend_name')
|
|
or self.driver_name)
|
|
self.ip = self.config.san_ip
|
|
self.username = self.config.san_login
|
|
self.password = self.config.san_password
|
|
# Allow for customized CA
|
|
self.array_cert_verify = self.config.driver_ssl_cert_verify
|
|
self.array_ca_cert_path = self.config.driver_ssl_cert_path
|
|
|
|
sys_version = self.client.system.system_version
|
|
if utils.is_before_4_1(sys_version):
|
|
raise exception.VolumeBackendAPIException(
|
|
data=_('Unity driver does not support array OE version: %s. '
|
|
'Upgrade to 4.1 or later.') % sys_version)
|
|
|
|
self.storage_pools_map = self.get_managed_pools()
|
|
|
|
self.allowed_ports = self.validate_ports(self.config.unity_io_ports)
|
|
|
|
self.remove_empty_host = self.config.remove_empty_host
|
|
self.to_lock_host = self.remove_empty_host
|
|
|
|
group_name = (self.config.config_group if self.config.config_group
|
|
else 'DEFAULT')
|
|
folder_name = '%(group)s.%(sys_name)s' % {
|
|
'group': group_name, 'sys_name': self.client.system.info.name}
|
|
persist_path = os.path.join(cfg.CONF.state_path, 'unity', folder_name)
|
|
storops.TCHelper.set_up(persist_path)
|
|
|
|
self.is_setup = True
|
|
|
|
def normalize_config(self, config):
|
|
config.unity_storage_pool_names = utils.remove_empty(
|
|
'%s.unity_storage_pool_names' % config.config_group,
|
|
config.unity_storage_pool_names)
|
|
|
|
config.unity_io_ports = utils.remove_empty(
|
|
'%s.unity_io_ports' % config.config_group,
|
|
config.unity_io_ports)
|
|
|
|
return config
|
|
|
|
def get_all_ports(self):
|
|
raise NotImplementedError()
|
|
|
|
def validate_ports(self, ports_whitelist):
|
|
all_ports = self.get_all_ports()
|
|
# After normalize_config, `ports_whitelist` could be only None or valid
|
|
# list in which the items are stripped.
|
|
if ports_whitelist is None:
|
|
return all_ports.id
|
|
|
|
# For iSCSI port, the format is 'spa_eth0', and 'spa_iom_0_fc0' for FC.
|
|
# Unix style glob like 'spa_*' is supported.
|
|
whitelist = set(ports_whitelist)
|
|
|
|
matched, _ignored, unmatched_whitelist = utils.match_any(all_ports.id,
|
|
whitelist)
|
|
if not matched:
|
|
LOG.error('No matched ports filtered by all patterns: %s',
|
|
whitelist)
|
|
raise exception.InvalidConfigurationValue(
|
|
option='%s.unity_io_ports' % self.config.config_group,
|
|
value=self.config.unity_io_ports)
|
|
|
|
if unmatched_whitelist:
|
|
LOG.error('No matched ports filtered by below patterns: %s',
|
|
unmatched_whitelist)
|
|
raise exception.InvalidConfigurationValue(
|
|
option='%s.unity_io_ports' % self.config.config_group,
|
|
value=self.config.unity_io_ports)
|
|
|
|
LOG.info('These ports %(matched)s will be used based on '
|
|
'the option unity_io_ports: %(config)s',
|
|
{'matched': matched,
|
|
'config': self.config.unity_io_ports})
|
|
return matched
|
|
|
|
@property
|
|
def verify_cert(self):
|
|
verify_cert = self.array_cert_verify
|
|
if verify_cert and self.array_ca_cert_path is not None:
|
|
verify_cert = self.array_ca_cert_path
|
|
return verify_cert
|
|
|
|
@property
|
|
def client(self):
|
|
if self._client is None:
|
|
self._client = client.UnityClient(
|
|
self.ip,
|
|
self.username,
|
|
self.password,
|
|
verify_cert=self.verify_cert)
|
|
return self._client
|
|
|
|
@property
|
|
def serial_number(self):
|
|
if self._serial_number is None:
|
|
self._serial_number = self.client.get_serial()
|
|
return self._serial_number
|
|
|
|
def get_managed_pools(self):
|
|
names = self.configured_pool_names
|
|
array_pools = self.client.get_pools()
|
|
valid_names = utils.validate_pool_names(names, array_pools.name)
|
|
return {p.name: p for p in array_pools if p.name in valid_names}
|
|
|
|
def makeup_model(self, lun_id, is_snap_lun=False):
|
|
lun_type = 'snap_lun' if is_snap_lun else 'lun'
|
|
location = self._build_provider_location(lun_id=lun_id,
|
|
lun_type=lun_type)
|
|
return {
|
|
'provider_location': location,
|
|
'provider_id': lun_id
|
|
}
|
|
|
|
def setup_replications(self, lun, model_update):
|
|
if not self.replication_manager.is_replication_configured:
|
|
LOG.debug('Replication device not configured, '
|
|
'skip setting up replication for lun %s',
|
|
lun.name)
|
|
return model_update
|
|
|
|
rep_data = {}
|
|
rep_devices = self.replication_manager.replication_devices
|
|
for backend_id, dst in rep_devices.items():
|
|
remote_serial_number = dst.adapter.serial_number
|
|
LOG.debug('Setting up replication to remote system %s',
|
|
remote_serial_number)
|
|
remote_system = self.client.get_remote_system(remote_serial_number)
|
|
if remote_system is None:
|
|
raise exception.VolumeBackendAPIException(
|
|
data=_('Setup replication to remote system %s failed.'
|
|
'Cannot find it.') % remote_serial_number)
|
|
rep_session = self.client.create_replication(
|
|
lun, dst.max_time_out_of_sync,
|
|
dst.destination_pool.get_id(), remote_system)
|
|
rep_data[backend_id] = rep_session.name
|
|
return utils.enable_replication_status(model_update, rep_data)
|
|
|
|
def create_volume(self, volume):
|
|
"""Creates a volume.
|
|
|
|
:param volume: volume information
|
|
"""
|
|
params = VolumeParams(self, volume)
|
|
log_params = {
|
|
'name': params.name,
|
|
'size': params.size,
|
|
'description': params.description,
|
|
'pool': params.pool,
|
|
'io_limit_policy': params.io_limit_policy,
|
|
'is_thick': params.is_thick,
|
|
'is_compressed': params.is_compressed,
|
|
'cg_id': params.cg_id,
|
|
'is_replication_enabled': params.is_replication_enabled,
|
|
'tiering_policy': params.tiering_policy
|
|
}
|
|
|
|
LOG.info('Create Volume: %(name)s, size: %(size)s, description: '
|
|
'%(description)s, pool: %(pool)s, io limit policy: '
|
|
'%(io_limit_policy)s, thick: %(is_thick)s, '
|
|
'compressed: %(is_compressed)s, cg_group: %(cg_id)s, '
|
|
'replication_enabled: %(is_replication_enabled)s.',
|
|
log_params)
|
|
|
|
lun = self.client.create_lun(
|
|
name=params.name,
|
|
size=params.size,
|
|
pool=params.pool,
|
|
description=params.description,
|
|
io_limit_policy=params.io_limit_policy,
|
|
is_thin=False if params.is_thick else None,
|
|
is_compressed=params.is_compressed,
|
|
tiering_policy=params.tiering_policy)
|
|
if params.cg_id:
|
|
LOG.debug('Adding lun %(lun)s to cg %(cg)s.',
|
|
{'lun': lun.get_id(), 'cg': params.cg_id})
|
|
self.client.update_cg(params.cg_id, [lun.get_id()], ())
|
|
|
|
model_update = self.makeup_model(lun.get_id())
|
|
|
|
if params.is_replication_enabled:
|
|
model_update = self.setup_replications(lun, model_update)
|
|
return model_update
|
|
|
|
def delete_volume(self, volume):
|
|
lun_id = self.get_lun_id(volume)
|
|
if lun_id is None:
|
|
LOG.info('Backend LUN not found, skipping the deletion. '
|
|
'Volume: %(volume_name)s.',
|
|
{'volume_name': volume.name})
|
|
else:
|
|
self.client.delete_lun(lun_id)
|
|
|
|
def _create_host_and_attach(self, host_name, lun_or_snap):
|
|
@utils.lock_if(self.to_lock_host, '{lock_name}')
|
|
def _lock_helper(lock_name):
|
|
if not self.to_lock_host:
|
|
host = self.client.create_host(host_name)
|
|
else:
|
|
# Use the lock in the decorator
|
|
host = self.client.create_host_wo_lock(host_name)
|
|
hlu = self.client.attach(host, lun_or_snap)
|
|
return host, hlu
|
|
|
|
return _lock_helper('{unity}-{host}'.format(unity=self.client.host,
|
|
host=host_name))
|
|
|
|
def _initialize_connection(self, lun_or_snap, connector, vol_id):
|
|
host, hlu = self._create_host_and_attach(connector['host'],
|
|
lun_or_snap)
|
|
self.client.update_host_initiators(
|
|
host, self.get_connector_uids(connector))
|
|
data = self.get_connection_info(hlu, host, connector)
|
|
data['target_discovered'] = True
|
|
if vol_id is not None:
|
|
data['volume_id'] = vol_id
|
|
conn_info = {
|
|
'driver_volume_type': self.driver_volume_type,
|
|
'data': data,
|
|
}
|
|
return conn_info
|
|
|
|
@cinder_utils.trace
|
|
def initialize_connection(self, volume, connector):
|
|
lun = self.client.get_lun(lun_id=self.get_lun_id(volume))
|
|
return self._initialize_connection(lun, connector, volume.id)
|
|
|
|
@staticmethod
|
|
def filter_targets_by_host(host):
|
|
# No target info for iSCSI driver
|
|
return []
|
|
|
|
def _detach_and_delete_host(self, host_name, lun_or_snap,
|
|
is_multiattach_to_host=False):
|
|
@utils.lock_if(self.to_lock_host, '{lock_name}')
|
|
def _lock_helper(lock_name):
|
|
# Only get the host from cache here
|
|
host = self.client.create_host_wo_lock(host_name)
|
|
if not is_multiattach_to_host:
|
|
self.client.detach(host, lun_or_snap)
|
|
host.update() # need update to get the latest `host_luns`
|
|
targets = self.filter_targets_by_host(host)
|
|
if self.remove_empty_host and not host.host_luns:
|
|
self.client.delete_host_wo_lock(host)
|
|
return targets
|
|
|
|
return _lock_helper('{unity}-{host}'.format(unity=self.client.host,
|
|
host=host_name))
|
|
|
|
@staticmethod
|
|
def get_terminate_connection_info(connector, targets):
|
|
# No return data from terminate_connection for iSCSI driver
|
|
return {}
|
|
|
|
def _terminate_connection(self, lun_or_snap, connector,
|
|
is_multiattach_to_host=False):
|
|
is_force_detach = connector is None
|
|
data = {}
|
|
if is_force_detach:
|
|
self.client.detach_all(lun_or_snap)
|
|
else:
|
|
targets = self._detach_and_delete_host(
|
|
connector['host'], lun_or_snap,
|
|
is_multiattach_to_host=is_multiattach_to_host)
|
|
data = self.get_terminate_connection_info(connector, targets)
|
|
return {
|
|
'driver_volume_type': self.driver_volume_type,
|
|
'data': data,
|
|
}
|
|
|
|
@cinder_utils.trace
|
|
def terminate_connection(self, volume, connector):
|
|
lun = self.client.get_lun(lun_id=self.get_lun_id(volume))
|
|
# None `connector` indicates force detach, then detach all even the
|
|
# volume is multi-attached.
|
|
multiattach_flag = (connector is not None and
|
|
utils.is_multiattach_to_host(
|
|
volume.volume_attachment,
|
|
connector['host']))
|
|
return self._terminate_connection(
|
|
lun, connector, is_multiattach_to_host=multiattach_flag)
|
|
|
|
def get_connector_uids(self, connector):
|
|
return None
|
|
|
|
def get_connection_info(self, hlu, host, connector):
|
|
return {}
|
|
|
|
def extend_volume(self, volume, new_size):
|
|
lun_id = self.get_lun_id(volume)
|
|
if lun_id is None:
|
|
msg = (_('Backend LUN not found for Volume: %(volume_name)s.') %
|
|
{'volume_name': volume.name})
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
else:
|
|
self.client.extend_lun(lun_id, new_size)
|
|
|
|
def _get_target_pool(self, volume):
|
|
return self.storage_pools_map[utils.get_pool_name(volume)]
|
|
|
|
def _build_provider_location(self, lun_id=None, lun_type=None):
|
|
return utils.build_provider_location(
|
|
system=self.serial_number,
|
|
lun_type=lun_type,
|
|
lun_id=lun_id,
|
|
version=self.version)
|
|
|
|
@utils.append_capabilities
|
|
def update_volume_stats(self):
|
|
return {
|
|
'volume_backend_name': self.volume_backend_name,
|
|
'storage_protocol': self.protocol,
|
|
'pools': self.get_pools_stats(),
|
|
'replication_enabled':
|
|
self.replication_manager.is_replication_configured,
|
|
'replication_targets':
|
|
list(self.replication_manager.replication_devices),
|
|
}
|
|
|
|
def get_pools_stats(self):
|
|
self.storage_pools_map = self.get_managed_pools()
|
|
return [self._get_pool_stats(pool) for pool in self.pools]
|
|
|
|
@property
|
|
def pools(self):
|
|
return self.storage_pools_map.values()
|
|
|
|
@utils.append_capabilities
|
|
def _get_pool_stats(self, pool):
|
|
return {
|
|
'pool_name': pool.name,
|
|
'total_capacity_gb': utils.byte_to_gib(pool.size_total),
|
|
'provisioned_capacity_gb': utils.byte_to_gib(
|
|
pool.size_subscribed),
|
|
'free_capacity_gb': utils.byte_to_gib(pool.size_free),
|
|
'reserved_percentage': self.reserved_percentage,
|
|
'location_info': ('%(pool_name)s|%(array_serial)s' %
|
|
{'pool_name': pool.name,
|
|
'array_serial': self.serial_number}),
|
|
'compression_support': pool.is_all_flash,
|
|
'max_over_subscription_ratio': (
|
|
self.max_over_subscription_ratio),
|
|
'multiattach': True,
|
|
'replication_enabled':
|
|
self.replication_manager.is_replication_configured,
|
|
'replication_targets':
|
|
list(self.replication_manager.replication_devices),
|
|
}
|
|
|
|
def get_lun_id(self, volume):
|
|
"""Retrieves id of the volume's backing LUN.
|
|
|
|
:param volume: volume information
|
|
"""
|
|
if volume.provider_location:
|
|
return utils.extract_provider_location(volume.provider_location,
|
|
'id')
|
|
else:
|
|
# In some cases, cinder will not update volume info in DB with
|
|
# provider_location returned by us. We need to retrieve the id
|
|
# from array.
|
|
lun = self.client.get_lun(name=volume.name)
|
|
return lun.get_id() if lun is not None else None
|
|
|
|
def create_snapshot(self, snapshot):
|
|
"""Creates a snapshot.
|
|
|
|
:param snapshot: snapshot information.
|
|
"""
|
|
src_lun_id = self.get_lun_id(snapshot.volume)
|
|
snap = self.client.create_snap(src_lun_id, snapshot.name)
|
|
location = self._build_provider_location(lun_type='snapshot',
|
|
lun_id=snap.get_id())
|
|
return {'provider_location': location,
|
|
'provider_id': snap.get_id()}
|
|
|
|
def delete_snapshot(self, snapshot):
|
|
"""Deletes a snapshot.
|
|
|
|
:param snapshot: the snapshot to delete.
|
|
"""
|
|
snap = self.client.get_snap(name=snapshot.name)
|
|
self.client.delete_snap(snap)
|
|
|
|
def _get_referenced_lun(self, existing_ref):
|
|
if 'source-id' in existing_ref:
|
|
lun = self.client.get_lun(lun_id=existing_ref['source-id'])
|
|
elif 'source-name' in existing_ref:
|
|
lun = self.client.get_lun(name=existing_ref['source-name'])
|
|
else:
|
|
reason = _('Reference must contain source-id or source-name key.')
|
|
raise exception.ManageExistingInvalidReference(
|
|
existing_ref=existing_ref, reason=reason)
|
|
if lun is None or not lun.existed:
|
|
raise exception.ManageExistingInvalidReference(
|
|
existing_ref=existing_ref,
|
|
reason=_("LUN doesn't exist."))
|
|
return lun
|
|
|
|
def manage_existing(self, volume, existing_ref):
|
|
"""Manages an existing LUN in the array.
|
|
|
|
The LUN should be in a manageable pool backend, otherwise return error.
|
|
Rename the backend storage object so that it matches the
|
|
`volume['name']` which is how drivers traditionally map between a
|
|
cinder volume and the associated backend storage object.
|
|
|
|
LUN ID or name are supported in `existing_ref`, like:
|
|
|
|
.. code-block:: none
|
|
|
|
existing_ref:{
|
|
|
|
'source-id':<LUN id in Unity>
|
|
|
|
}
|
|
|
|
or
|
|
|
|
.. code-block:: none
|
|
|
|
existing_ref:{
|
|
|
|
'source-name':<LUN name in Unity>
|
|
|
|
}
|
|
"""
|
|
lun = self._get_referenced_lun(existing_ref)
|
|
lun.modify(name=volume.name)
|
|
return {
|
|
'provider_location':
|
|
self._build_provider_location(lun_id=lun.get_id(),
|
|
lun_type='lun'),
|
|
'provider_id': lun.get_id()
|
|
}
|
|
|
|
def manage_existing_get_size(self, volume, existing_ref):
|
|
"""Returns size of volume to be managed by `manage_existing`.
|
|
|
|
The driver does some check here:
|
|
1. The LUN `existing_ref` should be managed by the `volume.host`.
|
|
"""
|
|
lun = self._get_referenced_lun(existing_ref)
|
|
target_pool_name = utils.get_pool_name(volume)
|
|
lun_pool_name = lun.pool.name
|
|
if target_pool_name and lun_pool_name != target_pool_name:
|
|
reason = (_('The imported LUN is in pool %(pool_name)s '
|
|
'which is not managed by the host %(host)s.') %
|
|
{'pool_name': lun_pool_name,
|
|
'host': volume.host})
|
|
raise exception.ManageExistingInvalidReference(
|
|
existing_ref=existing_ref, reason=reason)
|
|
|
|
return utils.byte_to_gib(lun.size_total)
|
|
|
|
def _disconnect_device(self, conn):
|
|
conn['connector'].disconnect_volume(conn['conn']['data'],
|
|
conn['device'])
|
|
|
|
def _connect_device(self, conn):
|
|
return self.driver._connect_device(conn)
|
|
|
|
@contextlib.contextmanager
|
|
def _connect_resource(self, lun_or_snap, connector, res_id):
|
|
"""Connects to LUN or snapshot, and makes sure disconnect finally.
|
|
|
|
:param lun_or_snap: the LUN or snapshot to connect/disconnect.
|
|
:param connector: the host connector information.
|
|
:param res_id: the ID of the LUN or snapshot.
|
|
|
|
:return: the connection information, in a dict with format
|
|
like (same as the one returned by `_connect_device`):
|
|
{
|
|
'conn': <info returned by `initialize_connection`>,
|
|
'device': <value returned by `connect_volume`>,
|
|
'connector': <host connector info>
|
|
}
|
|
"""
|
|
init_conn_func = functools.partial(self._initialize_connection,
|
|
lun_or_snap, connector, res_id)
|
|
term_conn_func = functools.partial(self._terminate_connection,
|
|
lun_or_snap, connector)
|
|
with utils.assure_cleanup(init_conn_func, term_conn_func,
|
|
False) as conn_info:
|
|
conn_device_func = functools.partial(self._connect_device,
|
|
conn_info)
|
|
with utils.assure_cleanup(conn_device_func,
|
|
self._disconnect_device,
|
|
True) as attach_info:
|
|
yield attach_info
|
|
|
|
def _dd_copy(self, vol_params, src_snap, src_lun=None):
|
|
"""Creates a volume via copying a Unity snapshot.
|
|
|
|
It attaches the `volume` and `snap`, then use `dd` to copy the
|
|
data from the Unity snapshot to the `volume`.
|
|
"""
|
|
dest_lun = self.client.create_lun(
|
|
name=vol_params.name, size=vol_params.size, pool=vol_params.pool,
|
|
description=vol_params.description,
|
|
io_limit_policy=vol_params.io_limit_policy,
|
|
is_thin=False if vol_params.is_thick else None,
|
|
is_compressed=vol_params.is_compressed)
|
|
src_id = src_snap.get_id()
|
|
try:
|
|
conn_props = cinder_utils.brick_get_connector_properties()
|
|
|
|
with self._connect_resource(dest_lun, conn_props,
|
|
vol_params.volume_id) as dest_info, \
|
|
self._connect_resource(src_snap, conn_props,
|
|
src_id) as src_info:
|
|
if src_lun is None:
|
|
# If size is not specified, need to get the size from LUN
|
|
# of snapshot.
|
|
size_in_m = utils.byte_to_mib(src_snap.size)
|
|
else:
|
|
size_in_m = utils.byte_to_mib(src_lun.size_total)
|
|
volume_utils.copy_volume(
|
|
src_info['device']['path'],
|
|
dest_info['device']['path'],
|
|
size_in_m,
|
|
self.driver.configuration.volume_dd_blocksize,
|
|
sparse=True)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
utils.ignore_exception(self.client.delete_lun,
|
|
dest_lun.get_id())
|
|
LOG.error('Failed to create cloned volume: %(vol_id)s, '
|
|
'from source unity snapshot: %(snap_name)s.',
|
|
{'vol_id': vol_params.volume_id,
|
|
'snap_name': src_snap.name})
|
|
|
|
return dest_lun
|
|
|
|
def _thin_clone(self, vol_params, src_snap, src_lun=None):
|
|
tc_src = src_snap if src_lun is None else src_lun
|
|
try:
|
|
LOG.debug('Try to thin clone from %s.', tc_src.name)
|
|
lun = self.client.thin_clone(
|
|
tc_src, vol_params.name,
|
|
description=vol_params.description,
|
|
io_limit_policy=vol_params.io_limit_policy,
|
|
new_size_gb=vol_params.size)
|
|
except storops_ex.UnityThinCloneLimitExceededError:
|
|
LOG.info('Number of thin clones of base LUN exceeds system '
|
|
'limit, dd-copy a new one and thin clone from it.')
|
|
# Copy via dd if thin clone meets the system limit
|
|
hidden = copy.copy(vol_params)
|
|
hidden.name = 'hidden-%s' % vol_params.name
|
|
hidden.description = 'hidden-%s' % vol_params.description
|
|
copied_lun = self._dd_copy(hidden, src_snap, src_lun=src_lun)
|
|
LOG.debug('Notify storops the dd action of lun: %(src_name)s. And '
|
|
'the newly copied lun is: %(copied)s.',
|
|
{'src_name': tc_src.name, 'copied': copied_lun.name})
|
|
storops.TCHelper.notify(tc_src,
|
|
storops.ThinCloneActionEnum.DD_COPY,
|
|
copied_lun)
|
|
lun = self.client.thin_clone(
|
|
copied_lun, vol_params.name,
|
|
description=vol_params.description,
|
|
io_limit_policy=vol_params.io_limit_policy,
|
|
new_size_gb=vol_params.size)
|
|
except storops_ex.SystemAPINotSupported:
|
|
# Thin clone not support on array version before Merlin
|
|
lun = self._dd_copy(vol_params, src_snap, src_lun=src_lun)
|
|
LOG.debug(
|
|
'Volume copied via dd because array OE is too old to support '
|
|
'thin clone api. source snap: %(src_snap)s, lun: %(src_lun)s.',
|
|
{'src_snap': src_snap.name,
|
|
'src_lun': 'Unknown' if src_lun is None else src_lun.name})
|
|
except storops_ex.UnityThinCloneNotAllowedError:
|
|
# Thin clone not allowed on some resources,
|
|
# like thick luns and their snaps
|
|
lun = self._dd_copy(vol_params, src_snap, src_lun=src_lun)
|
|
LOG.debug(
|
|
'Volume copied via dd because source snap/lun is not allowed '
|
|
'to thin clone, i.e. it is thick. source snap: %(src_snap)s, '
|
|
'lun: %(src_lun)s.',
|
|
{'src_snap': src_snap.name,
|
|
'src_lun': 'Unknown' if src_lun is None else src_lun.name})
|
|
return lun
|
|
|
|
def create_volume_from_snapshot(self, volume, snapshot):
|
|
snap = self.client.get_snap(snapshot.name)
|
|
params = VolumeParams(self, volume)
|
|
lun = self._thin_clone(params, snap)
|
|
model_update = self.makeup_model(lun.get_id(), is_snap_lun=True)
|
|
|
|
if params.is_replication_enabled:
|
|
model_update = self.setup_replications(lun, model_update)
|
|
return model_update
|
|
|
|
def create_cloned_volume(self, volume, src_vref):
|
|
"""Creates cloned volume.
|
|
|
|
1. Take an internal snapshot of source volume, and attach it.
|
|
2. Thin clone from the snapshot to a new volume.
|
|
Note: there are several cases the thin clone will downgrade to `dd`,
|
|
2.1 Source volume is attached (in-use).
|
|
2.2 Array OE version doesn't support thin clone.
|
|
2.3 The current LUN family reaches the thin clone limits.
|
|
3. Delete the internal snapshot created in step 1.
|
|
"""
|
|
|
|
src_lun_id = self.get_lun_id(src_vref)
|
|
if src_lun_id is None:
|
|
raise exception.VolumeBackendAPIException(
|
|
data=_(
|
|
"LUN ID of source volume: %s not found.") % src_vref.name)
|
|
src_lun = self.client.get_lun(lun_id=src_lun_id)
|
|
src_snap_name = 'snap_clone_%s' % volume.id
|
|
|
|
create_snap_func = functools.partial(self.client.create_snap,
|
|
src_lun_id, src_snap_name)
|
|
vol_params = VolumeParams(self, volume)
|
|
with utils.assure_cleanup(create_snap_func,
|
|
self.client.delete_snap,
|
|
True) as src_snap:
|
|
LOG.debug('Internal snapshot for clone is created, '
|
|
'name: %(name)s, id: %(id)s.',
|
|
{'name': src_snap_name,
|
|
'id': src_snap.get_id()})
|
|
if src_vref.volume_attachment:
|
|
lun = self._dd_copy(vol_params, src_snap, src_lun=src_lun)
|
|
LOG.debug('Volume copied using dd because source volume: '
|
|
'%(name)s is attached: %(attach)s.',
|
|
{'name': src_vref.name,
|
|
'attach': src_vref.volume_attachment})
|
|
model_update = self.makeup_model(lun.get_id())
|
|
else:
|
|
lun = self._thin_clone(vol_params, src_snap, src_lun=src_lun)
|
|
model_update = self.makeup_model(lun.get_id(),
|
|
is_snap_lun=True)
|
|
|
|
if vol_params.is_replication_enabled:
|
|
model_update = self.setup_replications(lun, model_update)
|
|
return model_update
|
|
|
|
def get_pool_name(self, volume):
|
|
return self.client.get_pool_name(volume.name)
|
|
|
|
def get_pool_id_by_name(self, name):
|
|
return self.client.get_pool_id_by_name(name=name)
|
|
|
|
@cinder_utils.trace
|
|
def initialize_connection_snapshot(self, snapshot, connector):
|
|
snap = self.client.get_snap(snapshot.name)
|
|
return self._initialize_connection(snap, connector, snapshot.id)
|
|
|
|
@cinder_utils.trace
|
|
def terminate_connection_snapshot(self, snapshot, connector):
|
|
snap = self.client.get_snap(snapshot.name)
|
|
return self._terminate_connection(snap, connector)
|
|
|
|
@cinder_utils.trace
|
|
def restore_snapshot(self, volume, snapshot):
|
|
return self.client.restore_snapshot(snapshot.name)
|
|
|
|
def migrate_volume(self, volume, host):
|
|
"""Leverage the Unity move session functionality.
|
|
|
|
This method is invoked at the source backend.
|
|
"""
|
|
log_params = {
|
|
'name': volume.name,
|
|
'src_host': volume.host,
|
|
'dest_host': host['host']
|
|
}
|
|
LOG.info('Migrate Volume: %(name)s, host: %(src_host)s, destination: '
|
|
'%(dest_host)s', log_params)
|
|
|
|
src_backend = utils.get_backend_name_from_volume(volume)
|
|
dest_backend = utils.get_backend_name_from_host(host)
|
|
|
|
if src_backend != dest_backend:
|
|
LOG.debug('Cross-backends migration not supported by Unity '
|
|
'driver. Falling back to host-assisted migration.')
|
|
return False, None
|
|
|
|
lun_id = self.get_lun_id(volume)
|
|
dest_pool_name = utils.get_pool_name_from_host(host)
|
|
dest_pool_id = self.get_pool_id_by_name(dest_pool_name)
|
|
|
|
if self.client.migrate_lun(lun_id, dest_pool_id):
|
|
LOG.debug('Volume migrated successfully.')
|
|
model_update = {}
|
|
return True, model_update
|
|
|
|
LOG.debug('Volume migrated failed. Falling back to '
|
|
'host-assisted migration.')
|
|
return False, None
|
|
|
|
def create_group(self, group):
|
|
"""Creates a generic group.
|
|
|
|
:param group: group information
|
|
"""
|
|
cg_name = group.id
|
|
description = group.description if group.description else group.name
|
|
|
|
LOG.info('Create group: %(name)s, description: %(description)s',
|
|
{'name': cg_name, 'description': description})
|
|
|
|
self.client.create_cg(cg_name, description=description)
|
|
return {'status': fields.GroupStatus.AVAILABLE}
|
|
|
|
def delete_group(self, group):
|
|
"""Deletes the generic group.
|
|
|
|
:param group: the group to delete
|
|
"""
|
|
|
|
# Deleting cg will also delete all the luns in it.
|
|
self.client.delete_cg(group.id)
|
|
return None, None
|
|
|
|
def update_group(self, group, add_volumes, remove_volumes):
|
|
add_lun_ids = (set(map(self.get_lun_id, add_volumes)) if add_volumes
|
|
else set())
|
|
remove_lun_ids = (set(map(self.get_lun_id, remove_volumes))
|
|
if remove_volumes else set())
|
|
self.client.update_cg(group.id, add_lun_ids, remove_lun_ids)
|
|
return {'status': fields.GroupStatus.AVAILABLE}, None, None
|
|
|
|
def copy_luns_in_group(self, group, volumes, src_cg_snap, src_volumes):
|
|
# Use dd to copy data here. The reason why not using thinclone is:
|
|
# 1. Cannot use cg thinclone due to the tight couple between source
|
|
# group and cloned one.
|
|
# 2. Cannot use lun thinclone due to clone lun in cg is not supported.
|
|
|
|
lun_snaps = self.client.filter_snaps_in_cg_snap(src_cg_snap.id)
|
|
|
|
# Make sure the `lun_snaps` is as order of `src_volumes`
|
|
src_lun_ids = [self.get_lun_id(volume) for volume in src_volumes]
|
|
lun_snaps.sort(key=lambda snap: src_lun_ids.index(snap.lun.id))
|
|
|
|
dest_luns = [self._dd_copy(VolumeParams(self, dest_volume), lun_snap)
|
|
for dest_volume, lun_snap in zip(volumes, lun_snaps)]
|
|
|
|
self.client.create_cg(group.id, lun_add=dest_luns)
|
|
return ({'status': fields.GroupStatus.AVAILABLE},
|
|
[{'id': dest_volume.id, 'status': fields.GroupStatus.AVAILABLE}
|
|
for dest_volume in volumes])
|
|
|
|
def create_group_from_snap(self, group, volumes,
|
|
group_snapshot, snapshots):
|
|
src_cg_snap = self.client.get_snap(group_snapshot.id)
|
|
src_vols = ([snap.volume for snap in snapshots] if snapshots else [])
|
|
return self.copy_luns_in_group(group, volumes, src_cg_snap, src_vols)
|
|
|
|
def create_cloned_group(self, group, volumes, source_group, source_vols):
|
|
src_group_snap_name = 'snap_clone_group_{}'.format(source_group.id)
|
|
create_snap_func = functools.partial(self.client.create_cg_snap,
|
|
source_group.id,
|
|
src_group_snap_name)
|
|
with utils.assure_cleanup(create_snap_func,
|
|
self.client.delete_snap,
|
|
True) as src_cg_snap:
|
|
LOG.debug('Internal group snapshot for clone is created, '
|
|
'name: %(name)s, id: %(id)s.',
|
|
{'name': src_group_snap_name,
|
|
'id': src_cg_snap.get_id()})
|
|
source_vols = source_vols if source_vols else []
|
|
return self.copy_luns_in_group(group, volumes, src_cg_snap,
|
|
source_vols)
|
|
|
|
def create_group_snapshot(self, group_snapshot, snapshots):
|
|
self.client.create_cg_snap(group_snapshot.group_id,
|
|
snap_name=group_snapshot.id)
|
|
|
|
model_update = {'status': fields.GroupStatus.AVAILABLE}
|
|
snapshots_model_update = [{'id': snapshot.id,
|
|
'status': fields.SnapshotStatus.AVAILABLE}
|
|
for snapshot in snapshots]
|
|
return model_update, snapshots_model_update
|
|
|
|
def delete_group_snapshot(self, group_snapshot):
|
|
cg_snap = self.client.get_snap(group_snapshot.id)
|
|
self.client.delete_snap(cg_snap)
|
|
return None, None
|
|
|
|
@cinder_utils.trace
|
|
def failover(self, volumes, secondary_id=None, groups=None):
|
|
# TODO(ryan) support group failover after group bp merges
|
|
# https://review.opendev.org/#/c/574119/
|
|
|
|
if secondary_id is None:
|
|
LOG.debug('No secondary specified when failover. '
|
|
'Randomly choose a secondary')
|
|
secondary_id = random.choice(
|
|
list(self.replication_manager.replication_devices))
|
|
LOG.debug('Chose %s as secondary', secondary_id)
|
|
|
|
is_failback = secondary_id == 'default'
|
|
|
|
def _failover_or_back(volume):
|
|
LOG.debug('Failing over volume: %(vol)s to secondary id: '
|
|
'%(sec_id)s', vol=volume.name, sec_id=secondary_id)
|
|
model_update = {
|
|
'volume_id': volume.id,
|
|
'updates': {}
|
|
}
|
|
|
|
if not volume.replication_driver_data:
|
|
LOG.error('Empty replication_driver_data of volume: %s, '
|
|
'replication session name should be in it.',
|
|
volume.name)
|
|
return utils.error_replication_status(model_update)
|
|
rep_data = utils.load_replication_data(
|
|
volume.replication_driver_data)
|
|
|
|
if is_failback:
|
|
# Failback executed on secondary backend which is currently
|
|
# active.
|
|
_adapter = self.replication_manager.default_device.adapter
|
|
_client = self.replication_manager.active_adapter.client
|
|
rep_name = rep_data[self.replication_manager.active_backend_id]
|
|
else:
|
|
# Failover executed on secondary backend because primary could
|
|
# die.
|
|
_adapter = self.replication_manager.replication_devices[
|
|
secondary_id].adapter
|
|
_client = _adapter.client
|
|
rep_name = rep_data[secondary_id]
|
|
|
|
try:
|
|
rep_session = _client.get_replication_session(name=rep_name)
|
|
|
|
if is_failback:
|
|
_client.failback_replication(rep_session)
|
|
new_model = _adapter.makeup_model(
|
|
rep_session.src_resource_id)
|
|
else:
|
|
_client.failover_replication(rep_session)
|
|
new_model = _adapter.makeup_model(
|
|
rep_session.dst_resource_id)
|
|
|
|
model_update['updates'].update(new_model)
|
|
self.replication_manager.failover_service(secondary_id)
|
|
return model_update
|
|
except client.ClientReplicationError as ex:
|
|
LOG.error('Failover failed, volume: %(vol)s, secondary id: '
|
|
'%(sec_id)s, error: %(err)s',
|
|
vol=volume.name, sec_id=secondary_id, err=ex)
|
|
return utils.error_replication_status(model_update)
|
|
|
|
return (secondary_id,
|
|
[_failover_or_back(volume) for volume in volumes],
|
|
[])
|
|
|
|
|
|
class ISCSIAdapter(CommonAdapter):
|
|
protocol = PROTOCOL_ISCSI
|
|
driver_name = 'UnityISCSIDriver'
|
|
driver_volume_type = 'iscsi'
|
|
|
|
def get_all_ports(self):
|
|
return self.client.get_ethernet_ports()
|
|
|
|
def get_connector_uids(self, connector):
|
|
return utils.extract_iscsi_uids(connector)
|
|
|
|
def get_connection_info(self, hlu, host, connector):
|
|
targets = self.client.get_iscsi_target_info(self.allowed_ports)
|
|
if not targets:
|
|
msg = _("There is no accessible iSCSI targets on the system.")
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
one_target = random.choice(targets)
|
|
portals = [a['portal'] for a in targets]
|
|
iqns = [a['iqn'] for a in targets]
|
|
data = {
|
|
'target_luns': [hlu] * len(portals),
|
|
'target_iqns': iqns,
|
|
'target_portals': portals,
|
|
'target_lun': hlu,
|
|
'target_portal': one_target['portal'],
|
|
'target_iqn': one_target['iqn'],
|
|
}
|
|
return data
|
|
|
|
|
|
class FCAdapter(CommonAdapter):
|
|
protocol = PROTOCOL_FC
|
|
driver_name = 'UnityFCDriver'
|
|
driver_volume_type = 'fibre_channel'
|
|
|
|
def __init__(self, version=None):
|
|
super(FCAdapter, self).__init__(version=version)
|
|
self.lookup_service = None
|
|
|
|
def do_setup(self, driver, config):
|
|
super(FCAdapter, self).do_setup(driver, config)
|
|
self.lookup_service = utils.create_lookup_service()
|
|
|
|
def get_all_ports(self):
|
|
return self.client.get_fc_ports()
|
|
|
|
def get_connector_uids(self, connector):
|
|
return utils.extract_fc_uids(connector)
|
|
|
|
@property
|
|
def auto_zone_enabled(self):
|
|
return self.lookup_service is not None
|
|
|
|
def get_connection_info(self, hlu, host, connector):
|
|
targets = self.client.get_fc_target_info(
|
|
host, logged_in_only=(not self.auto_zone_enabled),
|
|
allowed_ports=self.allowed_ports)
|
|
|
|
if not targets:
|
|
msg = _("There is no accessible fibre channel targets on the "
|
|
"system.")
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
if self.auto_zone_enabled:
|
|
data = self._get_fc_zone_info(connector['wwpns'], targets)
|
|
else:
|
|
data = {
|
|
'target_wwn': targets,
|
|
}
|
|
data['target_lun'] = hlu
|
|
return data
|
|
|
|
def filter_targets_by_host(self, host):
|
|
if self.auto_zone_enabled and not host.host_luns:
|
|
return self.client.get_fc_target_info(
|
|
host=host, logged_in_only=False,
|
|
allowed_ports=self.allowed_ports)
|
|
return []
|
|
|
|
def get_terminate_connection_info(self, connector, targets):
|
|
# For FC, terminate_connection needs to return data to zone manager
|
|
# which would clean the zone based on the data.
|
|
if targets:
|
|
return self._get_fc_zone_info(connector['wwpns'], targets)
|
|
return {}
|
|
|
|
def _get_fc_zone_info(self, initiator_wwns, target_wwns):
|
|
mapping = self.lookup_service.get_device_mapping_from_network(
|
|
initiator_wwns, target_wwns)
|
|
targets, itor_tgt_map = utils.convert_to_itor_tgt_map(mapping)
|
|
return {
|
|
'target_wwn': targets,
|
|
'initiator_target_map': itor_tgt_map,
|
|
}
|