VMAX driver - Detaching volumes if part of two or more MVs
When Live migration is used extensively there can be scenarios where a regular attached volume can belong to two or more Masking Views. Because of this, we did not remove the volume from the storage group, which is not typical behaviour. In this fix we use a temporary file to determine if a terminate_connection is a regular detach or a part of the live migration process. Change-Id: Ide38fa21d65859a5516c577a9983124d998a2e95 Closes-Bug: #1684595
This commit is contained in:
parent
7b304ce4aa
commit
9d2466bb29
|
@ -16,6 +16,7 @@
|
|||
import ast
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import uuid
|
||||
|
@ -3431,6 +3432,9 @@ class VMAXISCSIDriverNoFastTestCase(test.TestCase):
|
|||
self.driver.delete_volume,
|
||||
self.data.failed_delete_vol)
|
||||
|
||||
@mock.patch.object(
|
||||
utils.VMAXUtils,
|
||||
'insert_live_migration_record')
|
||||
@mock.patch.object(
|
||||
common.VMAXCommon,
|
||||
'_is_same_host',
|
||||
|
@ -3451,7 +3455,7 @@ class VMAXISCSIDriverNoFastTestCase(test.TestCase):
|
|||
return_value={'volume_backend_name': 'ISCSINoFAST'})
|
||||
def test_already_mapped_no_fast_success(
|
||||
self, _mock_volume_type, mock_wrap_group, mock_wrap_device,
|
||||
mock_is_same_host):
|
||||
mock_is_same_host, mock_rec):
|
||||
self.driver.common._get_correct_port_group = mock.Mock(
|
||||
return_value=self.data.port_group)
|
||||
self.driver.initialize_connection(self.data.test_volume,
|
||||
|
@ -3481,6 +3485,9 @@ class VMAXISCSIDriverNoFastTestCase(test.TestCase):
|
|||
self.driver.initialize_connection(self.data.test_volume,
|
||||
self.data.connector)
|
||||
|
||||
@mock.patch.object(
|
||||
utils.VMAXUtils,
|
||||
'insert_live_migration_record')
|
||||
@mock.patch.object(
|
||||
common.VMAXCommon,
|
||||
'_get_port_group_from_source',
|
||||
|
@ -3518,7 +3525,8 @@ class VMAXISCSIDriverNoFastTestCase(test.TestCase):
|
|||
mock_device,
|
||||
mock_same_host,
|
||||
mock_sg_from_mv,
|
||||
mock_pg_from_mv):
|
||||
mock_pg_from_mv,
|
||||
mock_rec):
|
||||
extraSpecs = self.data.extra_specs
|
||||
rollback_dict = self.driver.common._populate_masking_dict(
|
||||
self.data.test_volume, self.data.connector, extraSpecs)
|
||||
|
@ -3528,6 +3536,9 @@ class VMAXISCSIDriverNoFastTestCase(test.TestCase):
|
|||
self.driver.initialize_connection(self.data.test_volume,
|
||||
self.data.connector)
|
||||
|
||||
@mock.patch.object(
|
||||
utils.VMAXUtils,
|
||||
'insert_live_migration_record')
|
||||
@mock.patch.object(
|
||||
masking.VMAXMasking,
|
||||
'_get_initiator_group_from_masking_view',
|
||||
|
@ -3550,7 +3561,7 @@ class VMAXISCSIDriverNoFastTestCase(test.TestCase):
|
|||
return_value={'volume_backend_name': 'ISCSINoFAST'})
|
||||
def test_map_existing_masking_view_no_fast_success(
|
||||
self, _mock_volume_type, mock_wrap_group, mock_storage_group,
|
||||
mock_initiator_group, mock_ig_from_mv):
|
||||
mock_initiator_group, mock_ig_from_mv, mock_rec):
|
||||
self.driver.initialize_connection(self.data.test_volume,
|
||||
self.data.connector)
|
||||
|
||||
|
@ -4447,6 +4458,9 @@ class VMAXISCSIDriverFastTestCase(test.TestCase):
|
|||
self.driver.delete_volume,
|
||||
self.data.failed_delete_vol)
|
||||
|
||||
@mock.patch.object(
|
||||
utils.VMAXUtils,
|
||||
'insert_live_migration_record')
|
||||
@mock.patch.object(
|
||||
common.VMAXCommon,
|
||||
'_is_same_host',
|
||||
|
@ -4467,7 +4481,7 @@ class VMAXISCSIDriverFastTestCase(test.TestCase):
|
|||
return_value={'volume_backend_name': 'ISCSIFAST'})
|
||||
def test_already_mapped_fast_success(
|
||||
self, _mock_volume_type, mock_wrap_group, mock_wrap_device,
|
||||
mock_is_same_host):
|
||||
mock_is_same_host, mock_rec):
|
||||
self.driver.common._get_correct_port_group = mock.Mock(
|
||||
return_value=self.data.port_group)
|
||||
self.driver.initialize_connection(self.data.test_volume,
|
||||
|
@ -5068,6 +5082,9 @@ class VMAXFCDriverNoFastTestCase(test.TestCase):
|
|||
self.driver.delete_volume,
|
||||
self.data.failed_delete_vol)
|
||||
|
||||
@mock.patch.object(
|
||||
utils.VMAXUtils,
|
||||
'insert_live_migration_record')
|
||||
@mock.patch.object(
|
||||
common.VMAXCommon,
|
||||
'_is_same_host',
|
||||
|
@ -5082,7 +5099,8 @@ class VMAXFCDriverNoFastTestCase(test.TestCase):
|
|||
return_value={'volume_backend_name': 'FCNoFAST',
|
||||
'FASTPOLICY': 'FC_GOLD1'})
|
||||
def test_map_lookup_service_no_fast_success(
|
||||
self, _mock_volume_type, mock_maskingview, mock_is_same_host):
|
||||
self, _mock_volume_type, mock_maskingview, mock_is_same_host,
|
||||
mock_rec):
|
||||
self.data.test_volume['volume_name'] = "vmax-1234567"
|
||||
common = self.driver.common
|
||||
common.get_target_wwns_from_masking_view = mock.Mock(
|
||||
|
@ -5597,6 +5615,9 @@ class VMAXFCDriverFastTestCase(test.TestCase):
|
|||
self.driver.delete_volume,
|
||||
self.data.failed_delete_vol)
|
||||
|
||||
@mock.patch.object(
|
||||
utils.VMAXUtils,
|
||||
'insert_live_migration_record')
|
||||
@mock.patch.object(
|
||||
common.VMAXCommon,
|
||||
'_is_same_host',
|
||||
|
@ -5611,7 +5632,7 @@ class VMAXFCDriverFastTestCase(test.TestCase):
|
|||
return_value={'volume_backend_name': 'FCFAST',
|
||||
'FASTPOLICY': 'FC_GOLD1'})
|
||||
def test_map_fast_success(self, _mock_volume_type, mock_maskingview,
|
||||
mock_is_same_host):
|
||||
mock_is_same_host, mock_rec):
|
||||
common = self.driver.common
|
||||
common.get_target_wwns_list = mock.Mock(
|
||||
return_value=VMAXCommonData.target_wwns)
|
||||
|
@ -6660,6 +6681,9 @@ class EMCV3DriverTestCase(test.TestCase):
|
|||
self.data.test_ctxt, self.data.test_CG,
|
||||
add_volumes, remove_volumes)
|
||||
|
||||
@mock.patch.object(
|
||||
utils.VMAXUtils,
|
||||
'insert_live_migration_record')
|
||||
@mock.patch.object(
|
||||
utils.VMAXUtils,
|
||||
'get_volume_element_name',
|
||||
|
@ -6678,7 +6702,7 @@ class EMCV3DriverTestCase(test.TestCase):
|
|||
return_value={'volume_backend_name': 'V3_BE'})
|
||||
def test_map_v3_success(
|
||||
self, _mock_volume_type, mock_maskingview, mock_is_same_host,
|
||||
mock_element_name):
|
||||
mock_element_name, mock_rec):
|
||||
common = self.driver.common
|
||||
common.get_target_wwns_list = mock.Mock(
|
||||
return_value=VMAXCommonData.target_wwns)
|
||||
|
@ -8985,6 +9009,59 @@ class VMAXUtilsTest(test.TestCase):
|
|||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
utils.get_array_and_device_id, volume, external_ref)
|
||||
|
||||
@mock.patch('builtins.open' if sys.version_info >= (3,)
|
||||
else '__builtin__.open')
|
||||
def test_insert_live_migration_record(self, mock_open):
|
||||
volume = {'id': '12345678-87654321'}
|
||||
tempdir = tempfile.mkdtemp()
|
||||
utils.LIVE_MIGRATION_FILE = (
|
||||
tempdir + '/livemigrationarray')
|
||||
lm_file_name = ("%(prefix)s-%(volid)s"
|
||||
% {'prefix': utils.LIVE_MIGRATION_FILE,
|
||||
'volid': volume['id'][:8]})
|
||||
self.driver.utils.insert_live_migration_record(volume)
|
||||
mock_open.assert_called_once_with(lm_file_name, "w")
|
||||
self.driver.utils.delete_live_migration_record(volume)
|
||||
shutil.rmtree(tempdir)
|
||||
|
||||
def test_delete_live_migration_record(self):
|
||||
volume = {'id': '12345678-87654321'}
|
||||
tempdir = tempfile.mkdtemp()
|
||||
utils.LIVE_MIGRATION_FILE = (
|
||||
tempdir + '/livemigrationarray')
|
||||
lm_file_name = ("%(prefix)s-%(volid)s"
|
||||
% {'prefix': utils.LIVE_MIGRATION_FILE,
|
||||
'volid': volume['id'][:8]})
|
||||
m = mock.mock_open()
|
||||
with mock.patch('{}.open'.format(__name__), m, create=True):
|
||||
with open(lm_file_name, "w") as f:
|
||||
f.write('live migration details')
|
||||
self.driver.utils.insert_live_migration_record(volume)
|
||||
self.driver.utils.delete_live_migration_record(volume)
|
||||
m.assert_called_once_with(lm_file_name, "w")
|
||||
shutil.rmtree(tempdir)
|
||||
|
||||
def test_get_live_migration_record(self):
|
||||
volume = {'id': '12345678-87654321'}
|
||||
tempdir = tempfile.mkdtemp()
|
||||
utils.LIVE_MIGRATION_FILE = (
|
||||
tempdir + '/livemigrationarray')
|
||||
lm_file_name = ("%(prefix)s-%(volid)s"
|
||||
% {'prefix': utils.LIVE_MIGRATION_FILE,
|
||||
'volid': volume['id'][:8]})
|
||||
self.driver.utils.insert_live_migration_record(volume)
|
||||
record = self.driver.utils.get_live_migration_record(volume)
|
||||
self.assertEqual(volume['id'], record[0])
|
||||
os.remove(lm_file_name)
|
||||
shutil.rmtree(tempdir)
|
||||
|
||||
def test_get_live_migration_file_name(self):
|
||||
volume = {'id': '12345678-87654321'}
|
||||
lm_live_migration = self.driver.utils.get_live_migration_file_name(
|
||||
volume)
|
||||
self.assertIn('/livemigrationarray-12345678', lm_live_migration)
|
||||
self.assertIn('/tmp/', lm_live_migration)
|
||||
|
||||
|
||||
class VMAXCommonTest(test.TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -527,7 +527,12 @@ class VMAXCommon(object):
|
|||
vol_instance = self._find_lun(volume)
|
||||
storage_system = vol_instance['SystemName']
|
||||
|
||||
if self._is_volume_multiple_masking_views(vol_instance):
|
||||
livemigrationrecord = self.utils.get_live_migration_record(volume)
|
||||
if livemigrationrecord:
|
||||
self.utils.delete_live_migration_record(volume)
|
||||
|
||||
if livemigrationrecord and self._is_volume_multiple_masking_views(
|
||||
vol_instance):
|
||||
return
|
||||
|
||||
configservice = self.utils.find_controller_configuration_service(
|
||||
|
@ -613,12 +618,14 @@ class VMAXCommon(object):
|
|||
"The device number is %(deviceNumber)s.",
|
||||
{'volume': volumeName,
|
||||
'deviceNumber': deviceNumber})
|
||||
self.utils.insert_live_migration_record(volume)
|
||||
# Special case, we still need to get the iscsi ip address.
|
||||
portGroupName = (
|
||||
self._get_correct_port_group(
|
||||
deviceInfoDict, maskingViewDict['storageSystemName']))
|
||||
else:
|
||||
if isLiveMigration:
|
||||
self.utils.insert_live_migration_record(volume)
|
||||
maskingViewDict['storageGroupInstanceName'] = (
|
||||
self._get_storage_group_from_source(sourceInfoDict))
|
||||
maskingViewDict['portGroupInstanceName'] = (
|
||||
|
@ -676,6 +683,9 @@ class VMAXCommon(object):
|
|||
(rollbackDict['isV3'] is not None)):
|
||||
(self.masking._check_if_rollback_action_for_masking_required(
|
||||
self.conn, rollbackDict))
|
||||
livemigrationrecord = self.utils.get_live_migration_record(volume)
|
||||
if livemigrationrecord:
|
||||
self.utils.delete_live_migration_record(volume)
|
||||
exception_message = (_("Error Attaching volume %(vol)s.")
|
||||
% {'vol': volumeName})
|
||||
raise exception.VolumeBackendAPIException(
|
||||
|
@ -1945,14 +1955,16 @@ class VMAXCommon(object):
|
|||
"""
|
||||
maskedvols = []
|
||||
data = {}
|
||||
isLiveMigration = False
|
||||
source_data = {}
|
||||
foundController = None
|
||||
foundNumDeviceNumber = None
|
||||
foundMaskingViewName = None
|
||||
volumeName = volume['name']
|
||||
volumeInstance = self._find_lun(volume)
|
||||
storageSystemName = volumeInstance['SystemName']
|
||||
isLiveMigration = False
|
||||
source_data = {}
|
||||
if not volumeInstance:
|
||||
return data, isLiveMigration, source_data
|
||||
|
||||
unitnames = self.conn.ReferenceNames(
|
||||
volumeInstance.path,
|
||||
|
@ -1965,7 +1977,15 @@ class VMAXCommon(object):
|
|||
if index > -1:
|
||||
unitinstance = self.conn.GetInstance(unitname,
|
||||
LocalOnly=False)
|
||||
numDeviceNumber = int(unitinstance['DeviceNumber'], 16)
|
||||
if unitinstance['DeviceNumber']:
|
||||
numDeviceNumber = int(unitinstance['DeviceNumber'], 16)
|
||||
else:
|
||||
LOG.debug(
|
||||
"Device number not found for volume "
|
||||
"%(volumeName)s %(volumeInstance)s.",
|
||||
{'volumeName': volumeName,
|
||||
'volumeInstance': volumeInstance.path})
|
||||
break
|
||||
foundNumDeviceNumber = numDeviceNumber
|
||||
foundController = controller
|
||||
controllerInstance = self.conn.GetInstance(controller,
|
||||
|
|
|
@ -2110,10 +2110,16 @@ class VMAXMasking(object):
|
|||
self._last_volume_delete_masking_view(
|
||||
conn, controllerConfigService, mvInstanceName,
|
||||
maskingViewName, extraSpecs)
|
||||
self._last_volume_delete_initiator_group(
|
||||
conn, controllerConfigService,
|
||||
initiatorGroupInstanceName, extraSpecs, host)
|
||||
initiatorGroupInstance = conn.GetInstance(initiatorGroupInstanceName)
|
||||
if initiatorGroupInstance:
|
||||
initiatorGroupName = initiatorGroupInstance['ElementName']
|
||||
|
||||
@coordination.synchronized('emc-ig-{initiatorGroupName}')
|
||||
def inner_do_delete_initiator_group(initiatorGroupName):
|
||||
self._last_volume_delete_initiator_group(
|
||||
conn, controllerConfigService,
|
||||
initiatorGroupInstanceName, extraSpecs, host)
|
||||
inner_do_delete_initiator_group(initiatorGroupName)
|
||||
if not isV3:
|
||||
isTieringPolicySupported, tierPolicyServiceInstanceName = (
|
||||
self._get_tiering_info(conn, storageSystemInstanceName,
|
||||
|
@ -2672,15 +2678,19 @@ class VMAXMasking(object):
|
|||
:param hardwareIdManagementService - hardware id management service
|
||||
:param hardwareIdPath - The path of the initiator object
|
||||
"""
|
||||
ret = conn.InvokeMethod('DeleteStorageHardwareID',
|
||||
hardwareIdManagementService,
|
||||
HardwareID = hardwareIdPath)
|
||||
ret = -1
|
||||
try:
|
||||
ret = conn.InvokeMethod('DeleteStorageHardwareID',
|
||||
hardwareIdManagementService,
|
||||
HardwareID = hardwareIdPath)
|
||||
except Exception:
|
||||
pass
|
||||
if ret == 0:
|
||||
LOG.debug("Deletion of initiator path %(hardwareIdPath)s "
|
||||
"is successful.", {'hardwareIdPath': hardwareIdPath})
|
||||
else:
|
||||
LOG.warning("Deletion of initiator path %(hardwareIdPath)s "
|
||||
"is failed.", {'hardwareIdPath': hardwareIdPath})
|
||||
LOG.debug("Deletion of initiator path %(hardwareIdPath)s "
|
||||
"failed.", {'hardwareIdPath': hardwareIdPath})
|
||||
|
||||
def _delete_initiators_from_initiator_group(self, conn,
|
||||
controllerConfigService,
|
||||
|
@ -2745,7 +2755,6 @@ class VMAXMasking(object):
|
|||
"OS-%(shortHostName)s-%(protocol)s-IG"
|
||||
% {'shortHostName': host,
|
||||
'protocol': protocol}))
|
||||
|
||||
if initiatorGroupName == defaultInitiatorGroupName:
|
||||
maskingViewInstanceNames = (
|
||||
self.get_masking_views_by_initiator_group(
|
||||
|
|
|
@ -16,12 +16,15 @@
|
|||
import ast
|
||||
import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import tempfile
|
||||
import time
|
||||
from xml.dom import minidom
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_service import loopingcall
|
||||
from oslo_utils import units
|
||||
import six
|
||||
|
@ -53,7 +56,7 @@ EMC_ROOT = 'root/emc'
|
|||
CONCATENATED = 'concatenated'
|
||||
CINDER_EMC_CONFIG_FILE_PREFIX = '/etc/cinder/cinder_emc_config_'
|
||||
CINDER_EMC_CONFIG_FILE_POSTFIX = '.xml'
|
||||
LIVE_MIGRATION_FILE = '/etc/cinder/livemigrationarray'
|
||||
LIVE_MIGRATION_FILE = tempfile.gettempdir() + '/livemigrationarray'
|
||||
ISCSI = 'iscsi'
|
||||
FC = 'fc'
|
||||
JOB_RETRIES = 60
|
||||
|
@ -2978,3 +2981,68 @@ class VMAXUtils(object):
|
|||
default_dict[INTERVAL] = INTERVAL_10_SEC
|
||||
default_dict[RETRIES] = JOB_RETRIES
|
||||
return default_dict
|
||||
|
||||
def insert_live_migration_record(self, volume):
|
||||
"""Insert a record of live migration destination into a temporary file
|
||||
|
||||
:param volume: the volume dictionary
|
||||
"""
|
||||
lm_file_name = self.get_live_migration_file_name(volume)
|
||||
live_migration_details = self.get_live_migration_record(volume)
|
||||
if live_migration_details:
|
||||
return
|
||||
else:
|
||||
live_migration_details = {volume['id']: [volume['id']]}
|
||||
try:
|
||||
with open(lm_file_name, "w") as f:
|
||||
jsonutils.dump(live_migration_details, f)
|
||||
except Exception:
|
||||
exceptionMessage = (_(
|
||||
"Error in processing live migration file."))
|
||||
LOG.exception(exceptionMessage)
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=exceptionMessage)
|
||||
|
||||
def delete_live_migration_record(self, volume):
|
||||
"""Delete record of live migration
|
||||
|
||||
Delete record of live migration destination from file and if
|
||||
after deletion of record, delete file if empty.
|
||||
|
||||
:param volume: the volume dictionary
|
||||
"""
|
||||
lm_file_name = self.get_live_migration_file_name(volume)
|
||||
live_migration_details = self.get_live_migration_record(volume)
|
||||
if live_migration_details:
|
||||
if volume['id'] in live_migration_details:
|
||||
os.remove(lm_file_name)
|
||||
|
||||
def get_live_migration_record(self, volume):
|
||||
"""get record of live migration destination from a temporary file
|
||||
|
||||
:param volume: the volume dictionary
|
||||
:returns: returns a single record
|
||||
"""
|
||||
returned_record = None
|
||||
lm_file_name = self.get_live_migration_file_name(volume)
|
||||
if os.path.isfile(lm_file_name):
|
||||
with open(lm_file_name, "rb") as f:
|
||||
live_migration_details = jsonutils.load(f)
|
||||
if volume['id'] in live_migration_details:
|
||||
returned_record = live_migration_details[volume['id']]
|
||||
else:
|
||||
LOG.debug("%(Volume)s doesn't exist in live migration "
|
||||
"record.",
|
||||
{'Volume': volume['id']})
|
||||
return returned_record
|
||||
|
||||
def get_live_migration_file_name(self, volume):
|
||||
"""get name of temporary live migration file
|
||||
|
||||
:param volume: the volume dictionary
|
||||
:returns: returns file name
|
||||
"""
|
||||
lm_file_name = ("%(prefix)s-%(volid)s"
|
||||
% {'prefix': LIVE_MIGRATION_FILE,
|
||||
'volid': volume['id'][:8]})
|
||||
return lm_file_name
|
||||
|
|
Loading…
Reference in New Issue