compute-hyperv/compute_hyperv/nova/block_device_manager.py

363 lines
15 KiB
Python

# 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
"""
import os
from nova import block_device
from nova import exception
from nova import objects
from nova.virt import block_device as driver_block_device
from nova.virt import configdrive
from nova.virt import driver
from os_win import constants as os_win_const
from os_win import exceptions as os_win_exc
from os_win import utilsfactory
from oslo_log import log as logging
from oslo_serialization import jsonutils
from compute_hyperv.i18n import _
from compute_hyperv.nova import constants
from compute_hyperv.nova import pathutils
from compute_hyperv.nova import volumeops
LOG = logging.getLogger(__name__)
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,
'vhdx': constants.DISK,
'iso': constants.DVD}
_DEFAULT_ROOT_DEVICE = '/dev/sda'
def __init__(self):
self._volops = volumeops.VolumeOps()
self._pathutils = pathutils.PathUtils()
self._vmutils = utilsfactory.get_vmutils()
@staticmethod
def _get_device_bus(ctrl_type, ctrl_addr, ctrl_slot):
"""Determines the device bus and it's hypervisor assigned address.
"""
if ctrl_type == constants.CTRL_TYPE_SCSI:
address = ':'.join(map(str, [0, 0, ctrl_addr, ctrl_slot]))
return objects.SCSIDeviceBus(address=address)
elif ctrl_type == constants.CTRL_TYPE_IDE:
address = ':'.join(map(str, [ctrl_addr, ctrl_slot]))
return objects.IDEDeviceBus(address=address)
def _get_vol_bdm_attachment_info(self, bdm):
drv_vol_bdm = driver_block_device.convert_volume(bdm)
if not drv_vol_bdm:
return
connection_info = drv_vol_bdm['connection_info']
if not connection_info:
LOG.warning("Missing connection info for volume %s.",
bdm.volume_id)
return
attachment_info = self._volops.get_disk_attachment_info(
connection_info)
attachment_info['serial'] = connection_info['serial']
return attachment_info
def _get_eph_bdm_attachment_info(self, instance, bdm):
# When attaching ephemeral disks, we're setting this field so that
# we can map them with bdm objects.
connection_info = self.get_bdm_connection_info(bdm)
eph_filename = connection_info.get("eph_filename")
if not eph_filename:
LOG.warning("Missing ephemeral disk filename in "
"BDM connection info. BDM: %s", bdm)
return
eph_path = os.path.join(
self._pathutils.get_instance_dir(instance.name), eph_filename)
if not os.path.exists(eph_path):
LOG.warning("Could not find ephemeral disk %s.", eph_path)
return
return self._vmutils.get_disk_attachment_info(eph_path,
is_physical=False)
def _get_disk_metadata(self, instance, bdm):
attachment_info = None
if bdm.is_volume:
attachment_info = self._get_vol_bdm_attachment_info(bdm)
elif block_device.new_format_is_ephemeral(bdm):
attachment_info = self._get_eph_bdm_attachment_info(
instance, bdm)
if not attachment_info:
LOG.debug("No attachment info retrieved for bdm %s.", bdm)
return
tags = [bdm.tag] if bdm.tag else []
bus = self._get_device_bus(
attachment_info['controller_type'],
attachment_info['controller_addr'],
attachment_info['controller_slot'])
serial = attachment_info.get('serial')
return objects.DiskMetadata(bus=bus,
tags=tags,
serial=serial)
def get_bdm_metadata(self, context, instance):
"""Builds a metadata object for instance devices, that maps the user
provided tag to the hypervisor assigned device address.
"""
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
context, instance.uuid)
bdm_metadata = []
for bdm in bdms:
try:
device_metadata = self._get_disk_metadata(instance, bdm)
if device_metadata:
bdm_metadata.append(device_metadata)
except (exception.DiskNotFound, os_win_exc.DiskNotFound):
LOG.debug("Could not find disk attachment while "
"updating device metadata. It may have been "
"detached. BDM: %s", bdm)
return bdm_metadata
def set_volume_bdm_connection_info(self, context, instance,
connection_info):
# When attaching volumes to already existing instances, the connection
# info passed to the driver is not saved yet within the BDM table.
#
# Nova sets the volume id within the connection info using the
# 'serial' key.
volume_id = connection_info['serial']
bdm = objects.BlockDeviceMapping.get_by_volume_and_instance(
context, volume_id, instance.uuid)
bdm.connection_info = jsonutils.dumps(connection_info)
bdm.save()
@staticmethod
def get_bdm_connection_info(bdm):
# We're using the BDM 'connection_info' field to store ephemeral
# image information so that we can map them. In order to do so,
# we're using this helper.
# The ephemeral bdm object wrapper does not currently expose this
# field.
try:
conn_info = jsonutils.loads(bdm.connection_info)
except TypeError:
conn_info = {}
return conn_info
@staticmethod
def update_bdm_connection_info(bdm, **kwargs):
conn_info = BlockDeviceInfoManager.get_bdm_connection_info(bdm)
conn_info.update(**kwargs)
bdm.connection_info = jsonutils.dumps(conn_info)
bdm.save()
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
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)
if vm_gen == constants.VM_GEN_2 and configdrive.required_by(instance):
# for Generation 2 VMs, the configdrive is attached to the SCSI
# controller. Check that there is still a slot available for it.
if slot_map[constants.CTRL_TYPE_SCSI][0] == 0:
msg = _("There are no more free slots on controller %s for "
"configdrive.") % constants.CTRL_TYPE_SCSI
raise exception.InvalidBDMFormat(details=msg)
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_device(
block_device_info)
root_device = root_device 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')
def _sort_by_boot_order(self, bd_list):
# we sort the block devices by boot_index leaving the ones that don't
# have a specified boot_index at the end
bd_list.sort(key=lambda x: (x['boot_index'] is None, x['boot_index']))
def get_boot_order(self, vm_gen, block_device_info):
if vm_gen == constants.VM_GEN_1:
return self._get_boot_order_gen1(block_device_info)
else:
return self._get_boot_order_gen2(block_device_info)
def _get_boot_order_gen1(self, block_device_info):
if block_device_info['root_disk']['type'] == 'iso':
return [os_win_const.BOOT_DEVICE_CDROM,
os_win_const.BOOT_DEVICE_HARDDISK,
os_win_const.BOOT_DEVICE_NETWORK,
os_win_const.BOOT_DEVICE_FLOPPY]
else:
return [os_win_const.BOOT_DEVICE_HARDDISK,
os_win_const.BOOT_DEVICE_CDROM,
os_win_const.BOOT_DEVICE_NETWORK,
os_win_const.BOOT_DEVICE_FLOPPY]
def _get_boot_order_gen2(self, block_device_info):
devices = [block_device_info['root_disk']]
devices += driver.block_device_info_get_ephemerals(
block_device_info)
devices += driver.block_device_info_get_mapping(block_device_info)
self._sort_by_boot_order(devices)
boot_order = []
for dev in devices:
if dev.get('connection_info'):
dev_path = self._volops.get_disk_resource_path(
dev['connection_info'])
boot_order.append(dev_path)
else:
boot_order.append(dev['path'])
return boot_order