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:
<metadata>
  <nova:instance xmlns:nova="http://openstack.org/xmlns/libvirt/nova/1.1">
    ...
    <nova:ports>
      <nova:port uuid="567a4527-b0e4-4d0a-bcc2-71fda37897f7">
        <nova:ip type="fixed" address="192.168.1.1" ipVersion="4"/>
        <nova:ip type="fixed" address="fe80::f95c:b030:7094" ipVersion="6"/>
        <nova:ip type="floating" address="11.22.33.44" ipVersion="4"/>
      </nova:port>
    </nova:ports>
    ...
  </nova:instance>
</metadata>

Change-Id: I45f1df4935905170957c2ea2496c8a698a7464a2
blueprint: libvirt-driver-ip-metadata
Signed-off-by: Nobuhiro MIKI <nmiki@yahoo-corp.jp>
This commit is contained in:
Nobuhiro MIKI 2020-09-09 11:14:51 +09:00
parent 67c76de5f4
commit 838370a490
No known key found for this signature in database
GPG Key ID: F0C80E46D174038A
8 changed files with 326 additions and 8 deletions

View File

@ -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):

View File

@ -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, """
<nova:instance xmlns:nova='http://openstack.org/xmlns/libvirt/nova/1.0'>
<nova:instance xmlns:nova='http://openstack.org/xmlns/libvirt/nova/1.1'>
<nova:package version="2014.2.3"/>
<nova:name>moonbuggy</nova:name>
<nova:creationTime>2009-02-13 23:31:30</nova:creationTime>
@ -3666,6 +3687,18 @@ class LibvirtConfigGuestMetadataNovaTest(LibvirtConfigBaseTest):
uuid="f241e906-010e-4917-ae81-53f4fb8aa021">moonshot</nova:project>
</nova:owner>
<nova:root type="image" uuid="fe55c69a-8b2e-4bbc-811a-9ad2023a0426"/>
<nova:ports>
<nova:port uuid="567a4527-b0e4-4d0a-bcc2-71fda37897f7">
<nova:ip type="fixed" address="192.168.1.1" ipVersion="4"/>
<nova:ip type="fixed" address="fe80::f95c:b030:7094" ipVersion="6"/>
<nova:ip type="floating" address="11.22.33.44" ipVersion="4"/>
</nova:port>
<nova:port uuid="a3ca97e2-0cf9-4159-9bfc-afd55bc13ead">
<nova:ip type="fixed" address="10.0.0.1" ipVersion="4"/>
<nova:ip type="fixed" address="fdf8:f53b:82e4::52" ipVersion="6"/>
<nova:ip type="floating" address="1.2.3.4" ipVersion="4"/>
</nova:port>
</nova:ports>
</nova:instance>
""")

View File

@ -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='</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())

View File

@ -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 = "</xml>"
self.guest.set_metadata(meta)
self.domain.setMetadata.assert_called_once_with(
fakelibvirt.VIR_DOMAIN_METADATA_ELEMENT, "</xml>", "instance",
vconfig.NOVA_NS, flags=0)
def test_set_metadata_persistent(self):
meta = mock.Mock(spec=vconfig.LibvirtConfigGuestMetaNovaInstance)
meta.to_xml.return_value = "</xml>"
self.guest.set_metadata(meta, persistent=True)
self.domain.setMetadata.assert_called_once_with(
fakelibvirt.VIR_DOMAIN_METADATA_ELEMENT, "</xml>", "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 = "</xml>"
self.guest.set_metadata(meta, live=True)
self.domain.setMetadata.assert_called_once_with(
fakelibvirt.VIR_DOMAIN_METADATA_ELEMENT, "</xml>", "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 = "</xml>"
self.guest.set_metadata(meta, persistent=True, live=True)
self.domain.setMetadata.assert_called_once_with(
fakelibvirt.VIR_DOMAIN_METADATA_ELEMENT, "</xml>", "instance",
vconfig.NOVA_NS, flags=fakelibvirt.VIR_DOMAIN_AFFECT_LIVE |
fakelibvirt.VIR_DOMAIN_AFFECT_CONFIG)
class GuestBlockTestCase(test.NoDBTestCase):

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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.