Added support for new block device format in Hyper-V

This patch adds a block device manager to help with validation
of the data sent by the new block device format. Validation
includes checks for correct disk bus depending on the requested
instance generation, disk type. It also checks for the available
number of disk slots for each bus vs. the number of requested
bdms on each bus type.

Partially implements: blueprint hyper-v-block-device-mapping-support

Change-Id: Ia158e168561e3259083399139e6eac58c0e62757
This commit is contained in:
Adelina Tuvenie
2015-11-17 00:29:32 -08:00
committed by Claudiu Belu
parent 2f550642ab
commit ea4ac442cd
3 changed files with 469 additions and 0 deletions

View File

@@ -0,0 +1,293 @@
# Copyright (c) 2016 Cloudbase Solutions Srl
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from os_win import constants as os_win_const
from nova import exception
from nova.tests.unit.virt.hyperv import test_base
from nova.virt.hyperv import block_device_manager
from nova.virt.hyperv import constants
class BlockDeviceManagerTestCase(test_base.HyperVBaseTestCase):
"""Unit tests for the Hyper-V BlockDeviceInfoManager class."""
def setUp(self):
super(BlockDeviceManagerTestCase, self).setUp()
self._bdman = block_device_manager.BlockDeviceInfoManager()
@mock.patch('nova.virt.configdrive.required_by')
def test_init_controller_slot_counter_gen1_no_configdrive(
self, mock_cfg_drive_req):
mock_cfg_drive_req.return_value = False
slot_map = self._bdman._initialize_controller_slot_counter(
mock.sentinel.FAKE_INSTANCE, constants.VM_GEN_1)
self.assertEqual(slot_map[constants.CTRL_TYPE_IDE][0],
os_win_const.IDE_CONTROLLER_SLOTS_NUMBER)
self.assertEqual(slot_map[constants.CTRL_TYPE_IDE][1],
os_win_const.IDE_CONTROLLER_SLOTS_NUMBER)
self.assertEqual(slot_map[constants.CTRL_TYPE_SCSI][0],
os_win_const.SCSI_CONTROLLER_SLOTS_NUMBER)
@mock.patch('nova.virt.configdrive.required_by')
def test_init_controller_slot_counter_gen1(self, mock_cfg_drive_req):
slot_map = self._bdman._initialize_controller_slot_counter(
mock.sentinel.FAKE_INSTANCE, constants.VM_GEN_1)
self.assertEqual(slot_map[constants.CTRL_TYPE_IDE][1],
os_win_const.IDE_CONTROLLER_SLOTS_NUMBER - 1)
@mock.patch('nova.virt.configdrive.required_by')
def test_init_controller_slot_counter_gen2(self, mock_cfg_drive_req):
slot_map = self._bdman._initialize_controller_slot_counter(
mock.sentinel.FAKE_INSTANCE, constants.VM_GEN_2)
self.assertEqual(slot_map[constants.CTRL_TYPE_SCSI][0],
os_win_const.SCSI_CONTROLLER_SLOTS_NUMBER - 1)
@mock.patch.object(block_device_manager.BlockDeviceInfoManager,
'_initialize_controller_slot_counter')
@mock.patch.object(block_device_manager.BlockDeviceInfoManager,
'_check_and_update_root_device')
@mock.patch.object(block_device_manager.BlockDeviceInfoManager,
'_check_and_update_ephemerals')
@mock.patch.object(block_device_manager.BlockDeviceInfoManager,
'_check_and_update_volumes')
def test_validate_and_update_bdi(self, mock_check_and_update_vol,
mock_check_and_update_eph,
mock_check_and_update_root,
mock_init_ctrl_cntr):
mock_init_ctrl_cntr.return_value = mock.sentinel.FAKE_SLOT_MAP
self._bdman.validate_and_update_bdi(mock.sentinel.FAKE_INSTANCE,
mock.sentinel.IMAGE_META,
mock.sentinel.VM_GEN,
mock.sentinel.BLOCK_DEV_INFO)
mock_init_ctrl_cntr.assert_called_once_with(
mock.sentinel.FAKE_INSTANCE, mock.sentinel.VM_GEN)
mock_check_and_update_root.assert_called_once_with(
mock.sentinel.VM_GEN, mock.sentinel.IMAGE_META,
mock.sentinel.BLOCK_DEV_INFO, mock.sentinel.FAKE_SLOT_MAP)
mock_check_and_update_eph.assert_called_once_with(
mock.sentinel.VM_GEN, mock.sentinel.BLOCK_DEV_INFO,
mock.sentinel.FAKE_SLOT_MAP)
mock_check_and_update_vol.assert_called_once_with(
mock.sentinel.VM_GEN, mock.sentinel.BLOCK_DEV_INFO,
mock.sentinel.FAKE_SLOT_MAP)
@mock.patch.object(block_device_manager.BlockDeviceInfoManager,
'_get_available_controller_slot')
@mock.patch.object(block_device_manager.BlockDeviceInfoManager,
'is_boot_from_volume')
def _test_check_and_update_root_device(self, mock_is_boot_from_vol,
mock_get_avail_ctrl_slot,
disk_format,
vm_gen=constants.VM_GEN_1,
boot_from_volume=False):
image_meta = mock.MagicMock(disk_format=disk_format)
bdi = {'root_device': '/dev/sda',
'block_device_mapping': [
{'mount_device': '/dev/sda',
'connection_info': mock.sentinel.FAKE_CONN_INFO}]}
mock_is_boot_from_vol.return_value = boot_from_volume
mock_get_avail_ctrl_slot.return_value = (0, 0)
self._bdman._check_and_update_root_device(vm_gen, image_meta, bdi,
mock.sentinel.SLOT_MAP)
root_disk = bdi['root_disk']
if boot_from_volume:
self.assertEqual(root_disk['type'], constants.VOLUME)
self.assertIsNone(root_disk['path'])
self.assertEqual(root_disk['connection_info'],
mock.sentinel.FAKE_CONN_INFO)
else:
image_type = self._bdman._TYPE_FOR_DISK_FORMAT.get(
image_meta.disk_format)
self.assertEqual(root_disk['type'], image_type)
self.assertIsNone(root_disk['path'])
self.assertIsNone(root_disk['connection_info'])
disk_bus = (constants.CTRL_TYPE_IDE if
vm_gen == constants.VM_GEN_1 else constants.CTRL_TYPE_SCSI)
self.assertEqual(root_disk['disk_bus'], disk_bus)
self.assertEqual(root_disk['drive_addr'], 0)
self.assertEqual(root_disk['ctrl_disk_addr'], 0)
self.assertEqual(root_disk['boot_index'], 0)
self.assertEqual(root_disk['mount_device'], bdi['root_device'])
mock_get_avail_ctrl_slot.assert_called_once_with(
root_disk['disk_bus'], mock.sentinel.SLOT_MAP)
@mock.patch.object(block_device_manager.BlockDeviceInfoManager,
'is_boot_from_volume', return_value=False)
def test_check_and_update_root_device_exception(self, mock_is_boot_vol):
bdi = {}
image_meta = mock.MagicMock(disk_format=mock.sentinel.fake_format)
self.assertRaises(exception.InvalidImageFormat,
self._bdman._check_and_update_root_device,
constants.VM_GEN_1, image_meta, bdi,
mock.sentinel.SLOT_MAP)
def test_check_and_update_root_device_gen1(self):
self._test_check_and_update_root_device(disk_format='vhd')
def test_check_and_update_root_device_gen1_iso(self):
self._test_check_and_update_root_device(disk_format='iso')
def test_check_and_update_root_device_gen2(self):
self._test_check_and_update_root_device(disk_format='vhd',
vm_gen=constants.VM_GEN_2)
def test_check_and_update_root_device_boot_from_vol_gen1(self):
self._test_check_and_update_root_device(disk_format='vhd',
boot_from_volume=True)
def test_check_and_update_root_device_boot_from_vol_gen2(self):
self._test_check_and_update_root_device(disk_format='vhd',
vm_gen=constants.VM_GEN_2,
boot_from_volume=True)
@mock.patch('nova.virt.configdrive.required_by', return_value=True)
def _test_get_available_controller_slot(self, mock_config_drive_req,
bus=constants.CTRL_TYPE_IDE,
fail=False):
slot_map = self._bdman._initialize_controller_slot_counter(
mock.sentinel.FAKE_VM, constants.VM_GEN_1)
if fail:
slot_map[constants.CTRL_TYPE_IDE][0] = 0
slot_map[constants.CTRL_TYPE_IDE][1] = 0
self.assertRaises(exception.InvalidBDMFormat,
self._bdman._get_available_controller_slot,
constants.CTRL_TYPE_IDE,
slot_map)
else:
(disk_addr,
ctrl_disk_addr) = self._bdman._get_available_controller_slot(
bus, slot_map)
self.assertEqual(0, disk_addr)
self.assertEqual(0, ctrl_disk_addr)
def test_get_available_controller_slot(self):
self._test_get_available_controller_slot()
def test_get_available_controller_slot_scsi_ctrl(self):
self._test_get_available_controller_slot(bus=constants.CTRL_TYPE_SCSI)
def test_get_available_controller_slot_exception(self):
self._test_get_available_controller_slot(fail=True)
def test_is_boot_from_volume_true(self):
vol = {'mount_device': self._bdman._DEFAULT_ROOT_DEVICE}
block_device_info = {'block_device_mapping': [vol]}
ret = self._bdman.is_boot_from_volume(block_device_info)
self.assertTrue(ret)
def test_is_boot_from_volume_false(self):
block_device_info = {'block_device_mapping': []}
ret = self._bdman.is_boot_from_volume(block_device_info)
self.assertFalse(ret)
def test_get_root_device_bdm(self):
mount_device = '/dev/sda'
bdm1 = {'mount_device': None}
bdm2 = {'mount_device': mount_device}
bdi = {'block_device_mapping': [bdm1, bdm2]}
ret = self._bdman._get_root_device_bdm(bdi, mount_device)
self.assertEqual(bdm2, ret)
@mock.patch.object(block_device_manager.BlockDeviceInfoManager,
'_check_and_update_bdm')
def test_check_and_update_ephemerals(self, mock_check_and_update_bdm):
fake_ephemerals = [mock.sentinel.eph1, mock.sentinel.eph2,
mock.sentinel.eph3]
fake_bdi = {'ephemerals': fake_ephemerals}
expected_calls = []
for eph in fake_ephemerals:
expected_calls.append(mock.call(mock.sentinel.fake_slot_map,
mock.sentinel.fake_vm_gen,
eph))
self._bdman._check_and_update_ephemerals(mock.sentinel.fake_vm_gen,
fake_bdi,
mock.sentinel.fake_slot_map)
mock_check_and_update_bdm.assert_has_calls(expected_calls)
@mock.patch.object(block_device_manager.BlockDeviceInfoManager,
'_check_and_update_bdm')
@mock.patch.object(block_device_manager.BlockDeviceInfoManager,
'_get_root_device_bdm')
def test_check_and_update_volumes(self, mock_get_root_dev_bdm,
mock_check_and_update_bdm):
fake_vol1 = {'mount_device': '/dev/sda'}
fake_vol2 = {'mount_device': '/dev/sdb'}
fake_volumes = [fake_vol1, fake_vol2]
fake_bdi = {'block_device_mapping': fake_volumes,
'root_disk': {'mount_device': '/dev/sda'}}
mock_get_root_dev_bdm.return_value = fake_vol1
self._bdman._check_and_update_volumes(mock.sentinel.fake_vm_gen,
fake_bdi,
mock.sentinel.fake_slot_map)
mock_get_root_dev_bdm.assert_called_once_with(fake_bdi, '/dev/sda')
mock_check_and_update_bdm.assert_called_once_with(
mock.sentinel.fake_slot_map, mock.sentinel.fake_vm_gen, fake_vol2)
self.assertNotIn(fake_vol1, fake_bdi)
@mock.patch.object(block_device_manager.BlockDeviceInfoManager,
'_get_available_controller_slot')
def test_check_and_update_bdm_with_defaults(self, mock_get_ctrl_slot):
mock_get_ctrl_slot.return_value = ((mock.sentinel.DRIVE_ADDR,
mock.sentinel.CTRL_DISK_ADDR))
bdm = {'device_type': None,
'disk_bus': None,
'boot_index': None}
self._bdman._check_and_update_bdm(mock.sentinel.FAKE_SLOT_MAP,
constants.VM_GEN_1, bdm)
mock_get_ctrl_slot.assert_called_once_with(
bdm['disk_bus'], mock.sentinel.FAKE_SLOT_MAP)
self.assertEqual(mock.sentinel.DRIVE_ADDR, bdm['drive_addr'])
self.assertEqual(mock.sentinel.CTRL_DISK_ADDR, bdm['ctrl_disk_addr'])
self.assertEqual('disk', bdm['device_type'])
self.assertEqual(self._bdman._DEFAULT_BUS, bdm['disk_bus'])
self.assertIsNone(bdm['boot_index'])
def test_check_and_update_bdm_exception_device_type(self):
bdm = {'device_type': 'cdrom',
'disk_bus': 'IDE'}
self.assertRaises(exception.InvalidDiskInfo,
self._bdman._check_and_update_bdm,
mock.sentinel.FAKE_SLOT_MAP, constants.VM_GEN_1, bdm)
def test_check_and_update_bdm_exception_disk_bus(self):
bdm = {'device_type': 'disk',
'disk_bus': 'fake_bus'}
self.assertRaises(exception.InvalidDiskInfo,
self._bdman._check_and_update_bdm,
mock.sentinel.FAKE_SLOT_MAP, constants.VM_GEN_1, bdm)

View File

@@ -0,0 +1,175 @@
# Copyright (c) 2016 Cloudbase Solutions Srl
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Handling of block device information and mapping
Module contains helper methods for dealing with block device information
"""
from os_win import constants as os_win_const
from nova import block_device
from nova import exception
from nova.i18n import _
from nova.virt import configdrive
from nova.virt import driver
from nova.virt.hyperv import constants
from nova.virt.hyperv import volumeops
class BlockDeviceInfoManager(object):
_VALID_BUS = {constants.VM_GEN_1: (constants.CTRL_TYPE_IDE,
constants.CTRL_TYPE_SCSI),
constants.VM_GEN_2: (constants.CTRL_TYPE_SCSI,)}
_DEFAULT_BUS = constants.CTRL_TYPE_SCSI
_TYPE_FOR_DISK_FORMAT = {'vhd': constants.DISK,
'iso': constants.DVD}
_DEFAULT_ROOT_DEVICE = '/dev/sda'
def __init__(self):
self._volops = volumeops.VolumeOps()
def _initialize_controller_slot_counter(self, instance, vm_gen):
# we have 2 IDE controllers, for a total of 4 slots
free_slots_by_device_type = {
constants.CTRL_TYPE_IDE: [
os_win_const.IDE_CONTROLLER_SLOTS_NUMBER] * 2,
constants.CTRL_TYPE_SCSI: [
os_win_const.SCSI_CONTROLLER_SLOTS_NUMBER]
}
if configdrive.required_by(instance):
if vm_gen == constants.VM_GEN_1:
# reserve one slot for the config drive on the second
# controller in case of generation 1 virtual machines
free_slots_by_device_type[constants.CTRL_TYPE_IDE][1] -= 1
else:
free_slots_by_device_type[constants.CTRL_TYPE_SCSI][0] -= 1
return free_slots_by_device_type
def validate_and_update_bdi(self, instance, image_meta, vm_gen,
block_device_info):
slot_map = self._initialize_controller_slot_counter(instance, vm_gen)
self._check_and_update_root_device(vm_gen, image_meta,
block_device_info, slot_map)
self._check_and_update_ephemerals(vm_gen, block_device_info, slot_map)
self._check_and_update_volumes(vm_gen, block_device_info, slot_map)
def _check_and_update_root_device(self, vm_gen, image_meta,
block_device_info, slot_map):
# either booting from volume, or booting from image/iso
root_disk = {}
root_device = (driver.block_device_info_get_root(block_device_info) or
self._DEFAULT_ROOT_DEVICE)
if self.is_boot_from_volume(block_device_info):
root_volume = self._get_root_device_bdm(
block_device_info, root_device)
root_disk['type'] = constants.VOLUME
root_disk['path'] = None
root_disk['connection_info'] = root_volume['connection_info']
else:
root_disk['type'] = self._TYPE_FOR_DISK_FORMAT.get(
image_meta.disk_format)
if root_disk['type'] is None:
raise exception.InvalidImageFormat(
format=image_meta.disk_format)
root_disk['path'] = None
root_disk['connection_info'] = None
root_disk['disk_bus'] = (constants.CTRL_TYPE_IDE if
vm_gen == constants.VM_GEN_1 else constants.CTRL_TYPE_SCSI)
(root_disk['drive_addr'],
root_disk['ctrl_disk_addr']) = self._get_available_controller_slot(
root_disk['disk_bus'], slot_map)
root_disk['boot_index'] = 0
root_disk['mount_device'] = root_device
block_device_info['root_disk'] = root_disk
def _get_available_controller_slot(self, controller_type, slot_map):
max_slots = (os_win_const.IDE_CONTROLLER_SLOTS_NUMBER if
controller_type == constants.CTRL_TYPE_IDE else
os_win_const.SCSI_CONTROLLER_SLOTS_NUMBER)
for idx, ctrl in enumerate(slot_map[controller_type]):
if slot_map[controller_type][idx] >= 1:
drive_addr = idx
ctrl_disk_addr = max_slots - slot_map[controller_type][idx]
slot_map[controller_type][idx] -= 1
return (drive_addr, ctrl_disk_addr)
msg = _("There are no more free slots on controller %s"
) % controller_type
raise exception.InvalidBDMFormat(details=msg)
def is_boot_from_volume(self, block_device_info):
if block_device_info:
root_device = block_device_info.get('root_device_name')
if not root_device:
root_device = self._DEFAULT_ROOT_DEVICE
return block_device.volume_in_mapping(root_device,
block_device_info)
def _get_root_device_bdm(self, block_device_info, mount_device=None):
for mapping in driver.block_device_info_get_mapping(block_device_info):
if mapping['mount_device'] == mount_device:
return mapping
def _check_and_update_ephemerals(self, vm_gen, block_device_info,
slot_map):
ephemerals = driver.block_device_info_get_ephemerals(block_device_info)
for eph in ephemerals:
self._check_and_update_bdm(slot_map, vm_gen, eph)
def _check_and_update_volumes(self, vm_gen, block_device_info, slot_map):
volumes = driver.block_device_info_get_mapping(block_device_info)
root_device_name = block_device_info['root_disk']['mount_device']
root_bdm = self._get_root_device_bdm(block_device_info,
root_device_name)
if root_bdm:
volumes.remove(root_bdm)
for vol in volumes:
self._check_and_update_bdm(slot_map, vm_gen, vol)
def _check_and_update_bdm(self, slot_map, vm_gen, bdm):
disk_bus = bdm.get('disk_bus')
if not disk_bus:
bdm['disk_bus'] = self._DEFAULT_BUS
elif disk_bus not in self._VALID_BUS[vm_gen]:
msg = _("Hyper-V does not support bus type %(disk_bus)s "
"for generation %(vm_gen)s instances."
) % {'disk_bus': disk_bus,
'vm_gen': vm_gen}
raise exception.InvalidDiskInfo(reason=msg)
device_type = bdm.get('device_type')
if not device_type:
bdm['device_type'] = 'disk'
elif device_type != 'disk':
msg = _("Hyper-V does not support disk type %s for ephemerals "
"or volumes.") % device_type
raise exception.InvalidDiskInfo(reason=msg)
(bdm['drive_addr'],
bdm['ctrl_disk_addr']) = self._get_available_controller_slot(
bdm['disk_bus'], slot_map)
# make sure that boot_index is set.
bdm['boot_index'] = bdm.get('boot_index')

View File

@@ -49,6 +49,7 @@ DISK = "VHD"
DISK_FORMAT = DISK
DVD = "DVD"
DVD_FORMAT = "ISO"
VOLUME = "VOLUME"
DISK_FORMAT_MAP = {
DISK_FORMAT.lower(): DISK,