From 0c0361e8bf1ab85812b9b84a3d00ce6eca1b57c1 Mon Sep 17 00:00:00 2001 From: John Hua Date: Fri, 24 Jun 2016 14:13:26 +0800 Subject: [PATCH] XenAPI: device tagging XenAPI implementation of device tagging Partially-Implements: blueprint virt-device-role-tagging-xenapi Change-Id: I565617e05acf33e6254ea091b88d975270ffde05 Depends-On: I9fe520bc9b68d0bc7f879617f2cd27dd1029e4de --- nova/api/metadata/base.py | 2 + nova/objects/fields.py | 12 +++ nova/objects/virt_device_metadata.py | 9 ++ nova/tests/unit/objects/test_fields.py | 14 +++ nova/tests/unit/objects/test_objects.py | 1 + nova/tests/unit/virt/xenapi/test_vmops.py | 116 +++++++++++++++++++++- nova/virt/xenapi/driver.py | 2 +- nova/virt/xenapi/vmops.py | 75 ++++++++++++++ 8 files changed, 227 insertions(+), 4 deletions(-) diff --git a/nova/api/metadata/base.py b/nova/api/metadata/base.py index 11f205991fb3..0d57d1abe551 100644 --- a/nova/api/metadata/base.py +++ b/nova/api/metadata/base.py @@ -403,6 +403,8 @@ class InstanceMetadata(object): bus = 'scsi' elif isinstance(device.bus, metadata_obj.IDEDeviceBus): bus = 'ide' + elif isinstance(device.bus, metadata_obj.XenDeviceBus): + bus = 'xen' else: LOG.debug('Metadata for device with unknown bus %s ' 'has not been included in the ' diff --git a/nova/objects/fields.py b/nova/objects/fields.py index ab5f8f900d92..036e32b887b3 100644 --- a/nova/objects/fields.py +++ b/nova/objects/fields.py @@ -1032,6 +1032,14 @@ class IDEAddress(AddressBase): return AddressBase.coerce(IDEAddress, attr, value) +class XenAddress(AddressBase): + PATTERN = '(00[0-9]{2}00)|[1-9][0-9]+' + + @staticmethod + def coerce(obj, attr, value): + return AddressBase.coerce(XenAddress, attr, value) + + class PCIAddressField(AutoTypedField): AUTO_TYPE = PCIAddress() @@ -1048,6 +1056,10 @@ class IDEAddressField(AutoTypedField): AUTO_TYPE = IDEAddress() +class XenAddressField(AutoTypedField): + AUTO_TYPE = XenAddress() + + class ArchitectureField(BaseEnumField): AUTO_TYPE = Architecture() diff --git a/nova/objects/virt_device_metadata.py b/nova/objects/virt_device_metadata.py index 9602f79d220c..ef391d0eef44 100644 --- a/nova/objects/virt_device_metadata.py +++ b/nova/objects/virt_device_metadata.py @@ -61,6 +61,15 @@ class IDEDeviceBus(DeviceBus): } +@base.NovaObjectRegistry.register +class XenDeviceBus(DeviceBus): + VERSION = '1.0' + + fields = { + 'address': fields.XenAddressField(), + } + + @base.NovaObjectRegistry.register class DeviceMetadata(base.NovaObject): VERSION = '1.0' diff --git a/nova/tests/unit/objects/test_fields.py b/nova/tests/unit/objects/test_fields.py index 9c86c8b7e379..cb891218e59c 100644 --- a/nova/tests/unit/objects/test_fields.py +++ b/nova/tests/unit/objects/test_fields.py @@ -707,6 +707,20 @@ class TestIDEAddress(TestField): self.from_primitive_values = self.coerce_good_values +class TestXenAddress(TestField): + def setUp(self): + super(TestXenAddress, self).setUp() + self.field = fields.Field(fields.XenAddressField()) + self.coerce_good_values = [('000100', '000100'), + ('768', '768')] + self.coerce_bad_values = [ + '1', + '00100', + ] + self.to_primitive_values = self.coerce_good_values + self.from_primitive_values = self.coerce_good_values + + class TestSecureBoot(TestField): def setUp(self): super(TestSecureBoot, self).setUp() diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 5032531ceffc..5f019fc43406 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1179,6 +1179,7 @@ object_data = { 'VirtualInterface': '1.3-efd3ca8ebcc5ce65fff5a25f31754c54', 'VirtualInterfaceList': '1.0-9750e2074437b3077e46359102779fc6', 'VolumeUsage': '1.0-6c8190c46ce1469bb3286a1f21c2e475', + 'XenDeviceBus': '1.0-272a4f899b24e31e42b2b9a7ed7e9194', 'XenapiLiveMigrateData': '1.1-79e69f5ac9abfbcfcbaec18e8280bec6', } diff --git a/nova/tests/unit/virt/xenapi/test_vmops.py b/nova/tests/unit/virt/xenapi/test_vmops.py index 83cc8456744d..287bfdbd106a 100644 --- a/nova/tests/unit/virt/xenapi/test_vmops.py +++ b/nova/tests/unit/virt/xenapi/test_vmops.py @@ -302,6 +302,8 @@ class SpawnTestCase(VMOpsTestBase): self.mox.StubOutWithMock(self.vmops, '_create_vm_record') self.mox.StubOutWithMock(self.vmops, '_destroy') self.mox.StubOutWithMock(self.vmops, '_attach_disks') + self.mox.StubOutWithMock(self.vmops, '_save_device_metadata') + self.mox.StubOutWithMock(self.vmops, '_prepare_disk_metadata') self.mox.StubOutWithMock(pci_manager, 'get_instance_pci_devs') self.mox.StubOutWithMock(vm_utils, 'set_other_config_pci') self.mox.StubOutWithMock(self.vmops, '_attach_orig_disks') @@ -325,13 +327,22 @@ class SpawnTestCase(VMOpsTestBase): self.mox.StubOutWithMock(self.vmops, '_update_last_dom_id') self.mox.StubOutWithMock(self.vmops._session, 'call_xenapi') + @staticmethod + def _new_instance(obj): + class _Instance(dict): + __getattr__ = dict.__getitem__ + __setattr__ = dict.__setitem__ + return _Instance(**obj) + def _test_spawn(self, name_label_param=None, block_device_info_param=None, rescue=False, include_root_vdi=True, throw_exception=None, attach_pci_dev=False, neutron_exception=False, network_info=None): self._stub_out_common() - instance = {"name": "dummy", "uuid": "fake_uuid"} + instance = self._new_instance({"name": "dummy", "uuid": "fake_uuid", + "device_metadata": None}) + name_label = name_label_param if name_label is None: name_label = "dummy" @@ -383,6 +394,7 @@ class SpawnTestCase(VMOpsTestBase): step += 1 self.vmops._update_instance_progress(context, instance, step, steps) + self.vmops._save_device_metadata(context, instance, block_device_info) self.vmops._attach_disks(context, instance, image_meta, vm_ref, name_label, vdis, di_type, network_info, rescue, admin_password, injected_files) @@ -508,6 +520,103 @@ class SpawnTestCase(VMOpsTestBase): '_neutron_failed_callback') self._test_spawn(network_info=network_info) + @staticmethod + def _dev_mock(obj): + dev = mock.MagicMock(**obj) + dev.__contains__.side_effect = ( + lambda attr: getattr(dev, attr, None) is not None) + return dev + + @mock.patch.object(objects, 'XenDeviceBus') + @mock.patch.object(objects, 'IDEDeviceBus') + @mock.patch.object(objects, 'DiskMetadata') + def test_prepare_disk_metadata(self, mock_DiskMetadata, + mock_IDEDeviceBus, mock_XenDeviceBus): + mock_IDEDeviceBus.side_effect = \ + lambda **kw: \ + self._dev_mock({"address": kw.get("address"), "bus": "ide"}) + mock_XenDeviceBus.side_effect = \ + lambda **kw: \ + self._dev_mock({"address": kw.get("address"), "bus": "xen"}) + mock_DiskMetadata.side_effect = \ + lambda **kw: self._dev_mock(dict(**kw)) + + bdm = self._dev_mock({"device_name": "/dev/xvda", "tag": "disk_a"}) + disk_metadata = self.vmops._prepare_disk_metadata(bdm) + + self.assertEqual(disk_metadata[0].tags, ["disk_a"]) + self.assertEqual(disk_metadata[0].bus.bus, "ide") + self.assertEqual(disk_metadata[0].bus.address, "0:0") + self.assertEqual(disk_metadata[1].tags, ["disk_a"]) + self.assertEqual(disk_metadata[1].bus.bus, "xen") + self.assertEqual(disk_metadata[1].bus.address, "000000") + self.assertEqual(disk_metadata[2].tags, ["disk_a"]) + self.assertEqual(disk_metadata[2].bus.bus, "xen") + self.assertEqual(disk_metadata[2].bus.address, "51712") + self.assertEqual(disk_metadata[3].tags, ["disk_a"]) + self.assertEqual(disk_metadata[3].bus.bus, "xen") + self.assertEqual(disk_metadata[3].bus.address, "768") + + bdm = self._dev_mock({"device_name": "/dev/xvdc", "tag": "disk_c"}) + disk_metadata = self.vmops._prepare_disk_metadata(bdm) + + self.assertEqual(disk_metadata[0].tags, ["disk_c"]) + self.assertEqual(disk_metadata[0].bus.bus, "ide") + self.assertEqual(disk_metadata[0].bus.address, "1:0") + self.assertEqual(disk_metadata[1].tags, ["disk_c"]) + self.assertEqual(disk_metadata[1].bus.bus, "xen") + self.assertEqual(disk_metadata[1].bus.address, "000200") + self.assertEqual(disk_metadata[2].tags, ["disk_c"]) + self.assertEqual(disk_metadata[2].bus.bus, "xen") + self.assertEqual(disk_metadata[2].bus.address, "51744") + self.assertEqual(disk_metadata[3].tags, ["disk_c"]) + self.assertEqual(disk_metadata[3].bus.bus, "xen") + self.assertEqual(disk_metadata[3].bus.address, "5632") + + bdm = self._dev_mock({"device_name": "/dev/xvde", "tag": "disk_e"}) + disk_metadata = self.vmops._prepare_disk_metadata(bdm) + + self.assertEqual(disk_metadata[0].tags, ["disk_e"]) + self.assertEqual(disk_metadata[0].bus.bus, "xen") + self.assertEqual(disk_metadata[0].bus.address, "000400") + self.assertEqual(disk_metadata[1].tags, ["disk_e"]) + self.assertEqual(disk_metadata[1].bus.bus, "xen") + self.assertEqual(disk_metadata[1].bus.address, "51776") + + @mock.patch.object(objects.VirtualInterfaceList, 'get_by_instance_uuid') + @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid') + @mock.patch.object(objects, 'NetworkInterfaceMetadata') + @mock.patch.object(objects, 'InstanceDeviceMetadata') + @mock.patch.object(objects, 'PCIDeviceBus') + @mock.patch.object(vmops.VMOps, '_prepare_disk_metadata') + def test_save_device_metadata(self, mock_prepare_disk_metadata, + mock_PCIDeviceBus, mock_InstanceDeviceMetadata, + mock_NetworkInterfaceMetadata, mock_get_bdms, mock_get_vifs): + context = {} + instance = {"uuid": "fake_uuid"} + block_device_info = {'block_device_mapping': []} + vif = self._dev_mock({"address": "fake_address", "tag": "vif_tag"}) + bdm = self._dev_mock({"device_name": "/dev/xvdx", "tag": "bdm_tag"}) + + mock_get_vifs.return_value = [vif] + mock_get_bdms.return_value = [bdm] + mock_InstanceDeviceMetadata.side_effect = \ + lambda **kw: {"devices": kw.get("devices")} + mock_NetworkInterfaceMetadata.return_value = mock.sentinel.vif_metadata + mock_prepare_disk_metadata.return_value = [mock.sentinel.bdm_metadata] + + dev_meta = self.vmops._save_device_metadata(context, instance, + block_device_info) + + mock_get_vifs.assert_called_once_with(context, instance["uuid"]) + + mock_NetworkInterfaceMetadata.assert_called_once_with(mac=vif.address, + bus=mock_PCIDeviceBus.return_value, + tags=[vif.tag]) + mock_prepare_disk_metadata.assert_called_once_with(bdm) + self.assertEqual(dev_meta["devices"], + [mock.sentinel.vif_metadata, mock.sentinel.bdm_metadata]) + def test_spawn_with_neutron_exception(self): self.mox.StubOutWithMock(self.vmops, '_get_neutron_events') self.assertRaises(exception.VirtualInterfaceCreateException, @@ -523,8 +632,8 @@ class SpawnTestCase(VMOpsTestBase): context = "context" migration = {} name_label = "dummy" - instance = {"name": name_label, "uuid": "fake_uuid", - "root_device_name": "/dev/xvda"} + instance = self._new_instance({"name": name_label, "uuid": "fake_uuid", + "root_device_name": "/dev/xvda", "device_metadata": None}) disk_info = "disk_info" network_info = "net_info" image_meta = objects.ImageMeta.from_dict({"id": uuids.image_id}) @@ -566,6 +675,7 @@ class SpawnTestCase(VMOpsTestBase): if resize_instance: self.vmops._resize_up_vdis(instance, vdis) + self.vmops._save_device_metadata(context, instance, block_device_info) self.vmops._attach_disks(context, instance, image_meta, vm_ref, name_label, vdis, di_type, network_info, False, None, None) diff --git a/nova/virt/xenapi/driver.py b/nova/virt/xenapi/driver.py index 5403a69f0ce1..a626424f236e 100644 --- a/nova/virt/xenapi/driver.py +++ b/nova/virt/xenapi/driver.py @@ -71,7 +71,7 @@ class XenAPIDriver(driver.ComputeDriver): "supports_recreate": False, "supports_migrate_to_same_host": False, "supports_attach_interface": True, - "supports_device_tagging": False, + "supports_device_tagging": True, } def __init__(self, virtapi, read_only=False): diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 47337ef47518..da62f28cae4d 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -458,6 +458,8 @@ class VMOps(object): if resize: self._resize_up_vdis(instance, vdis) + instance.device_metadata = self._save_device_metadata( + context, instance, block_device_info) self._attach_disks(context, instance, image_meta, vm_ref, name_label, vdis, disk_image_type, network_info, rescue, @@ -816,6 +818,79 @@ class VMOps(object): admin_password=admin_password, files=files) + @staticmethod + def _prepare_disk_metadata(bdm): + """Returns the disk metadata with dual disk buses - ide and xen. More + details about Xen device number can be found in + http://xenbits.xen.org/docs/4.2-testing/misc/vbd-interface.txt + """ + path = bdm.device_name + disk_num = volume_utils.get_device_number(path) + + xen0 = objects.XenDeviceBus(address=("00%02d00" % disk_num)) + + registry = ('HKLM\\SYSTEM\\ControlSet001\\Enum\\SCSI\\' + 'Disk&Ven_XENSRC&Prod_PVDISK\\') + vbd_prefix = '/sys/devices/vbd-' + + if disk_num < 4: + ide = objects.IDEDeviceBus( + address=("%d:%d" % (disk_num / 2, disk_num % 2))) + + xen1 = objects.XenDeviceBus( + address=("%d" % (202 << 8 | disk_num << 4))) + xen2 = objects.XenDeviceBus() + if disk_num < 2: + xen2.address = "%d" % (3 << 8 | disk_num << 6) + else: + xen2.address = "%d" % (22 << 8 | (disk_num - 2) << 6) + + return [objects.DiskMetadata(path=path, bus=ide, tags=[bdm.tag]), + objects.DiskMetadata(path=registry + xen0.address, + bus=xen0, tags=[bdm.tag]), + objects.DiskMetadata(path=vbd_prefix + xen1.address, + bus=xen1, tags=[bdm.tag]), + objects.DiskMetadata(path=vbd_prefix + xen2.address, + bus=xen2, tags=[bdm.tag])] + else: + xen1 = objects.XenDeviceBus() + + if disk_num < 16: + xen1.address = "%d" % (202 << 8 | disk_num << 4) + else: + xen1.address = "%d" % (1 << 28 | disk_num << 8) + + return [objects.DiskMetadata(path=registry + xen0.address, + bus=xen0, tags=[bdm.tag]), + objects.DiskMetadata(path=vbd_prefix + xen1.address, + bus=xen1, tags=[bdm.tag])] + + 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. + """ + vifs = objects.VirtualInterfaceList.get_by_instance_uuid( + context, instance["uuid"]) + bdms = objects.BlockDeviceMappingList.get_by_instance_uuid( + context, instance["uuid"]) + + metadata = [] + for vif in vifs: + if 'tag' in vif and vif.tag: + device = objects.NetworkInterfaceMetadata( + mac=vif.address, + bus=objects.PCIDeviceBus(), + tags=[vif.tag]) + metadata.append(device) + + if block_device_info: + for bdm in bdms: + if 'tag' in bdm and bdm.tag: + metadata.extend(self._prepare_disk_metadata(bdm)) + + if metadata: + return objects.InstanceDeviceMetadata(devices=metadata) + def _wait_for_instance_to_start(self, instance, vm_ref): LOG.debug('Waiting for instance state to become running', instance=instance)