diff --git a/nova/tests/unit/virt/libvirt/fakelibvirt.py b/nova/tests/unit/virt/libvirt/fakelibvirt.py index 6332e5e71a0e..9fc693de47ae 100644 --- a/nova/tests/unit/virt/libvirt/fakelibvirt.py +++ b/nova/tests/unit/virt/libvirt/fakelibvirt.py @@ -183,6 +183,11 @@ VIR_SECRET_USAGE_TYPE_VOLUME = 1 VIR_SECRET_USAGE_TYPE_CEPH = 2 VIR_SECRET_USAGE_TYPE_ISCSI = 3 +# metadata types +VIR_DOMAIN_METADATA_DESCRIPTION = 0 +VIR_DOMAIN_METADATA_TITLE = 1 +VIR_DOMAIN_METADATA_ELEMENT = 2 + # Libvirt version to match MIN_LIBVIRT_VERSION in driver.py FAKE_LIBVIRT_VERSION = versionutils.convert_version_to_int( libvirt_driver.MIN_LIBVIRT_VERSION) @@ -1380,6 +1385,9 @@ class Domain(object): def fsThaw(self): pass + def setMetadata(self, metadata_type, metadata, key, uri, flags=0): + pass + class DomainSnapshot(object): def __init__(self, name, domain): diff --git a/nova/tests/unit/virt/libvirt/test_config.py b/nova/tests/unit/virt/libvirt/test_config.py index 9a9342efae77..e859b5a542a4 100644 --- a/nova/tests/unit/virt/libvirt/test_config.py +++ b/nova/tests/unit/virt/libvirt/test_config.py @@ -3646,9 +3646,30 @@ class LibvirtConfigGuestMetadataNovaTest(LibvirtConfigBaseTest): meta.flavor = flavor + meta.ports = config.LibvirtConfigGuestMetaNovaPorts( + ports=[ + config.LibvirtConfigGuestMetaNovaPort( + '567a4527-b0e4-4d0a-bcc2-71fda37897f7', + ips=[ + config.LibvirtConfigGuestMetaNovaIp( + 'fixed', '192.168.1.1', '4'), + config.LibvirtConfigGuestMetaNovaIp( + 'fixed', 'fe80::f95c:b030:7094', '6'), + config.LibvirtConfigGuestMetaNovaIp( + 'floating', '11.22.33.44', '4')]), + config.LibvirtConfigGuestMetaNovaPort( + 'a3ca97e2-0cf9-4159-9bfc-afd55bc13ead', + ips=[ + config.LibvirtConfigGuestMetaNovaIp( + 'fixed', '10.0.0.1', '4'), + config.LibvirtConfigGuestMetaNovaIp( + 'fixed', 'fdf8:f53b:82e4::52', '6'), + config.LibvirtConfigGuestMetaNovaIp( + 'floating', '1.2.3.4', '4')])]) + xml = meta.to_xml() self.assertXmlEqual(xml, """ - + moonbuggy 2009-02-13 23:31:30 @@ -3666,6 +3687,18 @@ class LibvirtConfigGuestMetadataNovaTest(LibvirtConfigBaseTest): uuid="f241e906-010e-4917-ae81-53f4fb8aa021">moonshot + + + + + + + + + + + + """) diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 199ed758d250..61ac0f84910b 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -784,6 +784,9 @@ class FakeVirtDomain(object): def undefine(self): return True + def setMetadata(self, metadata_type, metadata, key, uri, flags=0): + pass + class CacheConcurrencyTestCase(test.NoDBTestCase): def setUp(self): @@ -2701,6 +2704,44 @@ class LibvirtConnTestCase(test.NoDBTestCase, # our stub method is called which asserts the password is scrubbed self.assertTrue(debug_mock.called) + def test_get_guest_config_meta_with_no_port(self): + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + meta = drvr._get_guest_config_meta( + objects.Instance(**self.test_instance), + _fake_network_info(self, num_networks=0)) + + self.assertEqual(len(meta.ports.ports), 0) + + def test_get_guest_config_meta_with_multiple_ports(self): + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + meta = drvr._get_guest_config_meta( + objects.Instance(**self.test_instance), + _fake_network_info(self, num_networks=2)) + + self.assertEqual(len(meta.ports.ports), 2) + + # first port + self.assertEqual(meta.ports.ports[0].uuid, getattr(uuids, 'vif1')) + self.assertEqual(len(meta.ports.ports[0].ips), 2) + self.assertEqual(meta.ports.ports[0].ips[0].address, '192.168.1.100') + self.assertEqual(meta.ports.ports[0].ips[0].ip_type, 'fixed') + self.assertEqual(meta.ports.ports[0].ips[0].ip_version, 4) + self.assertEqual(meta.ports.ports[0].ips[1].address, + '2001:db8:0:1:dcad:beff:feef:1') + self.assertEqual(meta.ports.ports[0].ips[1].ip_type, 'fixed') + self.assertEqual(meta.ports.ports[0].ips[1].ip_version, 6) + + # second port + self.assertEqual(meta.ports.ports[1].uuid, getattr(uuids, 'vif2')) + self.assertEqual(len(meta.ports.ports[0].ips), 2) + self.assertEqual(meta.ports.ports[1].ips[0].address, '192.168.2.100') + self.assertEqual(meta.ports.ports[1].ips[0].ip_type, 'fixed') + self.assertEqual(meta.ports.ports[1].ips[0].ip_version, 4) + self.assertEqual(meta.ports.ports[1].ips[1].address, + '2001:db8:0:2:dcad:beff:feef:1') + self.assertEqual(meta.ports.ports[1].ips[1].ip_type, 'fixed') + self.assertEqual(meta.ports.ports[1].ips[1].ip_version, 6) + @mock.patch.object(time, "time") def test_get_guest_config(self, time_mock): time_mock.return_value = 1234567.89 @@ -19036,6 +19077,8 @@ class LibvirtConnTestCase(test.NoDBTestCase, mock_build.return_value = objects.InstanceDeviceMetadata() mock_save = self.useFixture(fixtures.MockPatchObject( objects.Instance, 'save')).mock + mock_get_network_info = self.useFixture(fixtures.MockPatchObject( + objects.Instance, 'get_network_info')).mock expected = drvr.vif_driver.get_config(instance, network_info[0], fake_image_meta, @@ -19050,6 +19093,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, network_info[0]) mock_build.assert_called_once_with(self.context, instance) mock_save.assert_called_once_with() + mock_get_network_info.assert_called_once_with() elif method_name == "detach_interface": drvr.detach_interface(self.context, instance, network_info[0]) else: @@ -23504,6 +23548,37 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin): mock_detach_interface.assert_called_with(self.context, instance, network_info[0]) + def test_attach_interface_guest_set_metadata(self): + guest = mock.Mock(spec=libvirt_guest.Guest) + instance = self._create_instance() + network_info = _fake_network_info(self)[0] + domain = FakeVirtDomain(fake_xml='') + image_meta = objects.ImageMeta.from_dict({}) + config_meta = vconfig.LibvirtConfigGuestMetaNovaInstance() + + with test.nested( + mock.patch.object(host.Host, '_get_domain', return_value=domain), + mock.patch.object(self.drvr, '_build_device_metadata', + return_value=objects.InstanceDeviceMetadata()), + mock.patch.object(instance, 'save'), + mock.patch.object(instance, 'get_network_info'), + mock.patch.object( + self.drvr, '_get_guest_config_meta', return_value=config_meta), + mock.patch.object(guest, 'set_metadata'), + mock.patch.object(self.drvr._host, 'get_guest', return_value=guest) + ) as ( + mock_get_domain, mock_build_device_metadata, mock_save, + mock_get_network_info, mock_get_guest_config_meta, + mock_set_metadata, mock_get_guest + ): + self.drvr.attach_interface( + self.context, instance, image_meta, network_info) + mock_build_device_metadata.assert_called_once_with( + self.context, instance) + mock_save.assert_called_once_with() + mock_set_metadata.assert_called_once_with(config_meta) + + @mock.patch.object(objects.Instance, 'get_network_info') @mock.patch.object(objects.Instance, 'save') @mock.patch.object(libvirt_driver.LibvirtDriver, '_build_device_metadata') @mock.patch.object(FakeVirtDomain, 'info') @@ -23511,7 +23586,8 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin): @mock.patch.object(host.Host, '_get_domain') def _test_attach_interface(self, power_state, expected_flags, mock_get_domain, mock_attach, - mock_info, mock_build, mock_save): + mock_info, mock_build, mock_save, + mock_get_network_info): instance = self._create_instance() network_info = _fake_network_info(self) domain = FakeVirtDomain(fake_xml=""" @@ -23551,6 +23627,7 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin): mock_info.assert_called_once_with() mock_build.assert_called_once_with(self.context, instance) mock_save.assert_called_once_with() + mock_get_network_info.assert_called_once_with() mock_attach.assert_called_once_with(expected.to_xml(), flags=expected_flags) @@ -23617,11 +23694,12 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin): side_effect=get_interface_calls), mock.patch.object(domain, 'detachDeviceFlags'), mock.patch('nova.virt.libvirt.driver.LOG.warning'), - mock.patch.object(self.drvr.vif_driver, 'unplug') + mock.patch.object(self.drvr.vif_driver, 'unplug'), + mock.patch.object(instance, 'get_network_info') ) as ( mock_get_guest, mock_get_config, mock_get_interface, mock_detach_device_flags, - mock_warning, mock_unplug + mock_warning, mock_unplug, mock_get_network_info ): # run the detach method self.drvr.detach_interface(self.context, instance, network_info[0]) @@ -23641,6 +23719,7 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin): mock_detach_device_flags.assert_called_once_with( expected_cfg.to_xml(), flags=expected_flags) mock_warning.assert_not_called() + mock_get_network_info.assert_called_once_with() mock_unplug.assert_called_once_with(instance, network_info[0]) @@ -23763,7 +23842,8 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin): side_effect=[expected, expected, None, None]), mock.patch.object(self.drvr.vif_driver, 'get_config', return_value=expected), - ) as (mock_get_interface, mock_get_config): + mock.patch.object(instance, 'get_network_info') + ) as (mock_get_interface, mock_get_config, mock_get_network_info): self.drvr.detach_interface(self.context, instance, network_info[0]) mock_get_interface.assert_has_calls([mock.call(expected)] * 3) @@ -23775,6 +23855,58 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin): mock_info.assert_called_once_with() mock_detach.assert_called_once_with(expected.to_xml(), flags=expected_flags) + mock_get_network_info.assert_called_once_with() + + def test_detach_interface_guest_set_metadata(self): + guest = mock.Mock(spec=libvirt_guest.Guest) + instance = self._create_instance() + network_info = _fake_network_info(self, num_networks=3) + vif = network_info[0] + interface = vconfig.LibvirtConfigGuestInterface() + image_meta = objects.ImageMeta.from_dict({}) + disk_info = blockinfo.get_disk_info( + CONF.libvirt.virt_type, instance, image_meta) + cfg = self.drvr._get_guest_config( + instance, network_info, image_meta, disk_info) + mock_wait_for_detach = mock.Mock() + config_meta = vconfig.LibvirtConfigGuestMetaNovaInstance() + + with test.nested( + mock.patch.object( + self.drvr._host, 'get_guest', return_value=guest), + mock.patch.object( + self.drvr.vif_driver, 'get_config', return_value=cfg), + mock.patch.object( + guest, 'get_interface_by_cfg', return_value=interface), + mock.patch.object(guest, 'get_power_state'), + mock.patch.object( + instance, 'get_network_info', return_value=network_info), + mock.patch.object(guest, + 'detach_device_with_retry', return_value=mock_wait_for_detach), + mock.patch.object( + self.drvr, '_get_guest_config_meta', return_value=config_meta), + mock.patch.object(guest, 'set_metadata') + ) as ( + mock_get_guest, mock_get_config, mock_get_interface_by_cfg, + mock_get_power_state, mock_get_network_info, + mock_detach_device_with_retry, mock_get_guest_config_meta, + mock_set_metadata + ): + self.drvr.detach_interface(self.context, instance, vif) + mock_get_guest.assert_called_once_with(instance) + mock_get_config.assert_called_once_with( + instance, vif, test.MatchType(objects.ImageMeta), + test.MatchType(objects.Flavor), CONF.libvirt.virt_type) + mock_get_interface_by_cfg.assert_called_once_with(cfg) + mock_get_power_state.assert_called_once_with(self.drvr._host) + mock_detach_device_with_retry.assert_called_once_with( + guest.get_interface_by_cfg, cfg, live=False, + alternative_device_name=None) + mock_wait_for_detach.assert_called_once_with() + mock_get_network_info.assert_called_once_with() + mock_get_guest_config_meta.assert_called_once_with( + instance, network_info[1:]) + mock_set_metadata.assert_called_once_with(config_meta) @mock.patch('nova.objects.block_device.BlockDeviceMapping.save', new=mock.Mock()) diff --git a/nova/tests/unit/virt/libvirt/test_guest.py b/nova/tests/unit/virt/libvirt/test_guest.py index 0dabb78f2fdb..6ccd81cd09d8 100644 --- a/nova/tests/unit/virt/libvirt/test_guest.py +++ b/nova/tests/unit/virt/libvirt/test_guest.py @@ -713,6 +713,39 @@ class GuestTestCase(test.NoDBTestCase): self.guest.migrate_configure_max_downtime(1000) self.domain.migrateSetMaxDowntime.assert_called_once_with(1000) + def test_set_metadata(self): + meta = mock.Mock(spec=vconfig.LibvirtConfigGuestMetaNovaInstance) + meta.to_xml.return_value = "" + self.guest.set_metadata(meta) + self.domain.setMetadata.assert_called_once_with( + fakelibvirt.VIR_DOMAIN_METADATA_ELEMENT, "", "instance", + vconfig.NOVA_NS, flags=0) + + def test_set_metadata_persistent(self): + meta = mock.Mock(spec=vconfig.LibvirtConfigGuestMetaNovaInstance) + meta.to_xml.return_value = "" + self.guest.set_metadata(meta, persistent=True) + self.domain.setMetadata.assert_called_once_with( + fakelibvirt.VIR_DOMAIN_METADATA_ELEMENT, "", "instance", + vconfig.NOVA_NS, flags=fakelibvirt.VIR_DOMAIN_AFFECT_CONFIG) + + def test_set_metadata_device_live(self): + meta = mock.Mock(spec=vconfig.LibvirtConfigGuestMetaNovaInstance) + meta.to_xml.return_value = "" + self.guest.set_metadata(meta, live=True) + self.domain.setMetadata.assert_called_once_with( + fakelibvirt.VIR_DOMAIN_METADATA_ELEMENT, "", "instance", + vconfig.NOVA_NS, flags=fakelibvirt.VIR_DOMAIN_AFFECT_LIVE) + + def test_set_metadata_persistent_live(self): + meta = mock.Mock(spec=vconfig.LibvirtConfigGuestMetaNovaInstance) + meta.to_xml.return_value = "" + self.guest.set_metadata(meta, persistent=True, live=True) + self.domain.setMetadata.assert_called_once_with( + fakelibvirt.VIR_DOMAIN_METADATA_ELEMENT, "", "instance", + vconfig.NOVA_NS, flags=fakelibvirt.VIR_DOMAIN_AFFECT_LIVE | + fakelibvirt.VIR_DOMAIN_AFFECT_CONFIG) + class GuestBlockTestCase(test.NoDBTestCase): diff --git a/nova/virt/libvirt/config.py b/nova/virt/libvirt/config.py index d0c82082ee8b..77f2f253cab6 100644 --- a/nova/virt/libvirt/config.py +++ b/nova/virt/libvirt/config.py @@ -37,7 +37,7 @@ from nova.virt import hardware # Namespace to use for Nova specific metadata items in XML -NOVA_NS = "http://openstack.org/xmlns/libvirt/nova/1.0" +NOVA_NS = "http://openstack.org/xmlns/libvirt/nova/1.1" class LibvirtConfigObject(object): @@ -3229,6 +3229,7 @@ class LibvirtConfigGuestMetaNovaInstance(LibvirtConfigObject): self.owner = None self.roottype = None self.rootid = None + self.ports = None def format_dom(self): meta = super(LibvirtConfigGuestMetaNovaInstance, self).format_dom() @@ -3252,6 +3253,8 @@ class LibvirtConfigGuestMetaNovaInstance(LibvirtConfigObject): root.set("type", self.roottype) root.set("uuid", str(self.rootid)) meta.append(root) + if self.ports is not None: + meta.append(self.ports.format_dom()) return meta @@ -3405,3 +3408,53 @@ class LibvirtConfigGuestVPMEM(LibvirtConfigGuestDevice): for sub in list(c): if sub.tag == "size": self.target_size = sub.text + + +class LibvirtConfigGuestMetaNovaPorts(LibvirtConfigObject): + + def __init__(self, ports=None): + super(LibvirtConfigGuestMetaNovaPorts, self).__init__( + root_name="ports", ns_prefix="nova", ns_uri=NOVA_NS) + + self.ports = ports + + def format_dom(self): + meta = self._new_node("ports") + for port in self.ports or []: + meta.append(port.format_dom()) + return meta + + +class LibvirtConfigGuestMetaNovaPort(LibvirtConfigObject): + + def __init__(self, uuid, ips=None): + super(LibvirtConfigGuestMetaNovaPort, self).__init__( + root_name="port", ns_prefix="nova", ns_uri=NOVA_NS) + + self.uuid = uuid + self.ips = ips + + def format_dom(self): + meta = self._new_node("port") + meta.set("uuid", str(self.uuid)) + for ip in self.ips or []: + meta.append(ip.format_dom()) + return meta + + +class LibvirtConfigGuestMetaNovaIp(LibvirtConfigObject): + + def __init__(self, ip_type, address, ip_version): + super(LibvirtConfigGuestMetaNovaIp, self).__init__( + root_name="ip", ns_prefix="nova", ns_uri=NOVA_NS) + + self.ip_type = ip_type + self.address = address + self.ip_version = ip_version + + def format_dom(self): + meta = self._new_node("ip") + meta.set("type", str(self.ip_type)) + meta.set("address", str(self.address)) + meta.set("ipVersion", str(self.ip_version)) + return meta diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index b5aa5287c913..9a03731d06dc 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -2226,6 +2226,12 @@ class LibvirtDriver(driver.ComputeDriver): self.detach_interface(context, instance, vif) raise exception.InterfaceAttachFailed( instance_uuid=instance.uuid) + try: + guest.set_metadata( + self._get_guest_config_meta( + instance, instance.get_network_info())) + except libvirt.libvirtError: + LOG.warning('updating libvirt metadata failed.', instance=instance) def detach_interface(self, context, instance, vif): guest = self._host.get_guest(instance) @@ -2319,6 +2325,17 @@ class LibvirtDriver(driver.ComputeDriver): # are failed to detach due to race conditions the unplug is # necessary for the same reason self.vif_driver.unplug(instance, vif) + try: + # NOTE(nmiki): In order for the interface to be removed from + # network_info, the nova-compute process need to wait for + # processing on the neutron side. + # Here, I simply exclude the target VIF from metadata. + network_info = list(filter(lambda info: info['id'] != vif['id'], + instance.get_network_info())) + guest.set_metadata( + self._get_guest_config_meta(instance, network_info)) + except libvirt.libvirtError: + LOG.warning('updating libvirt metadata failed.', instance=instance) def _create_snapshot_metadata(self, image_meta, instance, img_fmt, snp_name): @@ -4941,7 +4958,7 @@ class LibvirtDriver(driver.ComputeDriver): return dev - def _get_guest_config_meta(self, instance): + def _get_guest_config_meta(self, instance, network_info): """Get metadata config for guest.""" meta = vconfig.LibvirtConfigGuestMetaNovaInstance() @@ -4972,6 +4989,18 @@ class LibvirtDriver(driver.ComputeDriver): meta.flavor = fmeta + ports = [] + for vif in network_info: + ips = [] + for subnet in vif.get('network', {}).get('subnets', []): + for ip in subnet.get('ips', []): + ips.append(vconfig.LibvirtConfigGuestMetaNovaIp( + ip.get('type'), ip.get('address'), ip.get('version'))) + ports.append(vconfig.LibvirtConfigGuestMetaNovaPort( + vif.get('id'), ips=ips)) + + meta.ports = vconfig.LibvirtConfigGuestMetaNovaPorts(ports) + return meta @staticmethod @@ -6094,7 +6123,8 @@ class LibvirtDriver(driver.ComputeDriver): guest_numa_config.numatune, flavor, image_meta) - guest.metadata.append(self._get_guest_config_meta(instance)) + guest.metadata.append(self._get_guest_config_meta( + instance, network_info)) guest.idmaps = self._get_guest_idmaps() for event in self._supported_perf_events: diff --git a/nova/virt/libvirt/guest.py b/nova/virt/libvirt/guest.py index ef0d6e0bd649..5143f142eb73 100644 --- a/nova/virt/libvirt/guest.py +++ b/nova/virt/libvirt/guest.py @@ -303,6 +303,28 @@ class Guest(object): LOG.debug("attach device xml: %s", device_xml) self._domain.attachDeviceFlags(device_xml, flags=flags) + def set_metadata(self, metadata, persistent=False, live=False): + """Set metadata to the guest. + + Please note that this function completely replaces the existing + metadata. The scope of the replacement is limited to the Nova-specific + XML Namespace. + + :param metadata: A LibvirtConfigGuestMetaNovaInstance + :param persistent: A bool to indicate whether the change is + persistent or not + :param live: A bool to indicate whether it affect the guest + in running state + """ + flags = persistent and libvirt.VIR_DOMAIN_AFFECT_CONFIG or 0 + flags |= live and libvirt.VIR_DOMAIN_AFFECT_LIVE or 0 + + metadata_xml = metadata.to_xml() + LOG.debug("set metadata xml: %s", metadata_xml) + self._domain.setMetadata(libvirt.VIR_DOMAIN_METADATA_ELEMENT, + metadata_xml, "instance", + vconfig.NOVA_NS, flags=flags) + def get_config(self): """Returns the config instance for a guest diff --git a/releasenotes/notes/bp-libvirt-driver-ip-metadata-8754c623dbd13126.yaml b/releasenotes/notes/bp-libvirt-driver-ip-metadata-8754c623dbd13126.yaml new file mode 100644 index 000000000000..e62e791682b5 --- /dev/null +++ b/releasenotes/notes/bp-libvirt-driver-ip-metadata-8754c623dbd13126.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added IP addresses to the metadata in libvirt XML. If an instance has more + than one IP address, enumerate those IP addresses. The port attach or + detach is performed dynamically after the creation of the instance. Every + time there is a change, it is reflected in the contents of the XML.