2442 lines
104 KiB
Python
2442 lines
104 KiB
Python
# Copyright (c) 2012 - 2014 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 os.path
|
|
|
|
from oslo_config import cfg
|
|
import six
|
|
|
|
from cinder import exception
|
|
from cinder.i18n import _, _LE, _LI, _LW
|
|
from cinder.openstack.common import log as logging
|
|
from cinder.volume.drivers.emc import emc_vmax_fast
|
|
from cinder.volume.drivers.emc import emc_vmax_masking
|
|
from cinder.volume.drivers.emc import emc_vmax_provision
|
|
from cinder.volume.drivers.emc import emc_vmax_utils
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
CONF = cfg.CONF
|
|
|
|
try:
|
|
import pywbem
|
|
pywbemAvailable = True
|
|
except ImportError:
|
|
pywbemAvailable = False
|
|
|
|
CINDER_EMC_CONFIG_FILE = '/etc/cinder/cinder_emc_config.xml'
|
|
CINDER_EMC_CONFIG_FILE_PREFIX = '/etc/cinder/cinder_emc_config_'
|
|
CINDER_EMC_CONFIG_FILE_POSTFIX = '.xml'
|
|
EMC_ROOT = 'root/emc'
|
|
POOL = 'storagetype:pool'
|
|
ARRAY = 'storagetype:array'
|
|
FASTPOLICY = 'storagetype:fastpolicy'
|
|
BACKENDNAME = 'volume_backend_name'
|
|
COMPOSITETYPE = 'storagetype:compositetype'
|
|
STRIPECOUNT = 'storagetype:stripecount'
|
|
MEMBERCOUNT = 'storagetype:membercount'
|
|
STRIPED = 'striped'
|
|
CONCATENATED = 'concatenated'
|
|
|
|
emc_opts = [
|
|
cfg.StrOpt('cinder_emc_config_file',
|
|
default=CINDER_EMC_CONFIG_FILE,
|
|
help='use this file for cinder emc plugin '
|
|
'config data'), ]
|
|
|
|
CONF.register_opts(emc_opts)
|
|
|
|
|
|
class EMCVMAXCommon(object):
|
|
"""Common class for SMI-S based EMC volume drivers.
|
|
|
|
This common class is for EMC volume drivers based on SMI-S.
|
|
It supports VNX and VMAX arrays.
|
|
|
|
"""
|
|
|
|
stats = {'driver_version': '1.0',
|
|
'free_capacity_gb': 0,
|
|
'reserved_percentage': 0,
|
|
'storage_protocol': None,
|
|
'total_capacity_gb': 0,
|
|
'vendor_name': 'EMC',
|
|
'volume_backend_name': None}
|
|
|
|
def __init__(self, prtcl, configuration=None):
|
|
|
|
if not pywbemAvailable:
|
|
LOG.info(_LI(
|
|
'Module PyWBEM not installed. '
|
|
'Install PyWBEM using the python-pywbem package.'))
|
|
|
|
self.protocol = prtcl
|
|
self.configuration = configuration
|
|
self.configuration.append_config_values(emc_opts)
|
|
self.conn = None
|
|
self.url = None
|
|
self.user = None
|
|
self.passwd = None
|
|
self.masking = emc_vmax_masking.EMCVMAXMasking(prtcl)
|
|
self.utils = emc_vmax_utils.EMCVMAXUtils(prtcl)
|
|
self.fast = emc_vmax_fast.EMCVMAXFast(prtcl)
|
|
self.provision = emc_vmax_provision.EMCVMAXProvision(prtcl)
|
|
|
|
def create_volume(self, volume):
|
|
"""Creates a EMC(VMAX) volume from a pre-existing storage pool.
|
|
|
|
For a concatenated compositeType:
|
|
If the volume size is over 240GB then a composite is created
|
|
EMCNumberOfMembers > 1, otherwise it defaults to a non composite
|
|
|
|
For a striped compositeType:
|
|
The user must supply an extra spec to determine how many metas
|
|
will make up the striped volume. If the meta size is greater than
|
|
240GB an error is returned to the user. Otherwise the
|
|
EMCNumberOfMembers is what the user specifies.
|
|
|
|
:param volume: volume Object
|
|
:returns: volumeInstance, the volume instance
|
|
:raises: VolumeBackendAPIException
|
|
"""
|
|
volumeSize = int(self.utils.convert_gb_to_bits(volume['size']))
|
|
volumeName = volume['id']
|
|
|
|
extraSpecs = self._initial_setup(volume)
|
|
self.conn = self._get_ecom_connection()
|
|
|
|
rc, volumeDict, storageSystemName = self._create_composite_volume(
|
|
volume, extraSpecs, volumeName, volumeSize)
|
|
|
|
LOG.info(_LI("Leaving create_volume: %(volumeName)s "
|
|
"Return code: %(rc)lu "
|
|
"volume dict: %(name)s")
|
|
% {'volumeName': volumeName,
|
|
'rc': rc,
|
|
'name': volumeDict})
|
|
|
|
return volumeDict
|
|
|
|
def create_volume_from_snapshot(self, volume, snapshot):
|
|
"""Creates a volume from a snapshot.
|
|
|
|
For VMAX, replace snapshot with clone.
|
|
|
|
:param volume - volume Object
|
|
:param snapshot - snapshot object
|
|
:returns: cloneVolumeDict - the cloned volume dictionary
|
|
"""
|
|
return self._create_cloned_volume(volume, snapshot)
|
|
|
|
def create_cloned_volume(self, cloneVolume, sourceVolume):
|
|
"""Creates a clone of the specified volume.
|
|
|
|
:param CloneVolume - clone volume Object
|
|
:param sourceVolume - volume object
|
|
:returns: cloneVolumeDict - the cloned volume dictionary
|
|
"""
|
|
return self._create_cloned_volume(cloneVolume, sourceVolume)
|
|
|
|
def delete_volume(self, volume):
|
|
"""Deletes a EMC(VMAX) volume
|
|
|
|
:param volume: volume Object
|
|
"""
|
|
LOG.info(_LI("Deleting Volume: %(volume)s")
|
|
% {'volume': volume['name']})
|
|
|
|
rc, volumeName = self._delete_volume(volume)
|
|
LOG.info(_LI("Leaving delete_volume: %(volumename)s Return code: "
|
|
"%(rc)lu")
|
|
% {'volumename': volumeName,
|
|
'rc': rc})
|
|
|
|
def create_snapshot(self, snapshot, volume):
|
|
"""Creates a snapshot.
|
|
|
|
For VMAX, replace snapshot with clone
|
|
|
|
:param snapshot: snapshot object
|
|
:param volume: volume Object to create snapshot from
|
|
:returns: cloneVolumeDict,the cloned volume dictionary
|
|
"""
|
|
return self._create_cloned_volume(snapshot, volume, True)
|
|
|
|
def delete_snapshot(self, snapshot, volume):
|
|
"""Deletes a snapshot.
|
|
|
|
:param snapshot: snapshot object
|
|
:param volume: volume Object to create snapshot from
|
|
"""
|
|
LOG.info(_LI("Delete Snapshot: %(snapshotName)s ")
|
|
% {'snapshotName': snapshot['name']})
|
|
rc, snapshotName = self._delete_volume(snapshot)
|
|
LOG.debug("Leaving delete_snapshot: %(snapshotname)s Return code: "
|
|
"%(rc)lu "
|
|
% {'snapshotname': snapshotName,
|
|
'rc': rc})
|
|
|
|
def _remove_members(
|
|
self, controllerConfigService, volumeInstance, extraSpecs):
|
|
"""This method unmaps a volume from a host.
|
|
|
|
Removes volume from the Device Masking Group that belongs to
|
|
a Masking View.
|
|
Check if fast policy is in the extra specs. If it isn't we do
|
|
not need to do anything for FAST.
|
|
Assume that isTieringPolicySupported is False unless the FAST
|
|
policy is in the extra specs and tiering is enabled on the array
|
|
|
|
:param controllerConfigService: instance name of
|
|
ControllerConfigurationService
|
|
:param volume: volume Object
|
|
"""
|
|
volumeName = volumeInstance['ElementName']
|
|
LOG.debug("Detaching volume %s" % volumeName)
|
|
fastPolicyName = extraSpecs[FASTPOLICY]
|
|
return self.masking.remove_and_reset_members(
|
|
self.conn, controllerConfigService, volumeInstance,
|
|
fastPolicyName, volumeName)
|
|
|
|
def _unmap_lun(self, volume, connector):
|
|
"""Unmaps a volume from the host.
|
|
|
|
:param volume: the volume Object
|
|
:param connector: the connector Object
|
|
:raises: VolumeBackendAPIException
|
|
"""
|
|
extraSpecs = self._initial_setup(volume)
|
|
volumename = volume['name']
|
|
LOG.info(_LI("Unmap volume: %(volume)s")
|
|
% {'volume': volumename})
|
|
|
|
device_info = self.find_device_number(volume, connector)
|
|
device_number = device_info['hostlunid']
|
|
if device_number is None:
|
|
LOG.info(_LI("Volume %s is not mapped. No volume to unmap.")
|
|
% (volumename))
|
|
return
|
|
|
|
vol_instance = self._find_lun(volume)
|
|
storage_system = vol_instance['SystemName']
|
|
|
|
configservice = self.utils.find_controller_configuration_service(
|
|
self.conn, storage_system)
|
|
if configservice is None:
|
|
exception_message = (_("Cannot find Controller Configuration "
|
|
"Service for storage system "
|
|
"%(storage_system)s")
|
|
% {'storage_system': storage_system})
|
|
raise exception.VolumeBackendAPIException(data=exception_message)
|
|
|
|
self._remove_members(configservice, vol_instance, extraSpecs)
|
|
|
|
def initialize_connection(self, volume, connector):
|
|
"""Initializes the connection and returns device and connection info.
|
|
|
|
The volume may be already mapped, if this is so the deviceInfo tuple
|
|
is returned. If the volume is not already mapped then we need to
|
|
gather information to either 1. Create a new masking view or 2. Add
|
|
the volume to an existing storage group within an already existing
|
|
maskingview.
|
|
|
|
The naming convention is the following:
|
|
initiatorGroupName = OS-<shortHostName>-<shortProtocol>-IG
|
|
e.g OS-myShortHost-I-IG
|
|
storageGroupName = OS-<shortHostName>-<poolName>-<shortProtocol>-SG
|
|
e.g OS-myShortHost-SATA_BRONZ1-I-SG
|
|
portGroupName = OS-<target>-PG The portGroupName will come from
|
|
the EMC configuration xml file.
|
|
These are precreated. If the portGroup does not exist
|
|
then a error will be returned to the user
|
|
maskingView = OS-<shortHostName>-<poolName>-<shortProtocol>-MV
|
|
e.g OS-myShortHost-SATA_BRONZ1-I-MV
|
|
|
|
:param volume: volume Object
|
|
:param connector: the connector Object
|
|
:returns: deviceInfoDict, device information tuple
|
|
:raises: VolumeBackendAPIException
|
|
"""
|
|
extraSpecs = self._initial_setup(volume)
|
|
|
|
volumeName = volume['name']
|
|
LOG.info(_LI("Initialize connection: %(volume)s")
|
|
% {'volume': volumeName})
|
|
self.conn = self._get_ecom_connection()
|
|
deviceInfoDict = self._wrap_find_device_number(volume, connector)
|
|
if ('hostlunid' in deviceInfoDict and
|
|
deviceInfoDict['hostlunid'] is not None):
|
|
isSameHost = self._is_same_host(connector, deviceInfoDict)
|
|
if isSameHost:
|
|
# Device is already mapped so we will leave the state as is.
|
|
deviceNumber = deviceInfoDict['hostlunid']
|
|
LOG.info(_LI("Volume %(volume)s is already mapped. "
|
|
"The device number is %(deviceNumber)s.")
|
|
% {'volume': volumeName,
|
|
'deviceNumber': deviceNumber})
|
|
else:
|
|
deviceInfoDict = self._attach_volume(volume, connector,
|
|
extraSpecs, True)
|
|
else:
|
|
deviceInfoDict = self._attach_volume(volume, connector, extraSpecs)
|
|
|
|
return deviceInfoDict
|
|
|
|
def _attach_volume(self, volume, connector, extraSpecs,
|
|
isLiveMigration=None):
|
|
"""Attach a volume to a host.
|
|
|
|
If live migration is being undertaken then the volume
|
|
remains attached to the source host.
|
|
|
|
:params volume: the volume object
|
|
:params connector: the connector object
|
|
:params extraSpecs: the volume extra specs
|
|
:param isLiveMigration: boolean, can be None
|
|
:returns: deviceInfoDict
|
|
"""
|
|
volumeName = volume['name']
|
|
maskingViewDict = self._populate_masking_dict(
|
|
volume, connector, extraSpecs)
|
|
if isLiveMigration:
|
|
maskingViewDict['isLiveMigration'] = True
|
|
else:
|
|
maskingViewDict['isLiveMigration'] = False
|
|
|
|
rollbackDict = self.masking.get_or_create_masking_view_and_map_lun(
|
|
self.conn, maskingViewDict)
|
|
|
|
# Find host lun id again after the volume is exported to the host.
|
|
deviceInfoDict = self.find_device_number(volume, connector)
|
|
if 'hostlunid' not in deviceInfoDict:
|
|
# Did not successfully attach to host,
|
|
# so a rollback for FAST is required.
|
|
LOG.error(_LE("Error Attaching volume %(vol)s.")
|
|
% {'vol': volumeName})
|
|
if rollbackDict['fastPolicyName'] is not None:
|
|
(
|
|
self.masking
|
|
._check_if_rollback_action_for_masking_required(
|
|
self.conn,
|
|
rollbackDict['controllerConfigService'],
|
|
rollbackDict['volumeInstance'],
|
|
rollbackDict['volumeName'],
|
|
rollbackDict['fastPolicyName'],
|
|
rollbackDict['defaultStorageGroupInstanceName']))
|
|
exception_message = ("Error Attaching volume %(vol)s."
|
|
% {'vol': volumeName})
|
|
raise exception.VolumeBackendAPIException(
|
|
data=exception_message)
|
|
|
|
return deviceInfoDict
|
|
|
|
def _is_same_host(self, connector, deviceInfoDict):
|
|
"""Check if the host is the same.
|
|
|
|
Check if the host to attach to is the same host
|
|
that is already attached. This is necessary for
|
|
live migration.
|
|
|
|
:params connector: the connector object
|
|
:params deviceInfoDict: the device information
|
|
:returns: boolean True/False
|
|
"""
|
|
if 'host' in connector:
|
|
currentHost = connector['host']
|
|
if ('maskingview' in deviceInfoDict and
|
|
deviceInfoDict['maskingview'] is not None):
|
|
if currentHost in deviceInfoDict['maskingview']:
|
|
return True
|
|
return False
|
|
|
|
def _wrap_find_device_number(self, volume, connector):
|
|
"""Aid for unit testing
|
|
|
|
:params volume: the volume Object
|
|
:params connector: the connector Object
|
|
:returns: deviceInfoDict
|
|
"""
|
|
return self.find_device_number(volume, connector)
|
|
|
|
def terminate_connection(self, volume, connector):
|
|
"""Disallow connection from connector.
|
|
|
|
:params volume: the volume Object
|
|
:params connectorL the connector Object
|
|
"""
|
|
self._initial_setup(volume)
|
|
|
|
volumename = volume['name']
|
|
LOG.info(_LI("Terminate connection: %(volume)s")
|
|
% {'volume': volumename})
|
|
|
|
self.conn = self._get_ecom_connection()
|
|
self._unmap_lun(volume, connector)
|
|
|
|
def extend_volume(self, volume, newSize):
|
|
"""Extends an existing volume.
|
|
|
|
Prequisites:
|
|
1. The volume must be composite e.g StorageVolume.EMCIsComposite=True
|
|
2. The volume can only be concatenated
|
|
e.g StorageExtent.IsConcatenated=True
|
|
|
|
:params volume: the volume Object
|
|
:params newSize: the new size to increase the volume to
|
|
:raises: VolumeBackendAPIException
|
|
"""
|
|
originalVolumeSize = volume['size']
|
|
volumeName = volume['name']
|
|
self._initial_setup(volume)
|
|
self.conn = self._get_ecom_connection()
|
|
volumeInstance = self._find_lun(volume)
|
|
if volumeInstance is None:
|
|
exceptionMessage = (_("Cannot find Volume: %(volumename)s. "
|
|
"Extend operation. Exiting....")
|
|
% {'volumename': volumeName})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
|
|
if int(originalVolumeSize) > int(newSize):
|
|
exceptionMessage = (_(
|
|
"Your original size: %(originalVolumeSize)s GB is greater "
|
|
"than: %(newSize)s GB. Only Extend is supported. Exiting...")
|
|
% {'originalVolumeSize': originalVolumeSize,
|
|
'newSize': newSize})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
|
|
additionalVolumeSize = six.text_type(
|
|
int(newSize) - int(originalVolumeSize))
|
|
additionalVolumeSize = self.utils.convert_gb_to_bits(
|
|
additionalVolumeSize)
|
|
|
|
# Is the volume extendable.
|
|
isConcatenated = self.utils.check_if_volume_is_extendable(
|
|
self.conn, volumeInstance)
|
|
if 'True' not in isConcatenated:
|
|
exceptionMessage = (_(
|
|
"Volume: %(volumeName)s is not a concatenated volume. "
|
|
"You can only perform extend on concatenated volume. "
|
|
"Exiting...")
|
|
% {'volumeName': volumeName})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
else:
|
|
compositeType = self.utils.get_composite_type(CONCATENATED)
|
|
|
|
LOG.debug("Extend Volume: %(volume)s New size: %(newSize)s GBs"
|
|
% {'volume': volumeName,
|
|
'newSize': newSize})
|
|
|
|
deviceId = volumeInstance['DeviceID']
|
|
storageSystemName = volumeInstance['SystemName']
|
|
LOG.debug(
|
|
"Device ID: %(deviceid)s: Storage System: "
|
|
"%(storagesystem)s"
|
|
% {'deviceid': deviceId,
|
|
'storagesystem': storageSystemName})
|
|
|
|
storageConfigService = self.utils.find_storage_configuration_service(
|
|
self.conn, storageSystemName)
|
|
|
|
elementCompositionService = (
|
|
self.utils.find_element_composition_service(
|
|
self.conn, storageSystemName))
|
|
|
|
# create a volume to the size of the
|
|
# newSize - oldSize = additionalVolumeSize
|
|
unboundVolumeInstance = self._create_and_get_unbound_volume(
|
|
self.conn, storageConfigService, volumeInstance.path,
|
|
additionalVolumeSize)
|
|
if unboundVolumeInstance is None:
|
|
exceptionMessage = (_(
|
|
"Error Creating unbound volume on an Extend operation"))
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
|
|
# add the new unbound volume to the original composite volume
|
|
rc, modifiedVolumeDict = (
|
|
self._modify_and_get_composite_volume_instance(
|
|
self.conn, elementCompositionService, volumeInstance,
|
|
unboundVolumeInstance.path, volumeName, compositeType))
|
|
if modifiedVolumeDict is None:
|
|
exceptionMessage = (_(
|
|
"On an Extend Operation, error adding volume to composite "
|
|
"volume: %(volumename)s. ")
|
|
% {'volumename': volumeName})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
|
|
# check the occupied space of the new extended volume
|
|
extendedVolumeInstance = self.utils.find_volume_instance(
|
|
self.conn, modifiedVolumeDict, volumeName)
|
|
extendedVolumeSize = self.utils.get_volume_size(
|
|
self.conn, extendedVolumeInstance)
|
|
LOG.debug(
|
|
"The actual volume size of the extended volume: %(volumeName)s "
|
|
"is %(volumeSize)s"
|
|
% {'volumeName': volumeName,
|
|
'volumeSize': extendedVolumeSize})
|
|
|
|
# If the requested size and the actual size don't
|
|
# tally throw an exception
|
|
newSizeBits = self.utils.convert_gb_to_bits(newSize)
|
|
diffVolumeSize = self.utils.compare_size(
|
|
newSizeBits, extendedVolumeSize)
|
|
if diffVolumeSize != 0:
|
|
exceptionMessage = (_(
|
|
"The requested size : %(requestedSize)s is not the same as "
|
|
"resulting size: %(resultSize)s")
|
|
% {'requestedSize': newSizeBits,
|
|
'resultSize': extendedVolumeSize})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
|
|
LOG.debug(
|
|
"Leaving extend_volume: %(volumeName)s "
|
|
"Return code: %(rc)lu "
|
|
"volume dict: %(name)s"
|
|
% {'volumeName': volumeName,
|
|
'rc': rc,
|
|
'name': modifiedVolumeDict})
|
|
|
|
return modifiedVolumeDict
|
|
|
|
def update_volume_stats(self):
|
|
"""Retrieve stats info.
|
|
"""
|
|
if hasattr(self.configuration, 'cinder_emc_config_file'):
|
|
emcConfigFileName = self.configuration.cinder_emc_config_file
|
|
else:
|
|
emcConfigFileName = self.configuration.safe_get(
|
|
'cinder_emc_config_file')
|
|
|
|
backendName = self.configuration.safe_get('volume_backend_name')
|
|
LOG.debug(
|
|
"Updating volume stats on file %(emcConfigFileName)s on "
|
|
"backend %(backendName)s "
|
|
% {'emcConfigFileName': emcConfigFileName,
|
|
'backendName': backendName})
|
|
|
|
poolName = self.utils.parse_pool_name_from_file(emcConfigFileName)
|
|
if poolName is None:
|
|
LOG.error(_LE(
|
|
"PoolName %(poolName)s must be in the file "
|
|
"%(emcConfigFileName)s ")
|
|
% {'poolName': poolName,
|
|
'emcConfigFileName': emcConfigFileName})
|
|
arrayName = self.utils.parse_array_name_from_file(emcConfigFileName)
|
|
if arrayName is None:
|
|
LOG.error(_LE(
|
|
"Array Serial Number %(arrayName)s must be in the file "
|
|
"%(emcConfigFileName)s ")
|
|
% {'arrayName': arrayName,
|
|
'emcConfigFileName': emcConfigFileName})
|
|
# This value can be None
|
|
fastPolicyName = self.utils.parse_fast_policy_name_from_file(
|
|
emcConfigFileName)
|
|
if fastPolicyName is not None:
|
|
LOG.debug(
|
|
"Fast policy %(fastPolicyName)s is enabled on %(arrayName)s. "
|
|
% {'fastPolicyName': fastPolicyName,
|
|
'arrayName': arrayName})
|
|
else:
|
|
LOG.debug(
|
|
"No Fast policy for Array:%(arrayName)s "
|
|
"backend:%(backendName)s"
|
|
% {'arrayName': arrayName,
|
|
'backendName': backendName})
|
|
|
|
if self.conn is None:
|
|
self._set_ecom_credentials(emcConfigFileName)
|
|
|
|
storageSystemInstanceName = (
|
|
self.utils.find_storageSystem(self.conn, arrayName))
|
|
|
|
isTieringPolicySupported = (
|
|
self.fast.is_tiering_policy_enabled_on_storage_system(
|
|
self.conn, storageSystemInstanceName))
|
|
|
|
if (fastPolicyName is not None and
|
|
isTieringPolicySupported is True): # FAST enabled
|
|
total_capacity_gb, free_capacity_gb = (
|
|
self.fast.get_capacities_associated_to_policy(
|
|
self.conn, arrayName, fastPolicyName))
|
|
LOG.info(_LI(
|
|
"FAST: capacity stats for policy %(fastPolicyName)s on "
|
|
"array %(arrayName)s (total_capacity_gb=%(total_capacity_gb)lu"
|
|
", free_capacity_gb=%(free_capacity_gb)lu")
|
|
% {'fastPolicyName': fastPolicyName,
|
|
'arrayName': arrayName,
|
|
'total_capacity_gb': total_capacity_gb,
|
|
'free_capacity_gb': free_capacity_gb})
|
|
else: # NON-FAST
|
|
total_capacity_gb, free_capacity_gb = (
|
|
self.utils.get_pool_capacities(self.conn, poolName, arrayName))
|
|
LOG.info(_LI(
|
|
"NON-FAST: capacity stats for pool %(poolName)s on array "
|
|
"%(arrayName)s (total_capacity_gb=%(total_capacity_gb)lu, "
|
|
"free_capacity_gb=%(free_capacity_gb)lu")
|
|
% {'poolName': poolName,
|
|
'arrayName': arrayName,
|
|
'total_capacity_gb': total_capacity_gb,
|
|
'free_capacity_gb': free_capacity_gb})
|
|
|
|
if poolName is None:
|
|
LOG.debug("Unable to get the poolName for location_info")
|
|
if arrayName is None:
|
|
LOG.debug("Unable to get the arrayName for location_info")
|
|
if fastPolicyName is None:
|
|
LOG.debug("FAST is not enabled for this configuration: "
|
|
"%(emcConfigFileName)s"
|
|
% {'emcConfigFileName': emcConfigFileName})
|
|
|
|
location_info = ("%(arrayName)s#%(poolName)s#%(policyName)s"
|
|
% {'arrayName': arrayName,
|
|
'poolName': poolName,
|
|
'policyName': fastPolicyName})
|
|
|
|
data = {'total_capacity_gb': total_capacity_gb,
|
|
'free_capacity_gb': free_capacity_gb,
|
|
'reserved_percentage': 0,
|
|
'QoS_support': False,
|
|
'volume_backend_name': backendName or self.__class__.__name__,
|
|
'vendor_name': "EMC",
|
|
'driver_version': '2.0',
|
|
'storage_protocol': 'unknown',
|
|
'location_info': location_info}
|
|
|
|
self.stats = data
|
|
|
|
return self.stats
|
|
|
|
def retype(self, ctxt, volume, new_type, diff, host):
|
|
"""Migrate volume to another host using retype.
|
|
|
|
:param ctxt: context
|
|
:param volume: the volume object including the volume_type_id
|
|
:param new_type: the new volume type.
|
|
:param host: The host dict holding the relevant target(destination)
|
|
information
|
|
:returns: boolean True/False
|
|
:returns: list
|
|
"""
|
|
|
|
volumeName = volume['name']
|
|
volumeStatus = volume['status']
|
|
LOG.info(_LI("Migrating using retype Volume: %(volume)s")
|
|
% {'volume': volumeName})
|
|
|
|
extraSpecs = self._initial_setup(volume)
|
|
self.conn = self._get_ecom_connection()
|
|
|
|
volumeInstance = self._find_lun(volume)
|
|
if volumeInstance is None:
|
|
LOG.error(_LE("Volume %(name)s not found on the array. "
|
|
"No volume to migrate using retype.")
|
|
% {'name': volumeName})
|
|
return False
|
|
|
|
storageSystemName = volumeInstance['SystemName']
|
|
isValid, targetPoolName, targetFastPolicyName = (
|
|
self._is_valid_for_storage_assisted_migration(
|
|
volumeInstance.path, host, storageSystemName,
|
|
volumeName, volumeStatus))
|
|
|
|
if not isValid:
|
|
LOG.error(_LE("Volume %(name)s is not suitable for storage "
|
|
"assisted migration using retype")
|
|
% {'name': volumeName})
|
|
return False
|
|
if volume['host'] != host['host']:
|
|
LOG.debug(
|
|
"Retype Volume %(name)s from source host %(sourceHost)s "
|
|
"to target host %(targetHost)s"
|
|
% {'name': volumeName,
|
|
'sourceHost': volume['host'],
|
|
'targetHost': host['host']})
|
|
return self._migrate_volume(
|
|
volume, volumeInstance, targetPoolName, targetFastPolicyName,
|
|
extraSpecs[FASTPOLICY], new_type)
|
|
|
|
return True
|
|
|
|
def migrate_volume(self, ctxt, volume, host, new_type=None):
|
|
"""Migrate volume to another host
|
|
|
|
:param ctxt: context
|
|
:param volume: the volume object including the volume_type_id
|
|
:param host: the host dict holding the relevant target(destination)
|
|
information
|
|
:param new_type: None
|
|
:returns: boolean True/False
|
|
:returns: list
|
|
"""
|
|
LOG.warn(_LW("The VMAX plugin only supports Retype. "
|
|
"If a pool based migration is necessary "
|
|
"this will happen on a Retype "
|
|
"From the command line: "
|
|
"cinder --os-volume-api-version 2 retype "
|
|
"<volumeId> <volumeType> --migration-policy on-demand"))
|
|
return True, {}
|
|
|
|
def _migrate_volume(
|
|
self, volume, volumeInstance, targetPoolName,
|
|
targetFastPolicyName, sourceFastPolicyName, new_type=None):
|
|
"""Migrate volume to another host
|
|
|
|
:param volume: the volume object including the volume_type_id
|
|
:param volumeInstance: the volume instance
|
|
:param targetPoolName: the target poolName
|
|
:param targetFastPolicyName: the target FAST policy name, can be None
|
|
:param sourceFastPolicyName: the source FAST policy name, can be None
|
|
:param new_type: None
|
|
:returns: boolean True/False
|
|
:returns: empty list
|
|
"""
|
|
volumeName = volume['name']
|
|
storageSystemName = volumeInstance['SystemName']
|
|
|
|
sourcePoolInstanceName = self.utils.get_assoc_pool_from_volume(
|
|
self.conn, volumeInstance.path)
|
|
|
|
moved, rc = self._migrate_volume_from(
|
|
volume, volumeInstance, targetPoolName, sourceFastPolicyName)
|
|
|
|
if moved is False and sourceFastPolicyName is not None:
|
|
# Return the volume to the default source fast policy storage
|
|
# group because the migrate was unsuccessful
|
|
LOG.warn(_LW("Failed to migrate: %(volumeName)s from "
|
|
"default source storage group "
|
|
"for FAST policy: %(sourceFastPolicyName)s "
|
|
"Attempting cleanup... ")
|
|
% {'volumeName': volumeName,
|
|
'sourceFastPolicyName': sourceFastPolicyName})
|
|
if sourcePoolInstanceName == self.utils.get_assoc_pool_from_volume(
|
|
self.conn, volumeInstance.path):
|
|
self._migrate_cleanup(self.conn, volumeInstance,
|
|
storageSystemName, sourceFastPolicyName,
|
|
volumeName)
|
|
else:
|
|
# migrate was successful but still issues
|
|
self._migrate_rollback(
|
|
self.conn, volumeInstance, storageSystemName,
|
|
sourceFastPolicyName, volumeName, sourcePoolInstanceName)
|
|
|
|
return moved
|
|
|
|
if targetFastPolicyName == 'None':
|
|
targetFastPolicyName = None
|
|
|
|
if moved is True and targetFastPolicyName is not None:
|
|
if not self._migrate_volume_fast_target(
|
|
volumeInstance, storageSystemName,
|
|
targetFastPolicyName, volumeName):
|
|
LOG.warn(_LW("Attempting a rollback of: %(volumeName)s to "
|
|
"original pool %(sourcePoolInstanceName)s ")
|
|
% {'volumeName': volumeName,
|
|
'sourcePoolInstanceName': sourcePoolInstanceName})
|
|
self._migrate_rollback(
|
|
self.conn, volumeInstance, storageSystemName,
|
|
sourceFastPolicyName, volumeName, sourcePoolInstanceName)
|
|
|
|
if rc == 0:
|
|
moved = True
|
|
|
|
return moved
|
|
|
|
def _migrate_rollback(self, conn, volumeInstance,
|
|
storageSystemName, sourceFastPolicyName,
|
|
volumeName, sourcePoolInstanceName):
|
|
"""Full rollback
|
|
|
|
Failed on final step on adding migrated volume to new target
|
|
default storage group for the target FAST policy
|
|
|
|
:param conn: connection info to ECOM
|
|
:param volumeInstance: the volume instance
|
|
:param storageSystemName: the storage system name
|
|
:param sourceFastPolicyName: the source FAST policy name
|
|
:param volumeName: the volume Name
|
|
|
|
:returns: boolean True/False
|
|
:returns: int, the return code from migrate operation
|
|
"""
|
|
|
|
LOG.warn(_LW("_migrate_rollback on : %(volumeName)s from ")
|
|
% {'volumeName': volumeName})
|
|
|
|
storageRelocationService = self.utils.find_storage_relocation_service(
|
|
conn, storageSystemName)
|
|
|
|
try:
|
|
self.provision.migrate_volume_to_storage_pool(
|
|
conn, storageRelocationService, volumeInstance.path,
|
|
sourcePoolInstanceName)
|
|
except Exception:
|
|
exceptionMessage = (_(
|
|
"Failed to return volume %(volumeName)s to "
|
|
"original storage pool. Please contact your system "
|
|
"administrator to return it to the correct location ")
|
|
% {'volumeName': volumeName})
|
|
LOG.error(exceptionMessage)
|
|
|
|
if sourceFastPolicyName is not None:
|
|
self.add_to_default_SG(
|
|
conn, volumeInstance, storageSystemName, sourceFastPolicyName,
|
|
volumeName)
|
|
|
|
def _migrate_cleanup(self, conn, volumeInstance,
|
|
storageSystemName, sourceFastPolicyName,
|
|
volumeName):
|
|
"""If the migrate fails, put volume back to source FAST SG
|
|
|
|
:param conn: connection info to ECOM
|
|
:param volumeInstance: the volume instance
|
|
:param storageSystemName: the storage system name
|
|
:param sourceFastPolicyName: the source FAST policy name
|
|
:param volumeName: the volume Name
|
|
|
|
:returns: boolean True/False
|
|
:returns: int, the return code from migrate operation
|
|
"""
|
|
|
|
LOG.warn(_LW("_migrate_cleanup on : %(volumeName)s from ")
|
|
% {'volumeName': volumeName})
|
|
|
|
controllerConfigurationService = (
|
|
self.utils.find_controller_configuration_service(
|
|
conn, storageSystemName))
|
|
|
|
# check to see what SG it is in
|
|
assocStorageGroupInstanceName = (
|
|
self.utils.get_storage_group_from_volume(conn,
|
|
volumeInstance.path))
|
|
# This is the SG it should be in
|
|
defaultStorageGroupInstanceName = (
|
|
self.fast.get_policy_default_storage_group(
|
|
conn, controllerConfigurationService, sourceFastPolicyName))
|
|
|
|
# It is not in any storage group. Must add it to default source
|
|
if assocStorageGroupInstanceName is None:
|
|
self.add_to_default_SG(conn, volumeInstance,
|
|
storageSystemName, sourceFastPolicyName,
|
|
volumeName)
|
|
|
|
# It is in the incorrect storage group
|
|
if (assocStorageGroupInstanceName is not None and
|
|
(assocStorageGroupInstanceName !=
|
|
defaultStorageGroupInstanceName)):
|
|
self.provision.remove_device_from_storage_group(
|
|
conn, controllerConfigurationService,
|
|
assocStorageGroupInstanceName, volumeInstance.path, volumeName)
|
|
|
|
self.add_to_default_SG(
|
|
conn, volumeInstance, storageSystemName, sourceFastPolicyName,
|
|
volumeName)
|
|
|
|
def _migrate_volume_fast_target(
|
|
self, volumeInstance, storageSystemName,
|
|
targetFastPolicyName, volumeName):
|
|
"""If the target host is FAST enabled.
|
|
|
|
If the target host is FAST enabled then we need to add it to the
|
|
default storage group for that policy
|
|
|
|
:param volumeInstance: the volume instance
|
|
:param storageSystemName: the storage system name
|
|
:param targetFastPolicyName: the target fast policy name
|
|
:param volumeName: the volume name
|
|
:returns: boolean True/False
|
|
"""
|
|
falseRet = False
|
|
LOG.info(_LI("Adding volume: %(volumeName)s to default storage group "
|
|
"for FAST policy: %(fastPolicyName)s ")
|
|
% {'volumeName': volumeName,
|
|
'fastPolicyName': targetFastPolicyName})
|
|
|
|
controllerConfigurationService = (
|
|
self.utils.find_controller_configuration_service(
|
|
self.conn, storageSystemName))
|
|
|
|
defaultStorageGroupInstanceName = (
|
|
self.fast.get_or_create_default_storage_group(
|
|
self.conn, controllerConfigurationService,
|
|
targetFastPolicyName, volumeInstance))
|
|
if defaultStorageGroupInstanceName is None:
|
|
exceptionMessage = (_(
|
|
"Unable to create or get default storage group for FAST policy"
|
|
": %(fastPolicyName)s. ")
|
|
% {'fastPolicyName': targetFastPolicyName})
|
|
LOG.error(exceptionMessage)
|
|
|
|
return falseRet
|
|
|
|
defaultStorageGroupInstanceName = (
|
|
self.fast.add_volume_to_default_storage_group_for_fast_policy(
|
|
self.conn, controllerConfigurationService, volumeInstance,
|
|
volumeName, targetFastPolicyName))
|
|
if defaultStorageGroupInstanceName is None:
|
|
exceptionMessage = (_(
|
|
"Failed to verify that volume was added to storage group for "
|
|
"FAST policy: %(fastPolicyName)s. ")
|
|
% {'fastPolicyName': targetFastPolicyName})
|
|
LOG.error(exceptionMessage)
|
|
return falseRet
|
|
|
|
return True
|
|
|
|
def _migrate_volume_from(self, volume, volumeInstance,
|
|
targetPoolName, sourceFastPolicyName):
|
|
"""Check FAST policies and migrate from source pool
|
|
|
|
:param volume: the volume object including the volume_type_id
|
|
:param volumeInstance: the volume instance
|
|
:param targetPoolName: the target poolName
|
|
:param sourceFastPolicyName: the source FAST policy name, can be None
|
|
:returns: boolean True/False
|
|
:returns: int, the return code from migrate operation
|
|
"""
|
|
falseRet = (False, -1)
|
|
volumeName = volume['name']
|
|
storageSystemName = volumeInstance['SystemName']
|
|
|
|
LOG.debug("sourceFastPolicyName is : %(sourceFastPolicyName)s. "
|
|
% {'sourceFastPolicyName': sourceFastPolicyName})
|
|
|
|
# If the source volume is is FAST enabled it must first be removed
|
|
# from the default storage group for that policy
|
|
if sourceFastPolicyName is not None:
|
|
self.remove_from_default_SG(
|
|
self.conn, volumeInstance, storageSystemName,
|
|
sourceFastPolicyName, volumeName)
|
|
|
|
# migrate from one pool to another
|
|
storageRelocationService = self.utils.find_storage_relocation_service(
|
|
self.conn, storageSystemName)
|
|
|
|
targetPoolInstanceName = self.utils.get_pool_by_name(
|
|
self.conn, targetPoolName, storageSystemName)
|
|
if targetPoolInstanceName is None:
|
|
exceptionMessage = (_(
|
|
"Error finding targe pool instance name for pool: "
|
|
"%(targetPoolName)s. ")
|
|
% {'targetPoolName': targetPoolName})
|
|
LOG.error(exceptionMessage)
|
|
return falseRet
|
|
try:
|
|
rc = self.provision.migrate_volume_to_storage_pool(
|
|
self.conn, storageRelocationService, volumeInstance.path,
|
|
targetPoolInstanceName)
|
|
except Exception as e:
|
|
# rollback by deleting the volume if adding the volume to the
|
|
# default storage group were to fail
|
|
LOG.error(_LE("Exception: %s") % six.text_type(e))
|
|
exceptionMessage = (_("Error migrating volume: %(volumename)s. "
|
|
"to target pool %(targetPoolName)s. ")
|
|
% {'volumename': volumeName,
|
|
'targetPoolName': targetPoolName})
|
|
LOG.error(exceptionMessage)
|
|
return falseRet
|
|
|
|
# check that the volume is now migrated to the correct storage pool,
|
|
# if it is terminate the migrate session
|
|
foundPoolInstanceName = self.utils.get_assoc_pool_from_volume(
|
|
self.conn, volumeInstance.path)
|
|
|
|
if (foundPoolInstanceName is None or
|
|
(foundPoolInstanceName['InstanceID'] !=
|
|
targetPoolInstanceName['InstanceID'])):
|
|
exceptionMessage = (_(
|
|
"Volume : %(volumeName)s. was not successfully migrated to "
|
|
"target pool %(targetPoolName)s.")
|
|
% {'volumeName': volumeName,
|
|
'targetPoolName': targetPoolName})
|
|
LOG.error(exceptionMessage)
|
|
return falseRet
|
|
|
|
else:
|
|
LOG.debug("Terminating migration session on : %(volumeName)s. "
|
|
% {'volumeName': volumeName})
|
|
self.provision._terminate_migrate_session(
|
|
self.conn, volumeInstance.path)
|
|
|
|
if rc == 0:
|
|
moved = True
|
|
|
|
return moved, rc
|
|
|
|
def remove_from_default_SG(
|
|
self, conn, volumeInstance, storageSystemName,
|
|
sourceFastPolicyName, volumeName):
|
|
"""For FAST, remove volume from default storage group
|
|
|
|
:param conn: connection info to ECOM
|
|
:param volumeInstance: the volume instance
|
|
:param storageSystemName: the storage system name
|
|
:param sourceFastPolicyName: the source FAST policy name
|
|
:param volumeName: the volume Name
|
|
|
|
:returns: boolean True/False
|
|
:returns: int, the return code from migrate operation
|
|
"""
|
|
controllerConfigurationService = (
|
|
self.utils.find_controller_configuration_service(
|
|
conn, storageSystemName))
|
|
try:
|
|
defaultStorageGroupInstanceName = (
|
|
self.masking.remove_device_from_default_storage_group(
|
|
conn, controllerConfigurationService,
|
|
volumeInstance.path, volumeName, sourceFastPolicyName))
|
|
except Exception as ex:
|
|
LOG.error(_LE("Exception: %s") % six.text_type(ex))
|
|
exceptionMessage = (_("Failed to remove: %(volumename)s. "
|
|
"from the default storage group for "
|
|
"FAST policy %(fastPolicyName)s. ")
|
|
% {'volumename': volumeName,
|
|
'fastPolicyName': sourceFastPolicyName})
|
|
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
|
|
if defaultStorageGroupInstanceName is None:
|
|
warnMessage = (_("The volume: %(volumename)s. "
|
|
"was not first part of the default storage "
|
|
"group for FAST policy %(fastPolicyName)s.")
|
|
% {'volumename': volumeName,
|
|
'fastPolicyName': sourceFastPolicyName})
|
|
LOG.warn(warnMessage)
|
|
|
|
def add_to_default_SG(
|
|
self, conn, volumeInstance, storageSystemName,
|
|
targetFastPolicyName, volumeName):
|
|
"""For FAST, add volume to default storage group
|
|
|
|
:param conn: connection info to ECOM
|
|
:param volumeInstance: the volume instance
|
|
:param storageSystemName: the storage system name
|
|
:param targetFastPolicyName: the target FAST policy name
|
|
:param volumeName: the volume Name
|
|
|
|
:returns: boolean True/False
|
|
:returns: int, the return code from migrate operation
|
|
"""
|
|
controllerConfigurationService = (
|
|
self.utils.find_controller_configuration_service(
|
|
conn, storageSystemName))
|
|
assocDefaultStorageGroupName = (
|
|
self.fast
|
|
.add_volume_to_default_storage_group_for_fast_policy(
|
|
conn, controllerConfigurationService, volumeInstance,
|
|
volumeName, targetFastPolicyName))
|
|
if assocDefaultStorageGroupName is None:
|
|
errorMsg = (_LE(
|
|
"Failed to add %(volumeName)s "
|
|
"to default storage group for fast policy "
|
|
"%(fastPolicyName)s ")
|
|
% {'volumeName': volumeName,
|
|
'fastPolicyName': targetFastPolicyName})
|
|
LOG.error(errorMsg)
|
|
|
|
def _is_valid_for_storage_assisted_migration(
|
|
self, volumeInstanceName, host, sourceArraySerialNumber,
|
|
volumeName, volumeStatus):
|
|
"""Check if volume is suitable for storage assisted (pool) migration.
|
|
|
|
:param volumeInstanceName: the volume instance id
|
|
:param host: the host object
|
|
:param sourceArraySerialNumber: the array serial number of
|
|
the original volume
|
|
:param volumeName: the name of the volume to be migrated
|
|
:param volumeStatus: the status of the volume e.g
|
|
:returns: boolean, True/False
|
|
:returns: string, targetPool
|
|
:returns: string, targetFastPolicy
|
|
"""
|
|
falseRet = (False, None, None)
|
|
if 'location_info' not in host['capabilities']:
|
|
LOG.error(_LE('Error getting target pool name and array'))
|
|
return falseRet
|
|
info = host['capabilities']['location_info']
|
|
|
|
LOG.debug("Location info is : %(info)s."
|
|
% {'info': info})
|
|
try:
|
|
infoDetail = info.split('#')
|
|
targetArraySerialNumber = infoDetail[0]
|
|
targetPoolName = infoDetail[1]
|
|
targetFastPolicy = infoDetail[2]
|
|
except Exception:
|
|
LOG.error(_LE("Error parsing target pool name, array, "
|
|
"and fast policy"))
|
|
|
|
if targetArraySerialNumber not in sourceArraySerialNumber:
|
|
errorMessage = (_LE(
|
|
"The source array : %(sourceArraySerialNumber)s does not "
|
|
"match the target array: %(targetArraySerialNumber)s"
|
|
"skipping storage-assisted migration")
|
|
% {'sourceArraySerialNumber': sourceArraySerialNumber,
|
|
'targetArraySerialNumber': targetArraySerialNumber})
|
|
LOG.error(errorMessage)
|
|
return falseRet
|
|
|
|
# get the pool from the source array and check that is is different
|
|
# to the pool in the target array
|
|
assocPoolInstanceName = self.utils.get_assoc_pool_from_volume(
|
|
self.conn, volumeInstanceName)
|
|
assocPoolInstance = self.conn.GetInstance(
|
|
assocPoolInstanceName)
|
|
if assocPoolInstance['ElementName'] == targetPoolName:
|
|
errorMessage = (_LE("No action required. Volume : %(volumeName)s "
|
|
"is already part of pool : %(pool)s")
|
|
% {'volumeName': volumeName,
|
|
'pool': targetPoolName})
|
|
LOG.error(errorMessage)
|
|
return falseRet
|
|
|
|
LOG.info(_LI("Volume status is: %s"), volumeStatus)
|
|
if (host['capabilities']['storage_protocol'] != self.protocol and
|
|
(volumeStatus != 'available' and volumeStatus != 'retyping')):
|
|
errorMessage = (_LE(
|
|
"Only available volumes can be migrated between "
|
|
"different protocols"))
|
|
LOG.error(errorMessage)
|
|
return falseRet
|
|
|
|
return (True, targetPoolName, targetFastPolicy)
|
|
|
|
def _set_config_file_and_get_extra_specs(self, volume, filename=None):
|
|
"""Given the volume object get the associated volumetype.
|
|
|
|
Given the volume object get the associated volumetype and the
|
|
extra specs associated with it.
|
|
Based on the name of the config group, register the config file
|
|
|
|
:param volume: the volume object including the volume_type_id
|
|
:returns: tuple the extra specs tuple
|
|
:returns: string configuration file
|
|
"""
|
|
extraSpecs = self.utils.get_volumetype_extraspecs(volume)
|
|
configGroup = None
|
|
|
|
# If there are no extra specs then the default case is assumed
|
|
if extraSpecs:
|
|
configGroup = self.configuration.config_group
|
|
LOG.info(_LI("configGroup of current host: %s"), configGroup)
|
|
|
|
configurationFile = self._register_config_file_from_config_group(
|
|
configGroup)
|
|
|
|
return extraSpecs, configurationFile
|
|
|
|
def _get_ecom_connection(self):
|
|
"""Get the ecom connection
|
|
|
|
:returns: conn,the ecom connection
|
|
"""
|
|
conn = pywbem.WBEMConnection(self.url, (self.user, self.passwd),
|
|
default_namespace='root/emc')
|
|
if conn is None:
|
|
exception_message = (_("Cannot connect to ECOM server"))
|
|
raise exception.VolumeBackendAPIException(data=exception_message)
|
|
|
|
return conn
|
|
|
|
def _find_pool_in_array(self, arrayStr, poolNameInStr):
|
|
"""Find a pool based on the pool name on a given array.
|
|
|
|
:param arrayStr: the array Serial number (String)
|
|
:parampoolNameInStr: the name of the poolname (String)
|
|
:returns: foundPoolInstanceName, the CIM Instance Name of the Pool
|
|
"""
|
|
foundPoolInstanceName = None
|
|
systemNameStr = None
|
|
|
|
storageSystemInstanceName = self.utils.find_storageSystem(self.conn,
|
|
arrayStr)
|
|
|
|
vpools = self.conn.AssociatorNames(
|
|
storageSystemInstanceName,
|
|
resultClass='EMC_VirtualProvisioningPool')
|
|
|
|
for vpool in vpools:
|
|
poolinstance = vpool['InstanceID']
|
|
# Example: SYMMETRIX+000195900551+TP+Sol_Innov
|
|
poolnameStr, systemNameStr = self.utils.parse_pool_instance_id(
|
|
poolinstance)
|
|
if poolnameStr is not None and systemNameStr is not None:
|
|
if six.text_type(poolNameInStr) == six.text_type(poolnameStr):
|
|
foundPoolInstanceName = vpool
|
|
break
|
|
|
|
if foundPoolInstanceName is None:
|
|
exceptionMessage = (_("Pool %(poolNameInStr)s is not found.")
|
|
% {'poolNameInStr': poolNameInStr})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
|
|
if systemNameStr is None:
|
|
exception_message = (_("Storage system not found for pool "
|
|
"%(poolNameInStr)s.")
|
|
% {'poolNameInStr': poolNameInStr})
|
|
LOG.error(exception_message)
|
|
raise exception.VolumeBackendAPIException(data=exception_message)
|
|
|
|
LOG.debug("Pool: %(pool)s SystemName: %(systemname)s."
|
|
% {'pool': foundPoolInstanceName,
|
|
'systemname': systemNameStr})
|
|
return foundPoolInstanceName, systemNameStr
|
|
|
|
def _find_lun(self, volume):
|
|
"""Given the volume get the instance from it.
|
|
|
|
:param conn: connection the the ecom server
|
|
:param volume: volume object
|
|
:returns: foundVolumeinstance
|
|
"""
|
|
foundVolumeinstance = None
|
|
volumename = volume['name']
|
|
|
|
loc = volume['provider_location']
|
|
if self.conn is None:
|
|
self.conn = self._get_ecom_connection()
|
|
if isinstance(loc, six.string_types):
|
|
name = eval(loc)
|
|
|
|
instancename = self.utils.get_instance_name(
|
|
name['classname'], name['keybindings'])
|
|
|
|
# Handle the case where volume cannot be found.
|
|
foundVolumeinstance = self.utils.get_existing_instance(
|
|
self.conn, instancename)
|
|
|
|
if foundVolumeinstance is None:
|
|
LOG.debug("Volume %(volumename)s not found on the array."
|
|
% {'volumename': volumename})
|
|
else:
|
|
LOG.debug("Volume name: %(volumename)s Volume instance: "
|
|
"%(foundVolumeinstance)s."
|
|
% {'volumename': volumename,
|
|
'foundVolumeinstance': foundVolumeinstance})
|
|
|
|
return foundVolumeinstance
|
|
|
|
def _find_storage_sync_sv_sv(self, snapshot, volume,
|
|
waitforsync=True):
|
|
"""Find the storage synchronized name
|
|
|
|
:param snapshot: snapshot object
|
|
:param volume: volume object
|
|
:returns: foundsyncname (String)
|
|
:returns: storage_system (String)
|
|
"""
|
|
snapshotname = snapshot['name']
|
|
volumename = volume['name']
|
|
LOG.debug("Source: %(volumename)s Target: %(snapshotname)s."
|
|
% {'volumename': volumename, 'snapshotname': snapshotname})
|
|
|
|
snapshot_instance = self._find_lun(snapshot)
|
|
volume_instance = self._find_lun(volume)
|
|
storage_system = volume_instance['SystemName']
|
|
classname = 'SE_StorageSynchronized_SV_SV'
|
|
bindings = {'SyncedElement': snapshot_instance.path,
|
|
'SystemElement': volume_instance.path}
|
|
foundsyncname = self.utils.get_instance_name(classname, bindings)
|
|
|
|
if foundsyncname is None:
|
|
LOG.debug(
|
|
"Source: %(volumename)s Target: %(snapshotname)s. "
|
|
"Storage Synchronized not found. "
|
|
% {'volumename': volumename,
|
|
'snapshotname': snapshotname})
|
|
else:
|
|
LOG.debug("Storage system: %(storage_system)s "
|
|
"Storage Synchronized instance: %(sync)s."
|
|
% {'storage_system': storage_system,
|
|
'sync': foundsyncname})
|
|
# Wait for SE_StorageSynchronized_SV_SV to be fully synced
|
|
if waitforsync:
|
|
self.utils.wait_for_sync(self.conn, foundsyncname)
|
|
|
|
return foundsyncname, storage_system
|
|
|
|
def _find_initiator_names(self, connector):
|
|
foundinitiatornames = []
|
|
iscsi = 'iscsi'
|
|
fc = 'fc'
|
|
name = 'initiator name'
|
|
if self.protocol.lower() == iscsi and connector['initiator']:
|
|
foundinitiatornames.append(connector['initiator'])
|
|
elif self.protocol.lower() == fc and connector['wwpns']:
|
|
for wwn in connector['wwpns']:
|
|
foundinitiatornames.append(wwn)
|
|
name = 'world wide port names'
|
|
|
|
if foundinitiatornames is None or len(foundinitiatornames) == 0:
|
|
msg = (_("Error finding %s.") % name)
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
LOG.debug("Found %(name)s: %(initiator)s."
|
|
% {'name': name,
|
|
'initiator': foundinitiatornames})
|
|
return foundinitiatornames
|
|
|
|
def find_device_number(self, volume, connector):
|
|
"""Given the volume dict find a device number.
|
|
|
|
Find a device number that a host can see for a volume.
|
|
|
|
:param volume: the volume dict
|
|
:param connector: the connector dict
|
|
:returns: data, the data dict
|
|
|
|
"""
|
|
foundNumDeviceNumber = None
|
|
volumeName = volume['name']
|
|
volumeInstance = self._find_lun(volume)
|
|
storageSystemName = volumeInstance['SystemName']
|
|
|
|
unitnames = self.conn.ReferenceNames(
|
|
volumeInstance.path,
|
|
ResultClass='CIM_ProtocolControllerForUnit')
|
|
|
|
for unitname in unitnames:
|
|
controller = unitname['Antecedent']
|
|
classname = controller['CreationClassName']
|
|
index = classname.find('Symm_LunMaskingView')
|
|
if index > -1:
|
|
unitinstance = self.conn.GetInstance(unitname,
|
|
LocalOnly=False)
|
|
numDeviceNumber = int(unitinstance['DeviceNumber'],
|
|
16)
|
|
foundNumDeviceNumber = numDeviceNumber
|
|
controllerInstance = self.conn.GetInstance(controller,
|
|
LocalOnly=False)
|
|
propertiesList = controllerInstance.properties.items()
|
|
for properties in propertiesList:
|
|
if properties[0] == 'ElementName':
|
|
cimProperties = properties[1]
|
|
foundMaskingViewName = cimProperties.value
|
|
break
|
|
|
|
break
|
|
|
|
if foundNumDeviceNumber is None:
|
|
LOG.debug(
|
|
"Device number not found for volume "
|
|
"%(volumeName)s %(volumeInstance)s."
|
|
% {'volumeName': volumeName,
|
|
'volumeInstance': volumeInstance.path})
|
|
|
|
data = {'hostlunid': foundNumDeviceNumber,
|
|
'storagesystem': storageSystemName,
|
|
'maskingview': foundMaskingViewName}
|
|
|
|
LOG.debug("Device info: %(data)s." % {'data': data})
|
|
|
|
return data
|
|
|
|
def get_target_wwns(self, storageSystem, connector):
|
|
"""Find target WWNs.
|
|
|
|
:param storageSystem: the storage system name
|
|
:param connector: the connector dict
|
|
:returns: targetWwns, the target WWN list
|
|
"""
|
|
targetWwns = []
|
|
|
|
storageHardwareService = self.utils.find_storage_hardwareid_service(
|
|
self.conn, storageSystem)
|
|
|
|
hardwareIdInstances = self._find_storage_hardwareids(
|
|
connector, storageHardwareService)
|
|
|
|
LOG.debug(
|
|
"EMCGetTargetEndpoints: Service: %(service)s "
|
|
"Storage HardwareIDs: %(hardwareIds)s."
|
|
% {'service': storageHardwareService,
|
|
'hardwareIds': hardwareIdInstances})
|
|
|
|
for hardwareIdInstance in hardwareIdInstances:
|
|
LOG.debug("HardwareID instance is : %(hardwareIdInstance)s "
|
|
% {'hardwareIdInstance': hardwareIdInstance})
|
|
try:
|
|
rc, targetEndpoints = self.provision.get_target_endpoints(
|
|
self.conn, storageHardwareService, hardwareIdInstance)
|
|
except Exception as ex:
|
|
LOG.error(_LE("Exception: %s") % six.text_type(ex))
|
|
errorMessage = (_(
|
|
"Unable to get target endpoints for hardwareId "
|
|
"%(hardwareIdInstance)s")
|
|
% {'hardwareIdInstance': hardwareIdInstance})
|
|
LOG.error(errorMessage)
|
|
raise exception.VolumeBackendAPIException(data=errorMessage)
|
|
|
|
if targetEndpoints:
|
|
endpoints = targetEndpoints['TargetEndpoints']
|
|
|
|
LOG.debug("There are %(len)lu endpoints "
|
|
% {'len': len(endpoints)})
|
|
for targetendpoint in endpoints:
|
|
wwn = targetendpoint['Name']
|
|
# Add target wwn to the list if it is not already there
|
|
if not any(d == wwn for d in targetWwns):
|
|
targetWwns.append(wwn)
|
|
else:
|
|
LOG.error(_LE(
|
|
"Target end points do not exist for hardware Id : "
|
|
"%(hardwareIdInstance)s ")
|
|
% {'hardwareIdInstance': hardwareIdInstance})
|
|
|
|
LOG.debug("Target WWNs: : %(targetWwns)s "
|
|
% {'targetWwns': targetWwns})
|
|
|
|
return targetWwns
|
|
|
|
def _find_storage_hardwareids(
|
|
self, connector, hardwareIdManagementService):
|
|
"""Find the storage hardware ID instances.
|
|
|
|
:param connector: the connector dict
|
|
:param hardwareIdManagementService: the storage Hardware
|
|
management service
|
|
:returns: foundInstances, the list of storage hardware ID instances
|
|
"""
|
|
foundHardwareIdList = []
|
|
wwpns = self._find_initiator_names(connector)
|
|
|
|
hardwareIdInstances = (
|
|
self.utils.get_hardware_id_instances_from_array(
|
|
self.conn, hardwareIdManagementService))
|
|
for hardwareIdInstance in hardwareIdInstances:
|
|
storageId = hardwareIdInstance['StorageID']
|
|
for wwpn in wwpns:
|
|
if wwpn.lower() == storageId.lower():
|
|
# Check that the found hardwareId has not been
|
|
# deleted. If it has, we don't want to add it to the list.
|
|
instance = self.utils.get_existing_instance(
|
|
self.conn, hardwareIdInstance.path)
|
|
if instance is None:
|
|
# hardwareId doesn't exist any more. Skip it.
|
|
break
|
|
foundHardwareIdList.append(hardwareIdInstance.path)
|
|
break
|
|
|
|
LOG.debug("Storage Hardware IDs for %(wwpns)s is "
|
|
"%(foundInstances)s."
|
|
% {'wwpns': wwpns,
|
|
'foundInstances': foundHardwareIdList})
|
|
|
|
return foundHardwareIdList
|
|
|
|
def _register_config_file_from_config_group(self, configGroupName):
|
|
"""Given the config group name register the file.
|
|
|
|
:param configGroupName: the config group name
|
|
:returns: string configurationFile
|
|
"""
|
|
if configGroupName is None:
|
|
self._set_ecom_credentials(CINDER_EMC_CONFIG_FILE)
|
|
return CINDER_EMC_CONFIG_FILE
|
|
if hasattr(self.configuration, 'cinder_emc_config_file'):
|
|
configurationFile = self.configuration.cinder_emc_config_file
|
|
else:
|
|
configurationFile = (
|
|
CINDER_EMC_CONFIG_FILE_PREFIX + configGroupName +
|
|
CINDER_EMC_CONFIG_FILE_POSTFIX)
|
|
|
|
# The file saved in self.configuration may not be the correct one,
|
|
# double check
|
|
if configGroupName not in configurationFile:
|
|
configurationFile = (
|
|
CINDER_EMC_CONFIG_FILE_PREFIX + configGroupName +
|
|
CINDER_EMC_CONFIG_FILE_POSTFIX)
|
|
|
|
self._set_ecom_credentials(configurationFile)
|
|
return configurationFile
|
|
|
|
def _set_ecom_credentials(self, configurationFile):
|
|
"""Given the configuration file set the ecom credentials.
|
|
|
|
:param configurationFile: name of the file (String)
|
|
:raises: VolumeBackendAPIException
|
|
"""
|
|
if os.path.isfile(configurationFile):
|
|
LOG.debug("Configuration file : %(configurationFile)s exists"
|
|
% {'configurationFile': configurationFile})
|
|
else:
|
|
exceptionMessage = (_(
|
|
"Configuration file %(configurationFile)s does not exist ")
|
|
% {'configurationFile': configurationFile})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
|
|
ip, port = self.utils.get_ecom_server(configurationFile)
|
|
self.user, self.passwd = self.utils.get_ecom_cred(configurationFile)
|
|
self.url = 'http://' + ip + ':' + port
|
|
self.conn = self._get_ecom_connection()
|
|
|
|
def _initial_setup(self, volume):
|
|
"""Necessary setup to accumulate the relevant information.
|
|
|
|
The volume object has a host in which we can parse the
|
|
config group name. The config group name is the key to our EMC
|
|
configuration file. The emc configuration file contains pool name
|
|
and array name which are mandatory fields.
|
|
FastPolicy is optional.
|
|
StripedMetaCount is an extra spec that determines whether
|
|
the composite volume should be concatenated or striped.
|
|
|
|
:param volume: the volume Object
|
|
:returns: tuple extra spec tuple
|
|
:returns: string the configuration file
|
|
"""
|
|
try:
|
|
extraSpecs, configurationFile = (
|
|
self._set_config_file_and_get_extra_specs(volume))
|
|
poolName = None
|
|
|
|
try:
|
|
stripedMetaCount = extraSpecs[STRIPECOUNT]
|
|
extraSpecs[MEMBERCOUNT] = stripedMetaCount
|
|
extraSpecs[COMPOSITETYPE] = STRIPED
|
|
|
|
LOG.debug(
|
|
"There are: %(stripedMetaCount)s striped metas in "
|
|
"the extra specs"
|
|
% {'stripedMetaCount': stripedMetaCount})
|
|
except Exception:
|
|
memberCount = '1'
|
|
extraSpecs[MEMBERCOUNT] = memberCount
|
|
extraSpecs[COMPOSITETYPE] = CONCATENATED
|
|
LOG.debug("StripedMetaCount is not in the extra specs")
|
|
pass
|
|
|
|
poolName = self.utils.parse_pool_name_from_file(configurationFile)
|
|
if poolName is None:
|
|
exceptionMessage = (_(
|
|
"The pool cannot be null. The pool must be configured "
|
|
"either in the extra specs or in the EMC configuration "
|
|
"file corresponding to the Volume Type. "))
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(
|
|
data=exceptionMessage)
|
|
|
|
arrayName = self.utils.parse_array_name_from_file(
|
|
configurationFile)
|
|
if arrayName is None:
|
|
exceptionMessage = (_(
|
|
"The array cannot be null. The pool must be configured "
|
|
"either as a cinder extra spec for multi-backend or in "
|
|
"the EMC configuration file for the default case "))
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(
|
|
data=exceptionMessage)
|
|
|
|
# Get the FAST policy from the file this value can be None if the
|
|
# user doesn't want to associate with any FAST policy
|
|
fastPolicyName = self.utils.parse_fast_policy_name_from_file(
|
|
configurationFile)
|
|
if fastPolicyName is not None:
|
|
LOG.debug("The fast policy name is : %(fastPolicyName)s. "
|
|
% {'fastPolicyName': fastPolicyName})
|
|
|
|
extraSpecs[POOL] = poolName
|
|
extraSpecs[ARRAY] = arrayName
|
|
extraSpecs[FASTPOLICY] = fastPolicyName
|
|
|
|
LOG.debug("Pool is: %(pool)s "
|
|
"Array is: %(array)s "
|
|
"FastPolicy is: %(fastPolicy)s "
|
|
"CompositeType is: %(compositeType)s "
|
|
"MemberCount is: %(memberCount)s "
|
|
% {'pool': extraSpecs[POOL],
|
|
'array': extraSpecs[ARRAY],
|
|
'fastPolicy': extraSpecs[FASTPOLICY],
|
|
'compositeType': extraSpecs[COMPOSITETYPE],
|
|
'memberCount': extraSpecs[MEMBERCOUNT]})
|
|
|
|
except Exception:
|
|
exceptionMessage = (_(
|
|
"Unable to get configuration information necessary to create "
|
|
"a volume. Please check that there is a configuration file "
|
|
"for each config group, if multi-backend is enabled. "
|
|
"The should be in the following format "
|
|
"/etc/cinder/cinder_emc_config_<CONFIG_GROUP>.xml"))
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
|
|
return extraSpecs
|
|
|
|
def _get_pool_and_storage_system(self, extraSpecs):
|
|
"""Given the extra specs get the pool and storage system name.
|
|
|
|
:params extraSpecs: the extra spec tuple
|
|
:returns: poolInstanceName The pool instance name
|
|
:returns: String the storage system name
|
|
"""
|
|
|
|
try:
|
|
array = extraSpecs[ARRAY]
|
|
poolInstanceName, storageSystemStr = self._find_pool_in_array(
|
|
array, extraSpecs[POOL])
|
|
except Exception:
|
|
exceptionMessage = (_(
|
|
"You must supply an array in your EMC configuration file "))
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
|
|
if poolInstanceName is None or storageSystemStr is None:
|
|
exceptionMessage = (_(
|
|
"Cannot get necessary pool or storage system information "))
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
|
|
return poolInstanceName, storageSystemStr
|
|
|
|
def _populate_masking_dict(self, volume, connector, extraSpecs):
|
|
"""Get all the names of the maskingView and subComponents.
|
|
|
|
:param volume: the volume object
|
|
:param connector: the connector object
|
|
:param extraSpecs: the extra spec tuple
|
|
:returns: tuple maskingViewDict a tuple with masking view information
|
|
"""
|
|
maskingViewDict = {}
|
|
hostName = connector['host']
|
|
poolName = extraSpecs[POOL]
|
|
volumeName = volume['name']
|
|
protocol = self.utils.get_short_protocol_type(self.protocol)
|
|
|
|
shortHostName = self.utils.get_host_short_name(hostName)
|
|
|
|
volumeInstance = self._find_lun(volume)
|
|
storageSystemName = volumeInstance['SystemName']
|
|
|
|
maskingViewDict['controllerConfigService'] = (
|
|
self.utils.find_controller_configuration_service(
|
|
self.conn, storageSystemName))
|
|
maskingViewDict['sgGroupName'] = (
|
|
'OS-' + shortHostName + '-' + poolName + '-' + protocol + '-SG')
|
|
maskingViewDict['maskingViewName'] = (
|
|
'OS-' + shortHostName + '-' + poolName + '-' + protocol + '-MV')
|
|
# The portGroup is gotten from emc xml config file
|
|
maskingViewDict['pgGroupName'] = (
|
|
self.utils.parse_file_to_get_port_group_name(
|
|
self.configuration.cinder_emc_config_file))
|
|
|
|
maskingViewDict['igGroupName'] = (
|
|
'OS-' + shortHostName + '-' + protocol + '-IG')
|
|
maskingViewDict['connector'] = connector
|
|
maskingViewDict['volumeInstance'] = volumeInstance
|
|
maskingViewDict['volumeName'] = volumeName
|
|
maskingViewDict['fastPolicy'] = (
|
|
self.utils.parse_fast_policy_name_from_file(
|
|
self.configuration.cinder_emc_config_file))
|
|
maskingViewDict['storageSystemName'] = storageSystemName
|
|
|
|
return maskingViewDict
|
|
|
|
def _add_volume_to_default_storage_group_on_create(
|
|
self, volumeDict, volumeName, storageConfigService,
|
|
storageSystemName, fastPolicyName):
|
|
"""Add the volume to the default storage group for that policy.
|
|
|
|
On a create when fast policy is enable add the volume to the default
|
|
storage group for that policy. If it fails do the necessary rollback
|
|
|
|
:param volumeDict: the volume dictionary
|
|
:param volumeName: the volume name (String)
|
|
:param storageConfigService: the storage configuration service
|
|
:param storageSystemName: the storage system name (String)
|
|
:param fastPolicyName: the fast policy name (String)
|
|
:returns: tuple maskingViewDict with masking view information
|
|
"""
|
|
try:
|
|
volumeInstance = self.utils.find_volume_instance(
|
|
self.conn, volumeDict, volumeName)
|
|
controllerConfigurationService = (
|
|
self.utils.find_controller_configuration_service(
|
|
self.conn, storageSystemName))
|
|
|
|
self.fast.add_volume_to_default_storage_group_for_fast_policy(
|
|
self.conn, controllerConfigurationService, volumeInstance,
|
|
volumeName, fastPolicyName)
|
|
foundStorageGroupInstanceName = (
|
|
self.utils.get_storage_group_from_volume(
|
|
self.conn, volumeInstance.path))
|
|
|
|
if foundStorageGroupInstanceName is None:
|
|
exceptionMessage = (_(
|
|
"Error adding Volume: %(volumeName)s. "
|
|
"with instance path: %(volumeInstancePath)s. ")
|
|
% {'volumeName': volumeName,
|
|
'volumeInstancePath': volumeInstance.path})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(
|
|
data=exceptionMessage)
|
|
except Exception as e:
|
|
# rollback by deleting the volume if adding the volume to the
|
|
# default storage group were to fail
|
|
LOG.error(_LE("Exception: %s") % six.text_type(e))
|
|
errorMessage = (_(
|
|
"Rolling back %(volumeName)s by deleting it. ")
|
|
% {'volumeName': volumeName})
|
|
LOG.error(errorMessage)
|
|
self.provision.delete_volume_from_pool(
|
|
self.conn, storageConfigService, volumeInstance.path,
|
|
volumeName)
|
|
raise exception.VolumeBackendAPIException(data=errorMessage)
|
|
|
|
def _create_and_get_unbound_volume(
|
|
self, conn, storageConfigService, compositeVolumeInstanceName,
|
|
additionalSize):
|
|
"""Create an unbound volume.
|
|
|
|
Create an unbound volume so it is in the correct state to add to a
|
|
composite volume
|
|
|
|
:param conn: the connection information to the ecom server
|
|
:param storageConfigService: the storage config service instance name
|
|
:param compositeVolumeInstanceName: the composite volume instance name
|
|
:param additionalSize: the size you want to increase the volume by
|
|
:returns: volume instance modifiedCompositeVolumeInstance
|
|
"""
|
|
assocPoolInstanceName = self.utils.get_assoc_pool_from_volume(
|
|
conn, compositeVolumeInstanceName)
|
|
appendVolumeInstance = self._create_and_get_volume_instance(
|
|
conn, storageConfigService, assocPoolInstanceName, 'appendVolume',
|
|
additionalSize)
|
|
isVolumeBound = self.utils.is_volume_bound_to_pool(
|
|
conn, appendVolumeInstance)
|
|
|
|
if 'True' in isVolumeBound:
|
|
appendVolumeInstance = (
|
|
self._unbind_and_get_volume_from_storage_pool(
|
|
conn, storageConfigService, assocPoolInstanceName,
|
|
appendVolumeInstance.path, 'appendVolume'))
|
|
|
|
return appendVolumeInstance
|
|
|
|
def _create_and_get_volume_instance(
|
|
self, conn, storageConfigService, poolInstanceName,
|
|
volumeName, volumeSize):
|
|
"""Create and get a new volume.
|
|
|
|
:params conn: the connection information to the ecom server
|
|
:params storageConfigService: the storage config service instance name
|
|
:params poolInstanceName: the pool instance name
|
|
:params volumeName: the volume name
|
|
:params volumeSize: the size to create the volume
|
|
:returns: volumeInstance the volume instance
|
|
"""
|
|
volumeDict, rc = self.provision.create_volume_from_pool(
|
|
self.conn, storageConfigService, volumeName, poolInstanceName,
|
|
volumeSize)
|
|
volumeInstance = self.utils.find_volume_instance(
|
|
self.conn, volumeDict, volumeName)
|
|
return volumeInstance
|
|
|
|
def _unbind_and_get_volume_from_storage_pool(
|
|
self, conn, storageConfigService, poolInstanceName,
|
|
volumeInstanceName, volumeName):
|
|
"""Unbind a volume from a pool and return the unbound volume.
|
|
|
|
:param conn: the connection information to the ecom server
|
|
:param storageConfigService: the storage config service instance name
|
|
:param poolInstanceName: the pool instance name
|
|
:param volumeInstanceName: the volume instance name
|
|
:param volumeName: string the volumeName
|
|
:returns: unboundVolumeInstance the unbound volume instance
|
|
"""
|
|
|
|
rc, job = self.provision.unbind_volume_from_storage_pool(
|
|
conn, storageConfigService, poolInstanceName, volumeInstanceName,
|
|
volumeName)
|
|
volumeDict = self.provision.get_volume_dict_from_job(conn, job['Job'])
|
|
volumeInstance = self.utils.find_volume_instance(
|
|
self.conn, volumeDict, volumeName)
|
|
return volumeInstance
|
|
|
|
def _modify_and_get_composite_volume_instance(
|
|
self, conn, elementCompositionServiceInstanceName, volumeInstance,
|
|
appendVolumeInstanceName, volumeName, compositeType):
|
|
"""Given an existing composite volume add a new composite volume to it.
|
|
|
|
:param conn: the connection information to the ecom server
|
|
:param elementCompositionServiceInstanceName: the storage element
|
|
composition service
|
|
instance name
|
|
:param volumeInstanceName: the volume instance name
|
|
:param appendVolumeInstanceName: the appended volume instance name
|
|
:param volumeName: the volume name
|
|
:param compositeType: concatenated
|
|
:returns: int rc the return code
|
|
:returns: modifiedVolumeDict the modified volume Dict
|
|
"""
|
|
isComposite = self.utils.check_if_volume_is_composite(
|
|
self.conn, volumeInstance)
|
|
if 'True' in isComposite:
|
|
rc, job = self.provision.modify_composite_volume(
|
|
conn, elementCompositionServiceInstanceName,
|
|
volumeInstance.path, appendVolumeInstanceName)
|
|
elif 'False' in isComposite:
|
|
rc, job = self.provision.create_new_composite_volume(
|
|
conn, elementCompositionServiceInstanceName,
|
|
volumeInstance.path, appendVolumeInstanceName, compositeType)
|
|
else:
|
|
exception_message = (_(
|
|
"Unable to determine whether %(volumeName)s is "
|
|
"composite or not ")
|
|
% {'volumeName': volumeName})
|
|
LOG.error(exception_message)
|
|
raise
|
|
|
|
modifiedVolumeDict = self.provision.get_volume_dict_from_job(
|
|
conn, job['Job'])
|
|
|
|
return rc, modifiedVolumeDict
|
|
|
|
def _get_or_create_default_storage_group(
|
|
self, conn, storageSystemName, volumeDict, volumeName,
|
|
fastPolicyName):
|
|
"""Get or create a default storage group for a fast policy.
|
|
|
|
:param conn: the connection information to the ecom server
|
|
:param storageSystemName: the storage system name
|
|
:param volumeDict: the volume dictionary
|
|
:param volumeName: the volume name
|
|
:param fastPolicyName: the fast policy name
|
|
:returns: defaultStorageGroupInstanceName
|
|
"""
|
|
controllerConfigService = (
|
|
self.utils.find_controller_configuration_service(
|
|
self.conn, storageSystemName))
|
|
|
|
volumeInstance = self.utils.find_volume_instance(
|
|
self.conn, volumeDict, volumeName)
|
|
defaultStorageGroupInstanceName = (
|
|
self.fast.get_or_create_default_storage_group(
|
|
self.conn, controllerConfigService, fastPolicyName,
|
|
volumeInstance))
|
|
return defaultStorageGroupInstanceName
|
|
|
|
def _create_cloned_volume(
|
|
self, cloneVolume, sourceVolume, isSnapshot=False):
|
|
"""Create a clone volume from the source volume.
|
|
|
|
:param cloneVolume: clone volume
|
|
:param sourceVolume: source of the clone volume
|
|
:returns: cloneDict the cloned volume dictionary
|
|
"""
|
|
extraSpecs = self._initial_setup(cloneVolume)
|
|
|
|
sourceName = sourceVolume['name']
|
|
cloneName = cloneVolume['name']
|
|
|
|
LOG.info(_LI("Create a Clone from Volume: Clone "
|
|
"Volume: %(cloneName)s "
|
|
"Source Volume: %(sourceName)s")
|
|
% {'cloneName': cloneName,
|
|
'sourceName': sourceName})
|
|
|
|
self.conn = self._get_ecom_connection()
|
|
|
|
sourceInstance = self._find_lun(sourceVolume)
|
|
storageSystem = sourceInstance['SystemName']
|
|
repServiceInstanceName = self.utils.find_replication_service(
|
|
self.conn, storageSystem)
|
|
|
|
LOG.debug("Create Cloned Volume: Volume: %(cloneName)s "
|
|
"Source Volume: %(sourceName)s "
|
|
"Method: CreateElementReplica "
|
|
"ReplicationService: %(service)s ElementName: "
|
|
"%(elementname)s SyncType: 8 SourceElement: "
|
|
"%(sourceelement)s"
|
|
% {'cloneName': cloneName,
|
|
'sourceName': sourceName,
|
|
'service': repServiceInstanceName,
|
|
'elementname': cloneName,
|
|
'sourceelement': sourceInstance.path})
|
|
|
|
return self._examine_source_and_create_clone(
|
|
repServiceInstanceName, cloneVolume, sourceVolume,
|
|
sourceInstance, extraSpecs)
|
|
|
|
def _examine_source_and_create_clone(self, repServiceInstanceName,
|
|
cloneVolume, sourceVolume,
|
|
sourceInstance, extraSpecs):
|
|
"""Create a clone (v2).
|
|
|
|
:param repServiceInstanceName: the replication service
|
|
:param cloneVolume: the clone volume object
|
|
:param sourceVolume: the source volume object
|
|
:param sourceInstance: the device ID of the volume
|
|
:param fastPolicyName: the FAST policy name(if it exists)
|
|
:returns: rc
|
|
"""
|
|
# check if the source volume contains any meta devices
|
|
metaHeadInstanceName = self.utils.get_volume_meta_head(
|
|
self.conn, sourceInstance.path)
|
|
|
|
if metaHeadInstanceName is None: # simple volume
|
|
return self._create_replica_and_delete_clone_relationship(
|
|
repServiceInstanceName, cloneVolume, sourceVolume,
|
|
sourceInstance, None, extraSpecs)
|
|
else: # composite volume with meta device members
|
|
# check if the meta members' capacity
|
|
metaMemberInstanceNames = (
|
|
self.utils.get_meta_members_of_composite_volume(
|
|
self.conn, metaHeadInstanceName))
|
|
volumeCapacities = self.utils.get_meta_members_capacity_in_bit(
|
|
self.conn, metaMemberInstanceNames)
|
|
LOG.debug("Volume capacities: %(metasizes)s "
|
|
% {'metasizes': volumeCapacities})
|
|
if len(set(volumeCapacities)) == 1:
|
|
LOG.debug("Meta volume all of the same size")
|
|
return self._create_replica_and_delete_clone_relationship(
|
|
repServiceInstanceName, cloneVolume, sourceVolume,
|
|
sourceInstance, None, extraSpecs)
|
|
|
|
LOG.debug("Meta volumes are of different sizes: "
|
|
"%d different sizes." % len(set(volumeCapacities)))
|
|
|
|
baseTargetVolumeInstance = None
|
|
for volumeSizeInbits in volumeCapacities:
|
|
if baseTargetVolumeInstance is None: # Create base volume
|
|
baseVolumeName = "TargetBaseVol"
|
|
volume = {'size': int(self.utils.convert_bits_to_gbs(
|
|
volumeSizeInbits))}
|
|
rc, baseVolumeDict, storageSystemName = (
|
|
self._create_composite_volume(
|
|
volume, extraSpecs,
|
|
baseVolumeName, volumeSizeInbits))
|
|
baseTargetVolumeInstance = self.utils.find_volume_instance(
|
|
self.conn, baseVolumeDict, baseVolumeName)
|
|
LOG.info(_LI("Base target volume %(targetVol)s created. "
|
|
"Capacity in bits: %(capInBits)lu ")
|
|
% {'capInBits': volumeSizeInbits,
|
|
'targetVol': baseTargetVolumeInstance.path})
|
|
else: # create append volume
|
|
targetVolumeName = "MetaVol"
|
|
volume = {'size': int(self.utils.convert_bits_to_gbs(
|
|
volumeSizeInbits))}
|
|
storageConfigService = (
|
|
self.utils.find_storage_configuration_service(
|
|
self.conn, storageSystemName))
|
|
unboundVolumeInstance = (
|
|
self._create_and_get_unbound_volume(
|
|
self.conn, storageConfigService,
|
|
baseTargetVolumeInstance.path, volumeSizeInbits))
|
|
if unboundVolumeInstance is None:
|
|
exceptionMessage = (_(
|
|
"Error Creating unbound volume."))
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(
|
|
data=exceptionMessage)
|
|
|
|
# append the new unbound volume to the
|
|
# base target composite volume
|
|
baseTargetVolumeInstance = self.utils.find_volume_instance(
|
|
self.conn, baseVolumeDict, baseVolumeName)
|
|
elementCompositionService = (
|
|
self.utils.find_element_composition_service(
|
|
self.conn, storageSystemName))
|
|
compositeType = self.utils.get_composite_type(
|
|
extraSpecs[COMPOSITETYPE])
|
|
rc, modifiedVolumeDict = (
|
|
self._modify_and_get_composite_volume_instance(
|
|
self.conn, elementCompositionService,
|
|
baseTargetVolumeInstance,
|
|
unboundVolumeInstance.path,
|
|
targetVolumeName, compositeType))
|
|
if modifiedVolumeDict is None:
|
|
exceptionMessage = (_(
|
|
"Error appending volume %(volumename)s to "
|
|
"target base volume")
|
|
% {'volumename': targetVolumeName})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(
|
|
data=exceptionMessage)
|
|
|
|
LOG.debug("Create replica for meta members of different sizes.")
|
|
return self._create_replica_and_delete_clone_relationship(
|
|
repServiceInstanceName, cloneVolume, sourceVolume,
|
|
sourceInstance, baseTargetVolumeInstance, extraSpecs)
|
|
|
|
def _create_replica_and_delete_clone_relationship(
|
|
self, repServiceInstanceName, cloneVolume, sourceVolume,
|
|
sourceInstance, targetInstance, extraSpecs):
|
|
"""Helper function to create a clone and delete the relationship.
|
|
|
|
This function creates clone of the source volume and then
|
|
delete the clone replatinship once the copy completes.
|
|
|
|
:param repServiceInstanceName: replication service instance name
|
|
:param cloneVolume: target volume of the clone operation
|
|
:param sourceVolume: source volume of the clone operation
|
|
:param sourceInstance: instance of ECOM StorageVolume object
|
|
:param targetInstance: instance of ECOM StorageVolume object
|
|
:param extraSpecs: extraSpecs info
|
|
:returns: rc the return code, cloneDict the cloned volume dictionary
|
|
"""
|
|
sourceName = sourceVolume['name']
|
|
cloneName = cloneVolume['name']
|
|
# Create a Clone from source volume
|
|
rc, job = self.provision.create_element_replica(
|
|
self.conn, repServiceInstanceName, cloneName, sourceName,
|
|
sourceInstance, targetInstance)
|
|
|
|
cloneDict = self.provision.get_volume_dict_from_job(
|
|
self.conn, job['Job'])
|
|
|
|
cloneVolume['provider_location'] = six.text_type(cloneDict)
|
|
syncInstanceName, storageSystemName = (
|
|
self._find_storage_sync_sv_sv(cloneVolume, sourceVolume))
|
|
|
|
# Remove the Clone relationship so it can be used as a regular lun
|
|
# 8 - Detach operation
|
|
rc, job = self.provision.delete_clone_relationship(
|
|
self.conn, repServiceInstanceName, syncInstanceName, cloneName,
|
|
sourceName)
|
|
|
|
# if FAST enabled place clone volume or volume from snapshot to
|
|
# default storage group
|
|
if extraSpecs[FASTPOLICY] is not None:
|
|
LOG.debug("Adding volume: %(cloneName)s to default storage group "
|
|
"for FAST policy: %(fastPolicyName)s "
|
|
% {'cloneName': cloneName,
|
|
'fastPolicyName': extraSpecs[FASTPOLICY]})
|
|
|
|
storageConfigService = (
|
|
self.utils.find_storage_configuration_service(
|
|
self.conn, storageSystemName))
|
|
|
|
defaultStorageGroupInstanceName = (
|
|
self._get_or_create_default_storage_group(
|
|
self.conn, storageSystemName, cloneDict, cloneName,
|
|
extraSpecs[FASTPOLICY]))
|
|
if defaultStorageGroupInstanceName is None:
|
|
exceptionMessage = (_(
|
|
"Unable to create or get default storage group for FAST "
|
|
"policy: %(fastPolicyName)s. ")
|
|
% {'fastPolicyName': extraSpecs[FASTPOLICY]})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(
|
|
data=exceptionMessage)
|
|
|
|
# check if the clone/snapshot volume already part of the default sg
|
|
cloneInstance = self.utils.find_volume_instance(
|
|
self.conn, cloneDict, cloneName)
|
|
inDefaultSG = self.fast.is_volume_in_default_SG(
|
|
self.conn, cloneInstance.path)
|
|
if inDefaultSG is False:
|
|
self._add_volume_to_default_storage_group_on_create(
|
|
cloneDict, cloneName, storageConfigService,
|
|
storageSystemName, extraSpecs[FASTPOLICY])
|
|
|
|
LOG.debug("Leaving _create_cloned_volume: Volume: "
|
|
"%(cloneName)s Source Volume: %(sourceName)s "
|
|
"Return code: %(rc)lu."
|
|
% {'cloneName': cloneName,
|
|
'sourceName': sourceName,
|
|
'rc': rc})
|
|
|
|
return cloneDict
|
|
|
|
def _delete_volume(self, volume):
|
|
"""Helper function to delete the specified volume.
|
|
|
|
:param volume: volume object to be deleted
|
|
:returns: cloneDict the cloned volume dictionary
|
|
"""
|
|
|
|
volumeName = volume['name']
|
|
rc = -1
|
|
errorRet = (rc, volumeName)
|
|
|
|
extraSpecs = self._initial_setup(volume)
|
|
self.conn = self._get_ecom_connection()
|
|
|
|
volumeInstance = self._find_lun(volume)
|
|
if volumeInstance is None:
|
|
LOG.error(_LE("Volume %(name)s not found on the array. "
|
|
"No volume to delete.")
|
|
% {'name': volumeName})
|
|
return errorRet
|
|
|
|
storageSystemName = volumeInstance['SystemName']
|
|
|
|
storageConfigservice = self.utils.find_storage_configuration_service(
|
|
self.conn, storageSystemName)
|
|
controllerConfigurationService = (
|
|
self.utils.find_controller_configuration_service(
|
|
self.conn, storageSystemName))
|
|
|
|
deviceId = volumeInstance['DeviceID']
|
|
|
|
fastPolicyName = extraSpecs[FASTPOLICY]
|
|
if fastPolicyName is not None:
|
|
defaultStorageGroupInstanceName = (
|
|
self.masking.remove_device_from_default_storage_group(
|
|
self.conn, controllerConfigurationService,
|
|
volumeInstance.path, volumeName, fastPolicyName))
|
|
if defaultStorageGroupInstanceName is None:
|
|
warnMessage = (_(
|
|
"The volume: %(volumename)s. was not first part of the "
|
|
"default storage group for FAST policy %(fastPolicyName)s"
|
|
".")
|
|
% {'volumename': volumeName,
|
|
'fastPolicyName': fastPolicyName})
|
|
LOG.warn(warnMessage)
|
|
# check if it is part of another storage group
|
|
self._pre_check_for_deletion(controllerConfigurationService,
|
|
volumeInstance.path, volumeName)
|
|
|
|
else:
|
|
# check if volume is part of a storage group
|
|
self._pre_check_for_deletion(controllerConfigurationService,
|
|
volumeInstance.path, volumeName)
|
|
|
|
LOG.debug("Delete Volume: %(name)s Method: EMCReturnToStoragePool "
|
|
"ConfigServic: %(service)s TheElement: %(vol_instance)s "
|
|
"DeviceId: %(deviceId)s "
|
|
% {'service': storageConfigservice,
|
|
'name': volumeName,
|
|
'vol_instance': volumeInstance.path,
|
|
'deviceId': deviceId})
|
|
try:
|
|
rc = self.provision.delete_volume_from_pool(
|
|
self.conn, storageConfigservice, volumeInstance.path,
|
|
volumeName)
|
|
|
|
except Exception as e:
|
|
# if we cannot successfully delete the volume then we want to
|
|
# return the volume to the default storage group
|
|
if (fastPolicyName is not None and
|
|
defaultStorageGroupInstanceName is not None and
|
|
storageSystemName is not None):
|
|
assocDefaultStorageGroupName = (
|
|
self.fast
|
|
.add_volume_to_default_storage_group_for_fast_policy(
|
|
self.conn, controllerConfigurationService,
|
|
volumeInstance, volumeName, fastPolicyName))
|
|
if assocDefaultStorageGroupName is None:
|
|
errorMsg = (_LE(
|
|
"Failed to Roll back to re-add volume %(volumeName)s "
|
|
"to default storage group for fast policy "
|
|
"%(fastPolicyName)s: Please contact your sysadmin to "
|
|
"get the volume returned to the default storage group")
|
|
% {'volumeName': volumeName,
|
|
'fastPolicyName': fastPolicyName})
|
|
LOG.error(errorMsg)
|
|
|
|
LOG.error(_LE("Exception: %s") % six.text_type(e))
|
|
errorMessage = (_("Failed to delete volume %(volumeName)s")
|
|
% {'volumeName': volumeName})
|
|
LOG.error(errorMessage)
|
|
raise exception.VolumeBackendAPIException(data=errorMessage)
|
|
|
|
return (rc, volumeName)
|
|
|
|
def _pre_check_for_deletion(self, controllerConfigurationService,
|
|
volumeInstanceName, volumeName):
|
|
"""Check is volume is part of a storage group prior to delete
|
|
|
|
Log a warning if volume is part of storage group
|
|
|
|
:param controllerConfigurationService: controller configuration service
|
|
:param volumeInstanceName: volume instance name
|
|
:param volumeName: volume name (string)
|
|
"""
|
|
|
|
storageGroupInstanceName = (
|
|
self.masking.get_associated_masking_group_from_device(
|
|
self.conn, volumeInstanceName))
|
|
if storageGroupInstanceName is not None:
|
|
LOG.warn(_LW("Pre check for deletion "
|
|
"Volume: %(volumeName)s is part of a storage group "
|
|
"Attempting removal "
|
|
"from %(storageGroupInstanceName)s ")
|
|
% {'volumeName': volumeName,
|
|
'storageGroupInstanceName': storageGroupInstanceName})
|
|
self.provision.remove_device_from_storage_group(
|
|
self.conn, controllerConfigurationService,
|
|
storageGroupInstanceName,
|
|
volumeInstanceName, volumeName)
|
|
|
|
def _find_lunmasking_scsi_protocol_controller(self, storageSystemName,
|
|
connector):
|
|
"""Find LunMaskingSCSIProtocolController for the local host.
|
|
|
|
Find out how many volumes are mapped to a host
|
|
associated to the LunMaskingSCSIProtocolController.
|
|
|
|
:param storageSystemName: the storage system name
|
|
:param connector: volume object to be deleted
|
|
:returns: foundControllerInstanceName
|
|
"""
|
|
|
|
foundControllerInstanceName = None
|
|
initiators = self._find_initiator_names(connector)
|
|
|
|
storageSystemInstanceName = self.utils.find_storageSystem(
|
|
self.conn, storageSystemName)
|
|
controllerInstanceNames = self.conn.AssociatorNames(
|
|
storageSystemInstanceName,
|
|
ResultClass='EMC_LunMaskingSCSIProtocolController')
|
|
|
|
for controllerInstanceName in controllerInstanceNames:
|
|
try:
|
|
# This is a check to see if the controller has
|
|
# been deleted.
|
|
self.conn.GetInstance(controllerInstanceName)
|
|
storageHardwareIdInstances = self.conn.Associators(
|
|
controllerInstanceName,
|
|
ResultClass='EMC_StorageHardwareID')
|
|
|
|
for storageHardwareIdInstance in storageHardwareIdInstances:
|
|
# If EMC_StorageHardwareID matches the initiator, we
|
|
# found the existing EMC_LunMaskingSCSIProtocolController.
|
|
hardwareid = storageHardwareIdInstance['StorageID']
|
|
for initiator in initiators:
|
|
if hardwareid.lower() == initiator.lower():
|
|
# This is a check to see if the controller
|
|
# has been deleted.
|
|
instance = self.utils.get_existing_instance(
|
|
self.conn, controllerInstanceName)
|
|
if instance is None:
|
|
# Skip this controller as it doesn't exist
|
|
# any more
|
|
pass
|
|
else:
|
|
foundControllerInstanceName = (
|
|
controllerInstanceName)
|
|
break
|
|
|
|
if foundControllerInstanceName is not None:
|
|
break
|
|
except pywbem.cim_operations.CIMError as arg:
|
|
instance = self.utils.process_exception_args(
|
|
arg, controllerInstanceName)
|
|
if instance is None:
|
|
# Skip this controller as it doesn't exist any more.
|
|
pass
|
|
|
|
if foundControllerInstanceName is not None:
|
|
break
|
|
|
|
LOG.debug("LunMaskingSCSIProtocolController for storage system "
|
|
"%(storage_system)s and initiator %(initiator)s is "
|
|
"%(ctrl)s."
|
|
% {'storage_system': storageSystemName,
|
|
'initiator': initiators,
|
|
'ctrl': foundControllerInstanceName})
|
|
return foundControllerInstanceName
|
|
|
|
def get_num_volumes_mapped(self, volume, connector):
|
|
"""Returns how many volumes are in the same zone as the connector.
|
|
|
|
Find out how many volumes are mapped to a host
|
|
associated to the LunMaskingSCSIProtocolController
|
|
|
|
:param volume: volume object to be deleted
|
|
:param connector: volume object to be deleted
|
|
:returns: int numVolumesMapped
|
|
"""
|
|
|
|
volumename = volume['name']
|
|
vol_instance = self._find_lun(volume)
|
|
if vol_instance is None:
|
|
msg = (_("Volume %(name)s not found on the array. "
|
|
"Cannot determine if there are volumes mapped."),
|
|
{'name': volumename})
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
storage_system = vol_instance['SystemName']
|
|
|
|
ctrl = self._find_lunmasking_scsi_protocol_controller(
|
|
storage_system,
|
|
connector)
|
|
|
|
LOG.debug("LunMaskingSCSIProtocolController for storage system "
|
|
"%(storage)s and %(connector)s is %(ctrl)s."
|
|
% {'storage': storage_system,
|
|
'connector': connector,
|
|
'ctrl': ctrl})
|
|
|
|
# return 0 if masking view does not exist
|
|
if ctrl is None:
|
|
return 0
|
|
|
|
associators = self.conn.Associators(
|
|
ctrl,
|
|
ResultClass='EMC_StorageVolume')
|
|
|
|
numVolumesMapped = len(associators)
|
|
|
|
LOG.debug("Found %(numVolumesMapped)d volumes on storage system "
|
|
"%(storage)s mapped to %(connector)s."
|
|
% {'numVolumesMapped': numVolumesMapped,
|
|
'storage': storage_system,
|
|
'connector': connector})
|
|
|
|
return numVolumesMapped
|
|
|
|
def get_target_wwns_from_masking_view(
|
|
self, storageSystem, volume, connector):
|
|
"""Find target WWNs via the masking view.
|
|
|
|
:param storageSystem: the storage system name
|
|
:param volume: volume to be attached
|
|
:param connector: the connector dict
|
|
:returns: targetWwns, the target WWN list
|
|
"""
|
|
targetWwns = []
|
|
mvInstanceName = self.get_masking_view_by_volume(volume)
|
|
targetWwns = self.masking.get_target_wwns(self.conn, mvInstanceName)
|
|
LOG.info(_LI("Target wwns in masking view %(maskingView)s: "
|
|
"%(targetWwns)s"),
|
|
{'maskingView': mvInstanceName,
|
|
'targetWwns': str(targetWwns)})
|
|
return targetWwns
|
|
|
|
def get_port_group_from_masking_view(self, maskingViewInstanceName):
|
|
"""Find port group that is part of a masking view.
|
|
|
|
:param maskingViewInstanceName: the owning masking view
|
|
:returns: port group instance name
|
|
"""
|
|
return self.masking.get_port_group_from_masking_view(
|
|
self.conn, maskingViewInstanceName)
|
|
|
|
def get_masking_view_by_volume(self, volume):
|
|
"""Given volume, retrieve the masking view instance name.
|
|
|
|
:param volume: the volume
|
|
:param mvInstanceName: masking view instance name
|
|
:returns maskingviewInstanceName
|
|
"""
|
|
LOG.debug("Finding Masking View for volume %(volume)s"
|
|
% {'volume': volume})
|
|
volumeInstance = self._find_lun(volume)
|
|
return self.masking.get_masking_view_by_volume(
|
|
self.conn, volumeInstance)
|
|
|
|
def get_masking_views_by_port_group(self, portGroupInstanceName):
|
|
"""Given port group, retrieve the masking view instance name.
|
|
|
|
:param : the volume
|
|
:param mvInstanceName: masking view instance name
|
|
:returns: maksingViewInstanceNames
|
|
"""
|
|
LOG.debug("Finding Masking Views for port group %(pg)s"
|
|
% {'pg': portGroupInstanceName})
|
|
return self.masking.get_masking_views_by_port_group(
|
|
self.conn, portGroupInstanceName)
|
|
|
|
def _create_composite_volume(
|
|
self, volume, extraSpecs, volumeName, volumeSize):
|
|
"""Create a composite volume.
|
|
|
|
:param volume: the volume object
|
|
:param extraSpecs:
|
|
:param volumeName:
|
|
:param volumeSize:
|
|
:returns:
|
|
"""
|
|
memberCount, errorDesc = self.utils.determine_member_count(
|
|
volume['size'], extraSpecs[MEMBERCOUNT], extraSpecs[COMPOSITETYPE])
|
|
if errorDesc is not None:
|
|
exceptionMessage = (_("The striped meta count of %(memberCount)s "
|
|
"is too small for volume: %(volumeName)s. "
|
|
"with size %(volumeSize)s ")
|
|
% {'memberCount': memberCount,
|
|
'volumeName': volumeName,
|
|
'volumeSize': volume['size']})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(data=exceptionMessage)
|
|
|
|
poolInstanceName, storageSystemName = (
|
|
self._get_pool_and_storage_system(extraSpecs))
|
|
|
|
LOG.debug("Create Volume: %(volume)s Pool: %(pool)s "
|
|
"Storage System: %(storageSystem)s "
|
|
"Size: %(size)lu "
|
|
% {'volume': volumeName,
|
|
'pool': poolInstanceName,
|
|
'storageSystem': storageSystemName,
|
|
'size': volumeSize})
|
|
|
|
elementCompositionService = (
|
|
self.utils.find_element_composition_service(self.conn,
|
|
storageSystemName))
|
|
|
|
storageConfigService = self.utils.find_storage_configuration_service(
|
|
self.conn, storageSystemName)
|
|
|
|
# If FAST is intended to be used we must first check that the pool
|
|
# is associated with the correct storage tier
|
|
if extraSpecs[FASTPOLICY] is not None:
|
|
foundPoolInstanceName = self.fast.get_pool_associated_to_policy(
|
|
self.conn, extraSpecs[FASTPOLICY], extraSpecs[ARRAY],
|
|
storageConfigService, poolInstanceName)
|
|
if foundPoolInstanceName is None:
|
|
exceptionMessage = (_("Pool: %(poolName)s. "
|
|
"is not associated to storage tier for "
|
|
"fast policy %(fastPolicy)s.")
|
|
% {'poolName': extraSpecs[POOL],
|
|
'fastPolicy': extraSpecs[FASTPOLICY]})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(
|
|
data=exceptionMessage)
|
|
|
|
compositeType = self.utils.get_composite_type(
|
|
extraSpecs[COMPOSITETYPE])
|
|
|
|
volumeDict, rc = self.provision.create_composite_volume(
|
|
self.conn, elementCompositionService, volumeSize, volumeName,
|
|
poolInstanceName, compositeType, memberCount)
|
|
|
|
# Now that we have already checked that the pool is associated with
|
|
# the correct storage tier and the volume was successfully created
|
|
# add the volume to the default storage group created for
|
|
# volumes in pools associated with this fast policy
|
|
if extraSpecs[FASTPOLICY]:
|
|
LOG.info(_LI("Adding volume: %(volumeName)s to default storage "
|
|
"group for FAST policy: %(fastPolicyName)s ")
|
|
% {'volumeName': volumeName,
|
|
'fastPolicyName': extraSpecs[FASTPOLICY]})
|
|
defaultStorageGroupInstanceName = (
|
|
self._get_or_create_default_storage_group(
|
|
self.conn, storageSystemName, volumeDict,
|
|
volumeName, extraSpecs[FASTPOLICY]))
|
|
if not defaultStorageGroupInstanceName:
|
|
exceptionMessage = (_(
|
|
"Unable to create or get default storage group for "
|
|
"FAST policy: %(fastPolicyName)s. ")
|
|
% {'fastPolicyName': extraSpecs[FASTPOLICY]})
|
|
LOG.error(exceptionMessage)
|
|
raise exception.VolumeBackendAPIException(
|
|
data=exceptionMessage)
|
|
|
|
self._add_volume_to_default_storage_group_on_create(
|
|
volumeDict, volumeName, storageConfigService,
|
|
storageSystemName, extraSpecs[FASTPOLICY])
|
|
return rc, volumeDict, storageSystemName
|