Enable multi host live migration with NPIV Volumes

When the migration occurs the order of the WWPNs is important.  The
first WWPN is the client adapter that is logged in.  This gets
obfusicated when a live migration is occurring.  The source is primary
and logged in, and the target is secondary (also logged in).

This code adds a post migration call for the volume drivers.  It will
handle saving the updated WWPN order in the system metadata (as the
instance gets saved on the target host, post live migration).  It also
introduces a mig_vol_stor concept into post migration so that the volume
adapters can pass data from one connection to another.  This is
important for NPIV as it is concerned about fabrics rather than the
specific volumes within the fabric.

Change-Id: Ia4f3d0c35b10f6f106ba27b0fb9f408fbeb670fb
This commit is contained in:
Drew Thorstensen 2015-09-11 15:44:27 -04:00
parent 5135a3fa7f
commit 5e1a83ced4
8 changed files with 134 additions and 18 deletions

View File

@ -293,7 +293,7 @@ BODY{
<PortName kxe="false" kb="CUR">fcs0</PortName>
<UniqueDeviceID kb="ROR" kxe="false">1aU78CB.001.WZS007Y-P1-C3-T1</UniqueDeviceID>
<WWPN kb="CUR" kxe="false">21000024FF649104</WWPN>
<AvailablePorts kb="ROR" kxe="true">64</AvailablePorts>
<AvailablePorts kb="ROR" kxe="true">63</AvailablePorts>
<TotalPorts kb="ROR" kxe="true">64</TotalPorts>
</PhysicalFibreChannelPort>
<PhysicalFibreChannelPort schemaVersion="V1_2_0">

View File

@ -1136,4 +1136,4 @@ class TestPowerVMDriver(test.TestCase):
self.drv.post_live_migration_at_destination(
'context', self.lpm_inst, 'network_info')
self.lpm.post_live_migration_at_destination.assert_called_once_with(
'network_info')
'network_info', [])

View File

@ -141,7 +141,7 @@ class TestLPM(test.TestCase):
self.lpmsrc.post_live_migration_at_source('network_info')
def test_post_live_mig_dest(self):
self.lpmdst.post_live_migration_at_destination('network_info')
self.lpmdst.post_live_migration_at_destination('network_info', [])
@mock.patch('pypowervm.tasks.migration.migrate_recover')
def test_rollback(self, mock_migr):

View File

@ -345,7 +345,7 @@ class TestNPIVAdapter(test.TestCase):
wwpns = self.vol_drv.wwpns()
# Check
self.assertListEqual(['AA', 'BB', 'CC', 'DD'], wwpns)
self.assertListEqual(['AA', 'CC'], wwpns)
self.assertEqual('21000024FF649104,AA,BB,21000024FF649105,CC,DD',
self.vol_drv.instance.system_metadata[meta_key])
self.assertEqual(1, mock_add_port.call_count)
@ -368,7 +368,7 @@ class TestNPIVAdapter(test.TestCase):
self.vol_drv._sys_meta_fabric_key('A'): 'phys1,a,b,phys2,c,d'}
# Invoke and Verify
self.assertListEqual(['a', 'b', 'c', 'd'], self.vol_drv.wwpns())
self.assertListEqual(['a', 'c'], self.vol_drv.wwpns())
@mock.patch('nova_powervm.virt.powervm.volume.npiv.NPIVVolumeAdapter.'
'_get_fabric_state')
@ -382,4 +382,32 @@ class TestNPIVAdapter(test.TestCase):
# Invoke and Verify
for state in [task_states.DELETING, task_states.MIGRATING]:
self.vol_drv.instance.task_state = state
self.assertListEqual(['a', 'b', 'c', 'd'], self.vol_drv.wwpns())
self.assertListEqual(['a', 'c'], self.vol_drv.wwpns())
@mock.patch('nova_powervm.virt.powervm.volume.npiv.NPIVVolumeAdapter.'
'_set_fabric_meta')
@mock.patch('nova_powervm.virt.powervm.volume.npiv.NPIVVolumeAdapter.'
'_get_fabric_meta')
@mock.patch('nova_powervm.virt.powervm.volume.npiv.NPIVVolumeAdapter.'
'_fabric_names')
def test_post_live_migration_at_destination(
self, mock_fabric_names, mock_get_fabric_meta,
mock_set_fabric_meta):
mock_fabric_names.return_value = ['A', 'B']
mock_get_fabric_meta.side_effect = [
[('11', 'AA BB'), ('22', 'CC DD')],
[('33', 'EE FF')]]
# Execute the test
mig_vol_stor = {}
self.vol_drv.post_live_migration_at_destination(mig_vol_stor)
mock_set_fabric_meta.assert_any_call(
'A', [('11', 'BB AA'), ('22', 'DD CC')])
mock_set_fabric_meta.assert_any_call(
'B', [('33', 'FF EE')])
# Invoke a second time. Should not 're-flip' or even call set.
mock_set_fabric_meta.reset_mock()
self.vol_drv.post_live_migration_at_destination(mig_vol_stor)
self.assertFalse(mock_set_fabric_meta.called)

View File

@ -993,13 +993,10 @@ class PowerVMDriver(driver.ComputeDriver):
mig = self.live_migrations[instance.uuid]
# Get a volume driver for each volume
vol_drvs = []
bdms = self._extract_bdm(block_device_info)
for bdm in bdms or []:
vol_drvs.append(
self._get_inst_vol_adpt(
context, instance, conn_info=bdm.get('connection_info')))
vol_drvs = self._build_vol_drivers(context, instance,
block_device_info)
# Run pre-live migration
mig.pre_live_migration(context, block_device_info, network_info,
disk_info, migrate_data, vol_drvs)
@ -1133,9 +1130,26 @@ class PowerVMDriver(driver.ComputeDriver):
instance=instance)
mig = self.live_migrations[instance.uuid]
mig.instance = instance
mig.post_live_migration_at_destination(network_info)
# Build the volume drivers
vol_drvs = self._build_vol_drivers(context, instance,
block_device_info)
# Run post live migration
mig.post_live_migration_at_destination(network_info, vol_drvs)
del self.live_migrations[instance.uuid]
def _build_vol_drivers(self, context, instance, block_device_info):
"""Builds the volume connector drivers for a block device info."""
# Get a volume driver for each volume
vol_drvs = []
bdms = self._extract_bdm(block_device_info)
for bdm in bdms or []:
vol_drvs.append(
self._get_inst_vol_adpt(
context, instance, conn_info=bdm.get('connection_info')))
return vol_drvs
def unfilter_instance(self, instance, network_info):
"""Stop filtering instance."""
# No op for PowerVM

View File

@ -181,15 +181,33 @@ class LiveMigrationDest(LiveMigration):
host=self.drvr.host_wrapper.system_name,
name=self.instance.name, volume=vol_drv.volume_id)
def post_live_migration_at_destination(self, network_info):
def post_live_migration_at_destination(self, network_info, vol_drvs):
"""Do post migration cleanup on destination host.
:param network_info: instance network information
:param vol_drvs: volume drivers for the attached volumes
"""
# The LPAR should be on this host now.
LOG.debug("Post live migration at destination.",
instance=self.instance)
# An unbounded dictionary that each volume adapter can use to persist
# data from one call to the next.
mig_vol_stor = {}
# For each volume, make sure it's ready to migrate
for vol_drv in vol_drvs:
LOG.info(_LI('Performing post migration for volume %(volume)s'),
dict(volume=vol_drv.volume_id))
try:
vol_drv.post_live_migration_at_destination(mig_vol_stor)
except Exception as e:
LOG.exception(e)
# It failed.
raise LiveMigrationVolume(
host=self.drvr.host_wrapper.system_name,
name=self.instance.name, volume=vol_drv.volume_id)
class LiveMigrationSrc(LiveMigration):

View File

@ -111,10 +111,22 @@ class PowerVMVolumeAdapter(object):
"""Perform pre live migration steps for the volume on the target host.
This method performs any pre live migration that is needed.
"""
raise NotImplementedError()
def post_live_migration_at_destination(self, mig_vol_stor):
"""Perform post live migration steps for the volume on the target host.
This method performs any post live migration that is needed. Is not
required to be implemented.
:param mig_vol_stor: An unbounded dictionary that will be passed to
each volume adapter during the post live migration
call. Adapters can store data in here that may
be used by subsequent volume adapters.
"""
pass
def connect_volume(self):
"""Connects the volume."""
self._connect_volume()

View File

@ -109,6 +109,45 @@ class NPIVVolumeAdapter(v_driver.FibreChannelVolumeAdapter):
# happen to be no fabric changes.
self.stg_ftsk.execute()
def post_live_migration_at_destination(self, mig_vol_stor):
"""Perform post live migration steps for the volume on the target host.
This method performs any post live migration that is needed. Is not
required to be implemented.
:param mig_vol_stor: An unbounded dictionary that will be passed to
each volume adapter during the post live migration
call. Adapters can store data in here that may
be used by subsequent volume adapters.
"""
# This method will run on the target host after the migration is
# completed. Right after this the instance.save is invoked from the
# manager. Given that, we need to update the order of the WWPNs.
# The first WWPN is the one that is logged into the fabric and this
# will now indicate that our WWPN is logged in.
#
# TODO(thorst) Rather than just flipping...we should query the LPAR
# VFC's and find the logged in WWPN. That guarantees accuracy.
for fabric in self._fabric_names():
# We check the mig_vol_stor to see if this fabric has already been
# flipped. If so, we can continue.
fabric_key = '%s_flipped' % fabric
if mig_vol_stor.get(fabric_key, False):
continue
# Must not be flipped, so execute the flip
npiv_port_maps = self._get_fabric_meta(fabric)
new_port_maps = []
for port_map in npiv_port_maps:
c_wwpns = port_map[1].split()
c_wwpns.reverse()
new_map = (port_map[0], " ".join(c_wwpns))
new_port_maps.append(new_map)
self._set_fabric_meta(fabric, new_port_maps)
# Store that this fabric is now flipped.
mig_vol_stor[fabric_key] = True
def _is_initial_wwpn(self, fc_state, fabric):
"""Determines if the invocation to wwpns is for a general method.
@ -134,6 +173,8 @@ class NPIVVolumeAdapter(v_driver.FibreChannelVolumeAdapter):
def _is_migration_wwpn(self, fc_state):
"""Determines if the WWPN call is occurring during a migration.
This determines if it is on the target host.
:param fc_state: The fabrics state.
:return: True if the instance appears to be migrating to this host.
False otherwise.
@ -272,7 +313,9 @@ class NPIVVolumeAdapter(v_driver.FibreChannelVolumeAdapter):
# If not None, then add the WWPNs to the response.
if port_maps is not None:
for mapping in port_maps:
resp_wwpns.extend(mapping[1].split())
# Only add the first WWPN. That is the one that will be
# logged into the fabric.
resp_wwpns.append(mapping[1].split()[0])
# The return object needs to be a list for the volume connector.
return resp_wwpns
@ -345,7 +388,7 @@ class NPIVVolumeAdapter(v_driver.FibreChannelVolumeAdapter):
{'inst': self.instance.name, 'fabric': fabric}]
vios_w = pvm_vfcm.find_vios_for_port_map(vios_wraps, npiv_port_map)
# Add the subtask to remove the specific map.
# Add the subtask to remove the specific map
self.stg_ftsk.wrapper_tasks[vios_w.uuid].add_functor_subtask(
pvm_vfcm.remove_maps, self.vm_uuid, port_map=npiv_port_map,
logspec=ls)
@ -355,7 +398,8 @@ class NPIVVolumeAdapter(v_driver.FibreChannelVolumeAdapter):
:return: The host name.
"""
return self.instance.name
host = CONF.host if len(CONF.host) < 20 else CONF.host[:20]
return host + '_' + self.instance.name
def _set_fabric_state(self, fabric, state):
"""Sets the fabric state into the instance's system metadata.