From 838370a49014351051bbef2d1c2ada1f47ac2bfb Mon Sep 17 00:00:00 2001 From: Nobuhiro MIKI Date: Wed, 9 Sep 2020 11:14:51 +0900 Subject: [PATCH] libvirt: add IP address to libvirt guest metadata Libvirt XML contains useful configuration information such as instance names, flavors and images as metadata. This change extends this metadata to include the IP addresses of the instances. Example: ... ... Change-Id: I45f1df4935905170957c2ea2496c8a698a7464a2 blueprint: libvirt-driver-ip-metadata Signed-off-by: Nobuhiro MIKI --- nova/tests/unit/virt/libvirt/fakelibvirt.py | 8 + nova/tests/unit/virt/libvirt/test_config.py | 35 ++++- nova/tests/unit/virt/libvirt/test_driver.py | 140 +++++++++++++++++- nova/tests/unit/virt/libvirt/test_guest.py | 33 +++++ nova/virt/libvirt/config.py | 55 ++++++- nova/virt/libvirt/driver.py | 34 ++++- nova/virt/libvirt/guest.py | 22 +++ ...t-driver-ip-metadata-8754c623dbd13126.yaml | 7 + 8 files changed, 326 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/bp-libvirt-driver-ip-metadata-8754c623dbd13126.yaml 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.