diff --git a/nova/tests/unit/virt/hyperv/test_block_device_manager.py b/nova/tests/unit/virt/hyperv/test_block_device_manager.py index 810a0b589167..8ef7ae2c9649 100644 --- a/nova/tests/unit/virt/hyperv/test_block_device_manager.py +++ b/nova/tests/unit/virt/hyperv/test_block_device_manager.py @@ -28,6 +28,66 @@ class BlockDeviceManagerTestCase(test_base.HyperVBaseTestCase): super(BlockDeviceManagerTestCase, self).setUp() self._bdman = block_device_manager.BlockDeviceInfoManager() + def test_get_device_bus_scsi(self): + bdm = {'disk_bus': constants.CTRL_TYPE_SCSI, + 'drive_addr': 0, 'ctrl_disk_addr': 2} + + bus = self._bdman._get_device_bus(bdm) + self.assertEqual('0:0:0:2', bus.address) + + def test_get_device_bus_ide(self): + bdm = {'disk_bus': constants.CTRL_TYPE_IDE, + 'drive_addr': 0, 'ctrl_disk_addr': 1} + + bus = self._bdman._get_device_bus(bdm) + self.assertEqual('0:1', bus.address) + + @staticmethod + def _bdm_mock(**kwargs): + bdm = mock.MagicMock(**kwargs) + bdm.__contains__.side_effect = ( + lambda attr: getattr(bdm, attr, None) is not None) + return bdm + + @mock.patch.object(block_device_manager.objects, 'DiskMetadata') + @mock.patch.object(block_device_manager.BlockDeviceInfoManager, + '_get_device_bus') + @mock.patch.object(block_device_manager.objects.BlockDeviceMappingList, + 'get_by_instance_uuid') + def test_get_bdm_metadata(self, mock_get_by_inst_uuid, mock_get_device_bus, + mock_DiskMetadata): + mock_instance = mock.MagicMock() + root_disk = {'mount_device': mock.sentinel.dev0} + ephemeral = {'device_name': mock.sentinel.dev1} + block_device_info = { + 'root_disk': root_disk, + 'block_device_mapping': [ + {'mount_device': mock.sentinel.dev2}, + {'mount_device': mock.sentinel.dev3}, + ], + 'ephemerals': [ephemeral], + } + + bdm = self._bdm_mock(device_name=mock.sentinel.dev0, tag='taggy') + eph = self._bdm_mock(device_name=mock.sentinel.dev1, tag='ephy') + mock_get_by_inst_uuid.return_value = [ + bdm, eph, self._bdm_mock(device_name=mock.sentinel.dev2, tag=None), + ] + + bdm_metadata = self._bdman.get_bdm_metadata(mock.sentinel.context, + mock_instance, + block_device_info) + + mock_get_by_inst_uuid.assert_called_once_with(mock.sentinel.context, + mock_instance.uuid) + mock_get_device_bus.assert_has_calls( + [mock.call(root_disk), mock.call(ephemeral)], any_order=True) + mock_DiskMetadata.assert_has_calls( + [mock.call(bus=mock_get_device_bus.return_value, tags=[bdm.tag]), + mock.call(bus=mock_get_device_bus.return_value, tags=[eph.tag])], + any_order=True) + self.assertEqual([mock_DiskMetadata.return_value] * 2, bdm_metadata) + @mock.patch('nova.virt.configdrive.required_by') def test_init_controller_slot_counter_gen1_no_configdrive( self, mock_cfg_drive_req): diff --git a/nova/tests/unit/virt/hyperv/test_vmops.py b/nova/tests/unit/virt/hyperv/test_vmops.py index 4d016487f59b..0df6de62a7fc 100644 --- a/nova/tests/unit/virt/hyperv/test_vmops.py +++ b/nova/tests/unit/virt/hyperv/test_vmops.py @@ -341,11 +341,60 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): mock_create_dynamic_vhd.assert_called_once_with('fake_eph_path', 10 * units.Gi) + @mock.patch.object(vmops.objects, 'PCIDeviceBus') + @mock.patch.object(vmops.objects, 'NetworkInterfaceMetadata') + @mock.patch.object(vmops.objects.VirtualInterfaceList, + 'get_by_instance_uuid') + def test_get_vif_metadata(self, mock_get_by_inst_uuid, + mock_NetworkInterfaceMetadata, mock_PCIDevBus): + mock_vif = mock.MagicMock(tag='taggy') + mock_vif.__contains__.side_effect = ( + lambda attr: getattr(mock_vif, attr, None) is not None) + mock_get_by_inst_uuid.return_value = [mock_vif, + mock.MagicMock(tag=None)] + + vif_metadata = self._vmops._get_vif_metadata(self.context, + mock.sentinel.instance_id) + + mock_get_by_inst_uuid.assert_called_once_with( + self.context, mock.sentinel.instance_id) + mock_NetworkInterfaceMetadata.assert_called_once_with( + mac=mock_vif.address, + bus=mock_PCIDevBus.return_value, + tags=[mock_vif.tag]) + self.assertEqual([mock_NetworkInterfaceMetadata.return_value], + vif_metadata) + + @mock.patch.object(vmops.objects, 'InstanceDeviceMetadata') + @mock.patch.object(vmops.VMOps, '_get_vif_metadata') + def test_save_device_metadata(self, mock_get_vif_metadata, + mock_InstanceDeviceMetadata): + mock_instance = mock.MagicMock() + mock_get_vif_metadata.return_value = [mock.sentinel.vif_metadata] + self._vmops._block_dev_man.get_bdm_metadata.return_value = [ + mock.sentinel.bdm_metadata] + + self._vmops._save_device_metadata(self.context, mock_instance, + mock.sentinel.block_device_info) + + mock_get_vif_metadata.assert_called_once_with(self.context, + mock_instance.uuid) + self._vmops._block_dev_man.get_bdm_metadata.assert_called_once_with( + self.context, mock_instance, mock.sentinel.block_device_info) + + expected_metadata = [mock.sentinel.vif_metadata, + mock.sentinel.bdm_metadata] + mock_InstanceDeviceMetadata.assert_called_once_with( + devices=expected_metadata) + self.assertEqual(mock_InstanceDeviceMetadata.return_value, + mock_instance.device_metadata) + @mock.patch('nova.virt.hyperv.vmops.VMOps.destroy') @mock.patch('nova.virt.hyperv.vmops.VMOps.power_on') @mock.patch('nova.virt.hyperv.vmops.VMOps.attach_config_drive') @mock.patch('nova.virt.hyperv.vmops.VMOps._create_config_drive') @mock.patch('nova.virt.configdrive.required_by') + @mock.patch('nova.virt.hyperv.vmops.VMOps._save_device_metadata') @mock.patch('nova.virt.hyperv.vmops.VMOps.create_instance') @mock.patch('nova.virt.hyperv.vmops.VMOps.get_image_vm_generation') @mock.patch('nova.virt.hyperv.vmops.VMOps._create_ephemerals') @@ -353,7 +402,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): @mock.patch('nova.virt.hyperv.vmops.VMOps._delete_disk_files') def _test_spawn(self, mock_delete_disk_files, mock_create_root_device, mock_create_ephemerals, mock_get_image_vm_gen, - mock_create_instance, mock_configdrive_required, + mock_create_instance, mock_save_device_metadata, + mock_configdrive_required, mock_create_config_drive, mock_attach_config_drive, mock_power_on, mock_destroy, exists, configdrive_required, fail): @@ -401,6 +451,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): mock_create_instance.assert_called_once_with( mock_instance, mock.sentinel.INFO, root_device_info, block_device_info, fake_vm_gen) + mock_save_device_metadata.assert_called_once_with( + self.context, mock_instance, block_device_info) mock_configdrive_required.assert_called_once_with(mock_instance) if configdrive_required: mock_create_config_drive.assert_called_once_with( diff --git a/nova/virt/hyperv/block_device_manager.py b/nova/virt/hyperv/block_device_manager.py index c5f88eb4d4ba..c744795d7283 100644 --- a/nova/virt/hyperv/block_device_manager.py +++ b/nova/virt/hyperv/block_device_manager.py @@ -18,11 +18,14 @@ Handling of block device information and mapping Module contains helper methods for dealing with block device information """ +import itertools + from os_win import constants as os_win_const from nova import block_device from nova import exception from nova.i18n import _ +from nova import objects from nova.virt import configdrive from nova.virt import driver from nova.virt.hyperv import constants @@ -45,6 +48,47 @@ class BlockDeviceInfoManager(object): def __init__(self): self._volops = volumeops.VolumeOps() + @staticmethod + def _get_device_bus(bdm): + """Determines the device bus and it's hypervisor assigned address. + """ + if bdm['disk_bus'] == constants.CTRL_TYPE_SCSI: + address = ':'.join(['0', '0', str(bdm['drive_addr']), + str(bdm['ctrl_disk_addr'])]) + return objects.SCSIDeviceBus(address=address) + elif bdm['disk_bus'] == constants.CTRL_TYPE_IDE: + address = ':'.join([str(bdm['drive_addr']), + str(bdm['ctrl_disk_addr'])]) + return objects.IDEDeviceBus(address=address) + + def get_bdm_metadata(self, context, instance, block_device_info): + """Builds a metadata object for instance devices, that maps the user + provided tag to the hypervisor assigned device address. + """ + # block_device_info does not contain tags information. + bdm_obj_list = objects.BlockDeviceMappingList.get_by_instance_uuid( + context, instance.uuid) + + # create a map between BDM object and its device name. + bdm_obj_map = {bdm.device_name: bdm for bdm in bdm_obj_list} + + bdm_metadata = [] + for bdm in itertools.chain(block_device_info['block_device_mapping'], + block_device_info['ephemerals'], + [block_device_info['root_disk']]): + # NOTE(claudiub): ephemerals have device_name instead of + # mount_device. + device_name = bdm.get('mount_device') or bdm.get('device_name') + bdm_obj = bdm_obj_map.get(device_name) + + if bdm_obj and 'tag' in bdm_obj and bdm_obj.tag: + bus = self._get_device_bus(bdm) + device = objects.DiskMetadata(bus=bus, + tags=[bdm_obj.tag]) + bdm_metadata.append(device) + + return bdm_metadata + 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 = { diff --git a/nova/virt/hyperv/driver.py b/nova/virt/hyperv/driver.py index 3c7bbb341a89..dc5a7d3719a5 100644 --- a/nova/virt/hyperv/driver.py +++ b/nova/virt/hyperv/driver.py @@ -97,7 +97,8 @@ class HyperVDriver(driver.ComputeDriver): "has_imagecache": True, "supports_recreate": False, "supports_migrate_to_same_host": True, - "supports_attach_interface": True + "supports_attach_interface": True, + "supports_device_tagging": True, } def __init__(self, virtapi): diff --git a/nova/virt/hyperv/vmops.py b/nova/virt/hyperv/vmops.py index f68d758d26ec..49031b3b7994 100644 --- a/nova/virt/hyperv/vmops.py +++ b/nova/virt/hyperv/vmops.py @@ -39,6 +39,7 @@ from nova.compute import vm_states import nova.conf from nova import exception from nova.i18n import _, _LI, _LE, _LW +from nova import objects from nova import utils from nova.virt import configdrive from nova.virt import hardware @@ -245,6 +246,36 @@ class VMOps(object): self._vhdutils.create_dynamic_vhd(eph_info['path'], eph_info['size'] * units.Gi) + @staticmethod + def _get_vif_metadata(context, instance_id): + vifs = objects.VirtualInterfaceList.get_by_instance_uuid(context, + instance_id) + vif_metadata = [] + for vif in vifs: + if 'tag' in vif and vif.tag: + device = objects.NetworkInterfaceMetadata( + mac=vif.address, + bus=objects.PCIDeviceBus(), + tags=[vif.tag]) + vif_metadata.append(device) + + return vif_metadata + + def _save_device_metadata(self, context, instance, block_device_info): + """Builds a metadata object for instance devices, that maps the user + provided tag to the hypervisor assigned device address. + """ + metadata = [] + + metadata.extend(self._get_vif_metadata(context, instance.uuid)) + if block_device_info: + metadata.extend(self._block_dev_man.get_bdm_metadata( + context, instance, block_device_info)) + + if metadata: + instance.device_metadata = objects.InstanceDeviceMetadata( + devices=metadata) + @check_admin_permissions def spawn(self, context, instance, image_meta, injected_files, admin_password, network_info, block_device_info=None): @@ -269,6 +300,7 @@ class VMOps(object): try: self.create_instance(instance, network_info, root_device, block_device_info, vm_gen) + self._save_device_metadata(context, instance, block_device_info) if configdrive.required_by(instance): configdrive_path = self._create_config_drive(instance,