HyperV: Fix vm disk path issue
As the disk number of iSCSI attached disks can change after host reboot, passthrough attached volumes can get attached in this case. This bug was partially fixed during Icehouse by this patch: https://review.openstack.org/95356 One of the issues with this patch is that it only handles SCSI attached disks, for which reason this issue continues to occur when having generation 1 VMs booted from volume, in which case the disk will be placed on the IDE controller. In this case, one instance may end up booting from another tenant's volume, which is a critical security issue. Also, it assumes that the block device info volume order matches the according disk controller slot order, which is wrong. This patch fixes this issue, looping through the block device info and fixing the disk path for all the passthrough attached disks, regardless of the disk controller. This requires the virtual disk resources attached to the instance to be tagged with the serial number. Co-Authored-By: Alin Balutoiu <abalutoiu@cloudbasesolutions.com> Closes-Bug: #1526831 Depends-On: Ibe463ce9ffb9129cab4b99fe7967ccb5f2181958 Change-Id: Icf8697d917c814ec120fce9898661eb9a20ade48
This commit is contained in:
parent
82bf282dd5
commit
9de0bec4f9
|
@ -55,6 +55,7 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
super(VolumeOpsTestCase, self).setUp()
|
||||
self._volumeops = volumeops.VolumeOps()
|
||||
self._volumeops._volutils = mock.MagicMock()
|
||||
self._volumeops._vmutils = mock.Mock()
|
||||
|
||||
def test_get_volume_driver(self):
|
||||
fake_conn_info = {'driver_volume_type': mock.sentinel.fake_driver_type}
|
||||
|
@ -83,30 +84,53 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
block_device_info['block_device_mapping'][0]['connection_info'],
|
||||
mock.sentinel.instance_name, True)
|
||||
|
||||
def test_fix_instance_volume_disk_paths(self):
|
||||
def test_fix_instance_volume_disk_paths_empty_bdm(self):
|
||||
self._volumeops.fix_instance_volume_disk_paths(
|
||||
mock.sentinel.instance_name,
|
||||
block_device_info={})
|
||||
self.assertFalse(
|
||||
self._volumeops._vmutils.get_vm_physical_disk_mapping.called)
|
||||
|
||||
@mock.patch.object(volumeops.VolumeOps, 'get_disk_path_mapping')
|
||||
def test_fix_instance_volume_disk_paths(self, mock_get_disk_path_mapping):
|
||||
block_device_info = get_fake_block_dev_info()
|
||||
fake_vol_conn_info = (
|
||||
block_device_info['block_device_mapping'][0]['connection_info'])
|
||||
|
||||
with test.nested(
|
||||
mock.patch.object(self._volumeops,
|
||||
'_get_volume_driver'),
|
||||
mock.patch.object(self._volumeops,
|
||||
'ebs_root_in_block_devices')
|
||||
) as (mock_get_volume_driver,
|
||||
mock_ebs_in_block_devices):
|
||||
mock_disk1 = {
|
||||
'mounted_disk_path': mock.sentinel.mounted_disk1_path,
|
||||
'resource_path': mock.sentinel.resource1_path
|
||||
}
|
||||
mock_disk2 = {
|
||||
'mounted_disk_path': mock.sentinel.mounted_disk2_path,
|
||||
'resource_path': mock.sentinel.resource2_path
|
||||
}
|
||||
|
||||
fake_vol_driver = mock_get_volume_driver.return_value
|
||||
mock_ebs_in_block_devices.return_value = False
|
||||
mock_vm_disk_mapping = {
|
||||
mock.sentinel.disk1_serial: mock_disk1,
|
||||
mock.sentinel.disk2_serial: mock_disk2
|
||||
}
|
||||
# In this case, only the first disk needs to be updated.
|
||||
mock_phys_disk_path_mapping = {
|
||||
mock.sentinel.disk1_serial: mock.sentinel.actual_disk1_path,
|
||||
mock.sentinel.disk2_serial: mock.sentinel.mounted_disk2_path
|
||||
}
|
||||
|
||||
self._volumeops.fix_instance_volume_disk_paths(
|
||||
mock.sentinel.instance_name,
|
||||
block_device_info)
|
||||
vmutils = self._volumeops._vmutils
|
||||
vmutils.get_vm_physical_disk_mapping.return_value = (
|
||||
mock_vm_disk_mapping)
|
||||
|
||||
func = fake_vol_driver.fix_instance_volume_disk_path
|
||||
func.assert_called_once_with(
|
||||
mock.sentinel.instance_name,
|
||||
fake_vol_conn_info, 0)
|
||||
mock_get_disk_path_mapping.return_value = mock_phys_disk_path_mapping
|
||||
|
||||
self._volumeops.fix_instance_volume_disk_paths(
|
||||
mock.sentinel.instance_name,
|
||||
block_device_info)
|
||||
|
||||
vmutils.get_vm_physical_disk_mapping.assert_called_once_with(
|
||||
mock.sentinel.instance_name)
|
||||
mock_get_disk_path_mapping.assert_called_once_with(
|
||||
block_device_info)
|
||||
vmutils.set_disk_host_res.assert_called_once_with(
|
||||
mock.sentinel.resource1_path,
|
||||
mock.sentinel.actual_disk1_path)
|
||||
|
||||
@mock.patch.object(volumeops.VolumeOps, '_get_volume_driver')
|
||||
def test_disconnect_volumes(self, mock_get_volume_driver):
|
||||
|
@ -153,6 +177,26 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
init_vol_conn.assert_called_once_with(
|
||||
block_device_info['block_device_mapping'][0]['connection_info'])
|
||||
|
||||
@mock.patch.object(volumeops.VolumeOps,
|
||||
'get_mounted_disk_path_from_volume')
|
||||
def test_get_disk_path_mapping(self, mock_get_disk_path):
|
||||
block_device_info = get_fake_block_dev_info()
|
||||
block_device_mapping = block_device_info['block_device_mapping']
|
||||
fake_conn_info = get_fake_connection_info()
|
||||
block_device_mapping[0]['connection_info'] = fake_conn_info
|
||||
|
||||
mock_get_disk_path.return_value = mock.sentinel.disk_path
|
||||
|
||||
resulted_disk_path_mapping = self._volumeops.get_disk_path_mapping(
|
||||
block_device_info)
|
||||
|
||||
mock_get_disk_path.assert_called_once_with(fake_conn_info)
|
||||
expected_disk_path_mapping = {
|
||||
mock.sentinel.serial: mock.sentinel.disk_path
|
||||
}
|
||||
self.assertEqual(expected_disk_path_mapping,
|
||||
resulted_disk_path_mapping)
|
||||
|
||||
def test_group_block_devices_by_type(self):
|
||||
block_device_map = get_fake_block_dev_info()['block_device_mapping']
|
||||
block_device_map[0]['connection_info'] = {
|
||||
|
@ -163,6 +207,21 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
expected = {'iscsi': [block_device_map[0]]}
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@mock.patch.object(volumeops.VolumeOps, '_get_volume_driver')
|
||||
def test_get_mounted_disk_path_from_volume(self, mock_get_volume_driver):
|
||||
fake_conn_info = get_fake_connection_info()
|
||||
fake_volume_driver = mock_get_volume_driver.return_value
|
||||
|
||||
resulted_disk_path = self._volumeops.get_mounted_disk_path_from_volume(
|
||||
fake_conn_info)
|
||||
|
||||
mock_get_volume_driver.assert_called_once_with(
|
||||
connection_info=fake_conn_info)
|
||||
get_mounted_disk = fake_volume_driver.get_mounted_disk_path_from_volume
|
||||
get_mounted_disk.assert_called_once_with(fake_conn_info)
|
||||
self.assertEqual(get_mounted_disk.return_value,
|
||||
resulted_disk_path)
|
||||
|
||||
|
||||
class ISCSIVolumeDriverTestCase(test_base.HyperVBaseTestCase):
|
||||
"""Unit tests for Hyper-V ISCSIVolumeDriver class."""
|
||||
|
@ -230,6 +289,22 @@ class ISCSIVolumeDriverTestCase(test_base.HyperVBaseTestCase):
|
|||
def test_logout_storage_target(self):
|
||||
self._check_logout_storage_target(disconnected_luns_count=1)
|
||||
|
||||
@mock.patch.object(volumeops.ISCSIVolumeDriver,
|
||||
'_get_mounted_disk_from_lun')
|
||||
def test_get_mounted_disk_path_from_volume(self,
|
||||
mock_get_mounted_disk_from_lun):
|
||||
connection_info = get_fake_connection_info()
|
||||
resulted_disk_path = (
|
||||
self._volume_driver.get_mounted_disk_path_from_volume(
|
||||
connection_info))
|
||||
|
||||
mock_get_mounted_disk_from_lun.assert_called_once_with(
|
||||
connection_info['data']['target_iqn'],
|
||||
connection_info['data']['target_lun'],
|
||||
wait_for_device=True)
|
||||
self.assertEqual(mock_get_mounted_disk_from_lun.return_value,
|
||||
resulted_disk_path)
|
||||
|
||||
@mock.patch.object(volumeops.ISCSIVolumeDriver,
|
||||
'_get_mounted_disk_from_lun')
|
||||
@mock.patch.object(volumeops.ISCSIVolumeDriver, 'logout_storage_target')
|
||||
|
@ -269,7 +344,9 @@ class ISCSIVolumeDriverTestCase(test_base.HyperVBaseTestCase):
|
|||
|
||||
mock_login_storage_target.assert_called_once_with(connection_info)
|
||||
mock_get_mounted_disk_from_lun.assert_called_once_with(
|
||||
mock.sentinel.fake_iqn, mock.sentinel.fake_lun)
|
||||
mock.sentinel.fake_iqn,
|
||||
mock.sentinel.fake_lun,
|
||||
wait_for_device=True)
|
||||
if ebs_root:
|
||||
get_ide_path.assert_called_once_with(
|
||||
mock.sentinel.instance_name, 0)
|
||||
|
@ -302,7 +379,9 @@ class ISCSIVolumeDriverTestCase(test_base.HyperVBaseTestCase):
|
|||
mock.sentinel.instance_name)
|
||||
|
||||
mock_get_mounted_disk_from_lun.assert_called_once_with(
|
||||
mock.sentinel.fake_iqn, mock.sentinel.fake_lun)
|
||||
mock.sentinel.fake_iqn,
|
||||
mock.sentinel.fake_lun,
|
||||
wait_for_device=True)
|
||||
self._volume_driver._vmutils.detach_vm_disk.assert_called_once_with(
|
||||
mock.sentinel.instance_name,
|
||||
mock_get_mounted_disk_from_lun.return_value)
|
||||
|
@ -337,28 +416,6 @@ class ISCSIVolumeDriverTestCase(test_base.HyperVBaseTestCase):
|
|||
mock.sentinel.physical_drive_path)
|
||||
self.assertEqual(mock_get_target.return_value, result)
|
||||
|
||||
@mock.patch.object(volumeops.ISCSIVolumeDriver,
|
||||
'_get_mounted_disk_from_lun')
|
||||
def test_fix_instance_volume_disk_path(self, mock_get_disk_from_lun):
|
||||
connection_info = get_fake_connection_info()
|
||||
|
||||
set_disk_host_res = self._volume_driver._vmutils.set_disk_host_resource
|
||||
get_scsi_ctrl = self._volume_driver._vmutils.get_vm_scsi_controller
|
||||
get_scsi_ctrl.return_value = mock.sentinel.controller_path
|
||||
mock_get_disk_from_lun.return_value = mock.sentinel.mounted_path
|
||||
|
||||
self._volume_driver.fix_instance_volume_disk_path(
|
||||
mock.sentinel.instance_name,
|
||||
connection_info,
|
||||
mock.sentinel.disk_address)
|
||||
|
||||
mock_get_disk_from_lun.assert_called_once_with(
|
||||
mock.sentinel.fake_iqn, mock.sentinel.fake_lun, True)
|
||||
get_scsi_ctrl.assert_called_once_with(mock.sentinel.instance_name)
|
||||
set_disk_host_res.assert_called_once_with(
|
||||
mock.sentinel.instance_name, mock.sentinel.controller_path,
|
||||
mock.sentinel.disk_address, mock.sentinel.mounted_path)
|
||||
|
||||
@mock.patch('time.sleep')
|
||||
def test_get_mounted_disk_from_lun_failure(self, fake_sleep):
|
||||
self.flags(mounted_disk_query_retry_count=1, group='hyperv')
|
||||
|
@ -422,6 +479,15 @@ class SMBFSVolumeDriverTestCase(test_base.HyperVBaseTestCase):
|
|||
self._volume_driver._pathutils = mock.MagicMock()
|
||||
self._volume_driver._volutils = mock.MagicMock()
|
||||
|
||||
@mock.patch.object(volumeops.SMBFSVolumeDriver,
|
||||
'_get_disk_path')
|
||||
def test_get_mounted_disk_path_from_volume(self, mock_get_disk_path):
|
||||
disk_path = self._volume_driver.get_mounted_disk_path_from_volume(
|
||||
mock.sentinel.conn_info)
|
||||
|
||||
self.assertEqual(mock_get_disk_path.return_value, disk_path)
|
||||
mock_get_disk_path.assert_called_once_with(mock.sentinel.conn_info)
|
||||
|
||||
@mock.patch.object(volumeops.SMBFSVolumeDriver, 'ensure_share_mounted')
|
||||
@mock.patch.object(volumeops.SMBFSVolumeDriver, '_get_disk_path')
|
||||
def _check_attach_volume(self, mock_get_disk_path,
|
||||
|
|
|
@ -116,19 +116,23 @@ class VolumeOps(object):
|
|||
block_device_info)
|
||||
|
||||
def fix_instance_volume_disk_paths(self, instance_name, block_device_info):
|
||||
mapping = driver.block_device_info_get_mapping(block_device_info)
|
||||
# Mapping containing the current disk paths for each volume.
|
||||
actual_disk_mapping = self.get_disk_path_mapping(block_device_info)
|
||||
if not actual_disk_mapping:
|
||||
return
|
||||
|
||||
if self.ebs_root_in_block_devices(block_device_info):
|
||||
mapping = mapping[1:]
|
||||
# Mapping containing virtual disk resource path and the physical
|
||||
# disk path for each volume serial number. The physical path
|
||||
# associated with this resource may not be the right one,
|
||||
# as physical disk paths can get swapped after host reboots.
|
||||
vm_disk_mapping = self._vmutils.get_vm_physical_disk_mapping(
|
||||
instance_name)
|
||||
|
||||
disk_address = 0
|
||||
for vol in mapping:
|
||||
connection_info = vol['connection_info']
|
||||
volume_driver = self._get_volume_driver(
|
||||
connection_info=connection_info)
|
||||
volume_driver.fix_instance_volume_disk_path(
|
||||
instance_name, connection_info, disk_address)
|
||||
disk_address += 1
|
||||
for serial, vm_disk in vm_disk_mapping.items():
|
||||
actual_disk_path = actual_disk_mapping[serial]
|
||||
if vm_disk['mounted_disk_path'] != actual_disk_path:
|
||||
self._vmutils.set_disk_host_res(vm_disk['resource_path'],
|
||||
actual_disk_path)
|
||||
|
||||
def get_volume_connector(self, instance):
|
||||
if not self._initiator:
|
||||
|
@ -150,6 +154,17 @@ class VolumeOps(object):
|
|||
connection_info=connection_info)
|
||||
volume_driver.initialize_volume_connection(connection_info)
|
||||
|
||||
def get_disk_path_mapping(self, block_device_info):
|
||||
block_mapping = driver.block_device_info_get_mapping(block_device_info)
|
||||
disk_path_mapping = {}
|
||||
for vol in block_mapping:
|
||||
connection_info = vol['connection_info']
|
||||
disk_serial = connection_info['serial']
|
||||
|
||||
disk_path = self.get_mounted_disk_path_from_volume(connection_info)
|
||||
disk_path_mapping[disk_serial] = disk_path
|
||||
return disk_path_mapping
|
||||
|
||||
def _group_block_devices_by_type(self, block_device_mapping):
|
||||
block_devices = collections.defaultdict(list)
|
||||
for volume in block_device_mapping:
|
||||
|
@ -158,6 +173,12 @@ class VolumeOps(object):
|
|||
block_devices[volume_type].append(volume)
|
||||
return block_devices
|
||||
|
||||
def get_mounted_disk_path_from_volume(self, connection_info):
|
||||
volume_driver = self._get_volume_driver(
|
||||
connection_info=connection_info)
|
||||
return volume_driver.get_mounted_disk_path_from_volume(
|
||||
connection_info)
|
||||
|
||||
|
||||
class ISCSIVolumeDriver(object):
|
||||
def __init__(self):
|
||||
|
@ -220,6 +241,15 @@ class ISCSIVolumeDriver(object):
|
|||
LOG.debug("Skipping disconnecting target %s as there "
|
||||
"are LUNs still being used.", target_iqn)
|
||||
|
||||
def get_mounted_disk_path_from_volume(self, connection_info):
|
||||
data = connection_info['data']
|
||||
target_lun = data['target_lun']
|
||||
target_iqn = data['target_iqn']
|
||||
|
||||
# Getting the mounted disk
|
||||
return self._get_mounted_disk_from_lun(target_iqn, target_lun,
|
||||
wait_for_device=True)
|
||||
|
||||
def attach_volume(self, connection_info, instance_name, ebs_root=False):
|
||||
"""Attach a volume to the SCSI controller or to the IDE controller if
|
||||
ebs_root is True
|
||||
|
@ -231,14 +261,10 @@ class ISCSIVolumeDriver(object):
|
|||
try:
|
||||
self.login_storage_target(connection_info)
|
||||
|
||||
data = connection_info['data']
|
||||
target_lun = data['target_lun']
|
||||
target_iqn = data['target_iqn']
|
||||
serial = connection_info['serial']
|
||||
|
||||
# Getting the mounted disk
|
||||
mounted_disk_path = self._get_mounted_disk_from_lun(target_iqn,
|
||||
target_lun)
|
||||
mounted_disk_path = self.get_mounted_disk_path_from_volume(
|
||||
connection_info)
|
||||
|
||||
if ebs_root:
|
||||
# Find the IDE controller for the vm.
|
||||
|
@ -261,6 +287,7 @@ class ISCSIVolumeDriver(object):
|
|||
with excutils.save_and_reraise_exception():
|
||||
LOG.error(_LE('Unable to attach volume to instance %s'),
|
||||
instance_name)
|
||||
target_iqn = connection_info['data']['target_iqn']
|
||||
if target_iqn:
|
||||
self.logout_storage_target(target_iqn)
|
||||
|
||||
|
@ -271,13 +298,9 @@ class ISCSIVolumeDriver(object):
|
|||
{'connection_info': connection_info,
|
||||
'instance_name': instance_name})
|
||||
|
||||
data = connection_info['data']
|
||||
target_lun = data['target_lun']
|
||||
target_iqn = data['target_iqn']
|
||||
|
||||
# Getting the mounted disk
|
||||
mounted_disk_path = self._get_mounted_disk_from_lun(target_iqn,
|
||||
target_lun)
|
||||
target_iqn = connection_info['data']['target_iqn']
|
||||
mounted_disk_path = self.get_mounted_disk_path_from_volume(
|
||||
connection_info)
|
||||
|
||||
LOG.debug("Detaching physical disk from instance: %s",
|
||||
mounted_disk_path)
|
||||
|
@ -327,18 +350,6 @@ class ISCSIVolumeDriver(object):
|
|||
def get_target_from_disk_path(self, physical_drive_path):
|
||||
return self._volutils.get_target_from_disk_path(physical_drive_path)
|
||||
|
||||
def fix_instance_volume_disk_path(self, instance_name, connection_info,
|
||||
disk_address):
|
||||
data = connection_info['data']
|
||||
target_lun = data['target_lun']
|
||||
target_iqn = data['target_iqn']
|
||||
|
||||
mounted_disk_path = self._get_mounted_disk_from_lun(
|
||||
target_iqn, target_lun, True)
|
||||
ctrller_path = self._vmutils.get_vm_scsi_controller(instance_name)
|
||||
self._vmutils.set_disk_host_resource(
|
||||
instance_name, ctrller_path, disk_address, mounted_disk_path)
|
||||
|
||||
def get_target_lun_count(self, target_iqn):
|
||||
return self._volutils.get_target_lun_count(target_iqn)
|
||||
|
||||
|
@ -364,6 +375,9 @@ class SMBFSVolumeDriver(object):
|
|||
self._username_regex = re.compile(r'user(?:name)?=([^, ]+)')
|
||||
self._password_regex = re.compile(r'pass(?:word)?=([^, ]+)')
|
||||
|
||||
def get_mounted_disk_path_from_volume(self, connection_info):
|
||||
return self._get_disk_path(connection_info)
|
||||
|
||||
@export_path_synchronized
|
||||
def attach_volume(self, connection_info, instance_name, ebs_root=False):
|
||||
self.ensure_share_mounted(connection_info)
|
||||
|
@ -443,10 +457,6 @@ class SMBFSVolumeDriver(object):
|
|||
|
||||
return username, password
|
||||
|
||||
def fix_instance_volume_disk_path(self, instance_name, connection_info,
|
||||
disk_address):
|
||||
self.ensure_share_mounted(connection_info)
|
||||
|
||||
def initialize_volume_connection(self, connection_info):
|
||||
self.ensure_share_mounted(connection_info)
|
||||
|
||||
|
|
Loading…
Reference in New Issue