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:
Helen Walsh 2016-06-16 21:44:23 +01:00
parent 175667cf4e
commit 3f3a0c8639
6 changed files with 239 additions and 21 deletions

View File

@ -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):

View File

@ -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

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -0,0 +1,3 @@
---
fixes:
Fix for live migration on VMAX3