VMAX Driver - Live Migration for VMAX3
Enhancing live migration to add support for VMAX3. DocImpact Change-Id: I806ec74c244329415f2ec06f95741869b3acff00 Closes-Bug: #1587967
This commit is contained in:
parent
175667cf4e
commit
3f3a0c8639
@ -15,6 +15,7 @@
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
@ -2145,6 +2146,12 @@ class EMCVMAXISCSIDriverNoFastTestCase(test.TestCase):
|
||||
self.driver.common.find_device_number(self.data.test_volume_v2,
|
||||
host))
|
||||
self.assertEqual('OS-fakehost-MV', data['maskingview'])
|
||||
|
||||
@mock.patch.object(
|
||||
FakeEcomConnection,
|
||||
'ReferenceNames',
|
||||
return_value=[])
|
||||
def test_find_device_number_false(self, mock_ref_name):
|
||||
host = 'bogushost'
|
||||
data = (
|
||||
self.driver.common.find_device_number(self.data.test_volume_v2,
|
||||
@ -2661,6 +2668,33 @@ class EMCVMAXISCSIDriverNoFastTestCase(test.TestCase):
|
||||
conn, controllerConfigService, volumeInstance,
|
||||
volumeName, extraSpecs)
|
||||
|
||||
@mock.patch.object(
|
||||
emc_vmax_masking.EMCVMAXMasking,
|
||||
'get_associated_masking_groups_from_device',
|
||||
return_value=EMCVMAXCommonData.storagegroups)
|
||||
@mock.patch.object(
|
||||
emc_vmax_utils.EMCVMAXUtils,
|
||||
'get_existing_instance',
|
||||
return_value=None)
|
||||
def test_remove_and_reset_members_v3(self, mock_inst, mock_sg):
|
||||
extraSpecs = {'volume_backend_name': 'V3_BE',
|
||||
'isV3': True,
|
||||
'pool': 'SRP_1',
|
||||
'workload': 'DSS',
|
||||
'slo': 'Bronze'}
|
||||
conn = self.fake_ecom_connection()
|
||||
controllerConfigService = (
|
||||
self.driver.utils.find_controller_configuration_service(
|
||||
conn, self.data.storage_system))
|
||||
volumeInstanceName = (
|
||||
conn.EnumerateInstanceNames("EMC_StorageVolume")[0])
|
||||
volumeInstance = conn.GetInstance(volumeInstanceName)
|
||||
volumeName = "1416035-Vol"
|
||||
|
||||
self.driver.common.masking.remove_and_reset_members(
|
||||
conn, controllerConfigService, volumeInstance,
|
||||
volumeName, extraSpecs, reset=False)
|
||||
|
||||
# Bug 1393555 - masking view has been deleted by another process.
|
||||
def test_find_maskingview(self):
|
||||
conn = self.fake_ecom_connection()
|
||||
@ -3209,6 +3243,8 @@ class EMCVMAXISCSIDriverNoFastTestCase(test.TestCase):
|
||||
mock_storage_group,
|
||||
mock_same_host,
|
||||
mock_check):
|
||||
emc_vmax_utils.LIVE_MIGRATION_FILE = (self.tempdir +
|
||||
'/livemigrationarray')
|
||||
self.driver.initialize_connection(self.data.test_volume,
|
||||
self.data.connector)
|
||||
|
||||
@ -8062,6 +8098,87 @@ class EMCVMAXUtilsTest(test.TestCase):
|
||||
self.assertEqual('CIM_DeviceMaskingGroup',
|
||||
modifiedInstance['CreationClassName'])
|
||||
|
||||
@mock.patch.object(
|
||||
emc_vmax_common.EMCVMAXCommon,
|
||||
'_find_lun',
|
||||
return_value={'SystemName': EMCVMAXCommonData.storage_system})
|
||||
@mock.patch('builtins.open' if sys.version_info >= (3,)
|
||||
else '__builtin__.open')
|
||||
def test_insert_live_migration_record(self, mock_open, mock_lun):
|
||||
conn = FakeEcomConnection()
|
||||
self.driver.common.conn = conn
|
||||
extraSpecs = self.data.extra_specs
|
||||
connector = {'initiator': self.data.iscsi_initiator,
|
||||
'ip': '10.0.0.2',
|
||||
'platform': u'x86_64',
|
||||
'host': 'fakehost',
|
||||
'os_type': 'linux2',
|
||||
'multipath': False}
|
||||
maskingviewdict = self.driver.common._populate_masking_dict(
|
||||
self.data.test_volume, self.data.connector, extraSpecs)
|
||||
emc_vmax_utils.LIVE_MIGRATION_FILE = ('/tempdir/livemigrationarray')
|
||||
self.driver.utils.insert_live_migration_record(
|
||||
self.data.test_volume, maskingviewdict, connector, extraSpecs)
|
||||
mock_open.assert_called_once_with(
|
||||
emc_vmax_utils.LIVE_MIGRATION_FILE, "wb")
|
||||
|
||||
@mock.patch.object(
|
||||
emc_vmax_common.EMCVMAXCommon,
|
||||
'_find_lun',
|
||||
return_value={'SystemName': EMCVMAXCommonData.storage_system})
|
||||
def test_delete_live_migration_record(self, mock_lun):
|
||||
conn = FakeEcomConnection()
|
||||
self.driver.common.conn = conn
|
||||
extraSpecs = self.data.extra_specs
|
||||
connector = {'initiator': self.data.iscsi_initiator,
|
||||
'ip': '10.0.0.2',
|
||||
'platform': u'x86_64',
|
||||
'host': 'fakehost',
|
||||
'os_type': 'linux2',
|
||||
'multipath': False}
|
||||
maskingviewdict = self.driver.common._populate_masking_dict(
|
||||
self.data.test_volume, self.data.connector, extraSpecs)
|
||||
tempdir = tempfile.mkdtemp()
|
||||
emc_vmax_utils.LIVE_MIGRATION_FILE = (tempdir +
|
||||
'/livemigrationarray')
|
||||
m = mock.mock_open()
|
||||
with mock.patch('{}.open'.format(__name__), m, create=True):
|
||||
with open(emc_vmax_utils.LIVE_MIGRATION_FILE, "wb") as f:
|
||||
f.write('live migration details')
|
||||
self.driver.utils.insert_live_migration_record(
|
||||
self.data.test_volume, maskingviewdict, connector, extraSpecs)
|
||||
self.driver.utils.delete_live_migration_record(self.data.test_volume)
|
||||
m.assert_called_once_with(emc_vmax_utils.LIVE_MIGRATION_FILE, "wb")
|
||||
shutil.rmtree(tempdir)
|
||||
|
||||
@mock.patch.object(
|
||||
emc_vmax_common.EMCVMAXCommon,
|
||||
'_find_lun',
|
||||
return_value={'SystemName': EMCVMAXCommonData.storage_system})
|
||||
def test_get_live_migration_record(self, mock_lun):
|
||||
conn = FakeEcomConnection()
|
||||
self.driver.common.conn = conn
|
||||
extraSpecs = self.data.extra_specs
|
||||
connector = {'initiator': self.data.iscsi_initiator,
|
||||
'ip': '10.0.0.2',
|
||||
'platform': u'x86_64',
|
||||
'host': 'fakehost',
|
||||
'os_type': 'linux2',
|
||||
'multipath': False}
|
||||
maskingviewdict = self.driver.common._populate_masking_dict(
|
||||
self.data.test_volume, self.data.connector, extraSpecs)
|
||||
tempdir = tempfile.mkdtemp()
|
||||
emc_vmax_utils.LIVE_MIGRATION_FILE = (tempdir +
|
||||
'/livemigrationarray')
|
||||
self.driver.utils.insert_live_migration_record(
|
||||
self.data.test_volume, maskingviewdict, connector, extraSpecs)
|
||||
record = self.driver.utils.get_live_migration_record(
|
||||
self.data.test_volume, False)
|
||||
self.assertEqual(maskingviewdict, record[0])
|
||||
self.assertEqual(connector, record[1])
|
||||
os.remove(emc_vmax_utils.LIVE_MIGRATION_FILE)
|
||||
shutil.rmtree(tempdir)
|
||||
|
||||
|
||||
class EMCVMAXCommonTest(test.TestCase):
|
||||
def setUp(self):
|
||||
|
@ -353,6 +353,16 @@ class EMCVMAXCommon(object):
|
||||
|
||||
self._remove_members(configservice, vol_instance, connector,
|
||||
extraSpecs)
|
||||
livemigrationrecord = self.utils.get_live_migration_record(volume,
|
||||
False)
|
||||
if livemigrationrecord:
|
||||
live_maskingviewdict = livemigrationrecord[0]
|
||||
live_connector = livemigrationrecord[1]
|
||||
live_extraSpecs = livemigrationrecord[2]
|
||||
self._attach_volume(
|
||||
volume, live_connector, live_extraSpecs,
|
||||
live_maskingviewdict, True)
|
||||
self.utils.delete_live_migration_record(volume)
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
"""Initializes the connection and returns device and connection info.
|
||||
@ -451,6 +461,8 @@ class EMCVMAXCommon(object):
|
||||
volume, connector, extraSpecs)
|
||||
if isLiveMigration:
|
||||
maskingViewDict['isLiveMigration'] = True
|
||||
self.utils.insert_live_migration_record(volume, maskingViewDict,
|
||||
connector, extraSpecs)
|
||||
else:
|
||||
maskingViewDict['isLiveMigration'] = False
|
||||
|
||||
@ -466,9 +478,9 @@ class EMCVMAXCommon(object):
|
||||
{'vol': volumeName})
|
||||
if ((rollbackDict['fastPolicyName'] is not None) or
|
||||
(rollbackDict['isV3'] is not None)):
|
||||
(self.masking
|
||||
._check_if_rollback_action_for_masking_required(
|
||||
self.conn, rollbackDict))
|
||||
(self.masking._check_if_rollback_action_for_masking_required(
|
||||
self.conn, rollbackDict))
|
||||
self.utils.delete_live_migration_record(volume)
|
||||
exception_message = (_("Error Attaching volume %(vol)s.")
|
||||
% {'vol': volumeName})
|
||||
raise exception.VolumeBackendAPIException(
|
||||
@ -1570,16 +1582,16 @@ class EMCVMAXCommon(object):
|
||||
host = self.utils.get_host_short_name(host)
|
||||
hoststr = ("-%(host)s-"
|
||||
% {'host': host})
|
||||
|
||||
for maskedvol in maskedvols:
|
||||
if hoststr.lower() in maskedvol['maskingview'].lower():
|
||||
data = maskedvol
|
||||
break
|
||||
if not data:
|
||||
LOG.warning(_LW(
|
||||
"Volume is masked but not to host %(host)s as "
|
||||
"expected. Returning empty dictionary."),
|
||||
{'host': hoststr})
|
||||
if len(maskedvols) > 0:
|
||||
data = maskedvols[0]
|
||||
LOG.warning(_LW(
|
||||
"Volume is masked but not to host %(host)s as is "
|
||||
"expected. Assuming live migration."),
|
||||
{'host': hoststr})
|
||||
|
||||
LOG.debug("Device info: %(data)s.", {'data': data})
|
||||
return data
|
||||
|
@ -242,9 +242,10 @@ class EMCVMAXISCSIDriver(driver.ISCSIDriver):
|
||||
return targets
|
||||
outList = []
|
||||
for iscsi_ip_address in self.iscsi_ip_addresses:
|
||||
out, _err, ex = self._call_iscsiadm(iscsi_ip_address)
|
||||
if out:
|
||||
outList.append(out)
|
||||
if iscsi_ip_address:
|
||||
out, _err, ex = self._call_iscsiadm(iscsi_ip_address)
|
||||
if out:
|
||||
outList.append(out)
|
||||
|
||||
if len(outList) == 0:
|
||||
if ex:
|
||||
|
@ -107,6 +107,11 @@ class EMCVMAXMasking(object):
|
||||
volumeInstance.path,
|
||||
volumeName, fastPolicyName,
|
||||
extraSpecs))
|
||||
else:
|
||||
# Live Migration
|
||||
self.remove_and_reset_members(
|
||||
conn, controllerConfigService, volumeInstance, volumeName,
|
||||
extraSpecs, maskingViewDict['connector'], False)
|
||||
|
||||
# If anything has gone wrong with the masking view we rollback
|
||||
try:
|
||||
@ -1782,17 +1787,23 @@ class EMCVMAXMasking(object):
|
||||
:returns: storageGroupInstanceName
|
||||
"""
|
||||
storageGroupInstanceName = None
|
||||
if connector is not None:
|
||||
storageGroupInstanceName = self._get_sg_associated_with_connector(
|
||||
conn, controllerConfigService, volumeInstance.path,
|
||||
volumeName, connector)
|
||||
if storageGroupInstanceName:
|
||||
self._remove_volume_from_sg(
|
||||
conn, controllerConfigService, storageGroupInstanceName,
|
||||
volumeInstance, extraSpecs)
|
||||
else: # Connector is None in V3 volume deletion case.
|
||||
if extraSpecs[ISV3]:
|
||||
self._cleanup_deletion_v3(
|
||||
conn, controllerConfigService, volumeInstance, extraSpecs)
|
||||
else:
|
||||
if connector:
|
||||
storageGroupInstanceName = (
|
||||
self._get_sg_associated_with_connector(
|
||||
conn, controllerConfigService, volumeInstance.path,
|
||||
volumeName, connector))
|
||||
if storageGroupInstanceName:
|
||||
self._remove_volume_from_sg(
|
||||
conn, controllerConfigService,
|
||||
storageGroupInstanceName,
|
||||
volumeInstance, extraSpecs)
|
||||
else:
|
||||
LOG.warning(_LW("Cannot get storage from connector."))
|
||||
|
||||
if reset:
|
||||
self._return_back_to_default_sg(
|
||||
conn, controllerConfigService, volumeInstance, volumeName,
|
||||
|
@ -15,6 +15,8 @@
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import pickle
|
||||
import random
|
||||
import re
|
||||
from xml.dom import minidom
|
||||
@ -50,6 +52,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'
|
||||
ISCSI = 'iscsi'
|
||||
FC = 'fc'
|
||||
JOB_RETRIES = 60
|
||||
@ -2715,3 +2718,74 @@ class EMCVMAXUtils(object):
|
||||
modifiedInstance = conn.ModifyInstance(storagegroupInstance,
|
||||
PropertyList=propertylist)
|
||||
return modifiedInstance
|
||||
|
||||
def insert_live_migration_record(self, volume, maskingviewdict,
|
||||
connector, extraSpecs):
|
||||
"""Insert a record of live migration destination into a temporary file
|
||||
|
||||
:param volume: the volume dictionary
|
||||
:param maskingviewdict: the storage group instance name
|
||||
:param connector: the connector Object
|
||||
:param extraSpecs: the extraSpecs dict
|
||||
"""
|
||||
live_migration_details = self.get_live_migration_record(volume, True)
|
||||
if live_migration_details:
|
||||
if volume['id'] not in live_migration_details:
|
||||
live_migration_details[volume['id']] = [maskingviewdict,
|
||||
connector, extraSpecs]
|
||||
else:
|
||||
live_migration_details = {volume['id']: [maskingviewdict,
|
||||
connector, extraSpecs]}
|
||||
try:
|
||||
with open(LIVE_MIGRATION_FILE, "wb") as f:
|
||||
pickle.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
|
||||
"""
|
||||
live_migration_details = self.get_live_migration_record(volume, True)
|
||||
if live_migration_details:
|
||||
if volume['id'] in live_migration_details:
|
||||
del live_migration_details[volume['id']]
|
||||
with open(LIVE_MIGRATION_FILE, "wb") as f:
|
||||
pickle.dump(live_migration_details, f)
|
||||
else:
|
||||
LOG.debug("%(Volume)s doesn't exist in live migration "
|
||||
"record.",
|
||||
{'Volume': volume['id']})
|
||||
if not live_migration_details:
|
||||
os.remove(LIVE_MIGRATION_FILE)
|
||||
|
||||
def get_live_migration_record(self, volume, returnallrecords):
|
||||
"""get record of live migration destination from a temporary file
|
||||
|
||||
:param volume: the volume dictionary
|
||||
:param returnallrecords: if true, return all records in file
|
||||
:returns: returns a single record or all records depending on
|
||||
returnallrecords flag
|
||||
"""
|
||||
returned_record = None
|
||||
if os.path.isfile(LIVE_MIGRATION_FILE):
|
||||
with open(LIVE_MIGRATION_FILE, "rb") as f:
|
||||
live_migration_details = pickle.load(f)
|
||||
if returnallrecords:
|
||||
returned_record = live_migration_details
|
||||
else:
|
||||
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
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
fixes:
|
||||
Fix for live migration on VMAX3
|
Loading…
x
Reference in New Issue
Block a user