Publish image meta from notifications and pollsters (if available)

Currently, Ceilometer is only able to use the image_meta
structure from notifications provided by Nova, since the relevant
metadata is not written to the guests' libvirt XML files.

A new patch is available that adds the same metadata that gets
exposed in Nova notifications to libvirt XML metadata, so this
change implements reading this metadata and adding it to the
Ceilometer samples.

Since image_meta is now available from the compute pollsters,
meters.yaml has been also updated to also publish image_meta
from compute notification meter samples.

Processing for generating image_meta was also implemented when using
Nova client to discover instances (instance_discovery_method is
set to workload_partitioning), using the image metadata already
fetched using a Glance API request. This seems to work okay, but
has the disadvantages of a) only being available for ephemeral
instances (booted from image), and b) possibly being out of date
with what the instance is actually running, as the Glance image may
have been updated after the instance was launched
(the libvirt metadata contains what the instance was *actually*
configured with).

Signed-off-by: Callum Dickinson <callum.dickinson@catalystcloud.nz>
Depends-On: https://review.opendev.org/c/openstack/nova/+/942766
Story: #2011371
Task: #51740
Change-Id: I7d8164f80322a9914b15d9a6991b2bc91b158ff3
This commit is contained in:
Callum Dickinson
2025-02-27 16:51:22 +13:00
parent a5478e9270
commit a00f7cf695
10 changed files with 347 additions and 15 deletions

View File

@@ -217,12 +217,45 @@ class InstanceDiscovery(plugin_base.DiscoveryBase):
if extra_specs is not None:
flavor["extra_specs"] = extra_specs
# The image description is partial, but Gnocchi only care about
# the id, so we are fine
image_xml = metadata_xml.find("./root[@type='image']")
image = ({'id': image_xml.attrib['uuid']}
if image_xml is not None else None)
image_meta_xml = metadata_xml.find("./image")
if image_meta_xml is not None:
# If the <image> element exists at all, Nova supports
# image_meta in libvirt metadata. Add it to the instance
# attributes even if all the required values are empty.
image_meta = {}
base_image_ref = image_meta_xml.attrib.get("uuid")
if base_image_ref is not None:
image_meta["base_image_ref"] = base_image_ref
# The following properties get special treatment
# because they are set as such in SM_INHERITABLE_KEYS,
# as defined in nova/utils.py.
container_format_xml = image_meta_xml.find(
"./containerFormat")
if container_format_xml is not None:
image_meta["container_format"] = (
container_format_xml.text)
disk_format_xml = image_meta_xml.find("./diskFormat")
if disk_format_xml is not None:
image_meta["disk_format"] = disk_format_xml.text
min_disk_xml = image_meta_xml.find("./minDisk")
if min_disk_xml is not None:
image_meta["min_disk"] = min_disk_xml.text
min_ram_xml = image_meta_xml.find("./minRam")
if min_ram_xml is not None:
image_meta["min_ram"] = min_ram_xml.text
# Get additional properties defined in image_meta.
properties_xml = image_meta_xml.find("./properties")
if properties_xml is not None:
for prop in properties_xml.findall("./property"):
image_meta[prop.attrib["name"]] = prop.text
else:
# None for "no image_meta found".
image_meta = None
# Getting the server metadata requires expensive Nova API
# queries, and may potentially contain sensitive user info,
# so it is only fetched when configured to do so.
@@ -276,6 +309,8 @@ class InstanceDiscovery(plugin_base.DiscoveryBase):
# 'ramdisk_id',
# some image detail
}
if image_meta is not None:
instance_data["image_meta"] = image_meta
LOG.debug("instance data: %s", instance_data)
instances.append(NovaLikeServer(**instance_data))

View File

@@ -59,6 +59,8 @@ def _get_metadata_from_object(conf, instance):
metadata['image'] = None
metadata['image_ref'] = None
metadata['image_ref_url'] = None
if hasattr(instance, 'image_meta') and instance.image_meta:
metadata['image_meta'] = instance.image_meta
for name in INSTANCE_PROPERTIES:
if hasattr(instance, name):

View File

@@ -201,6 +201,7 @@ metric:
flavor_name: $.payload.instance_type
display_name: $.payload.display_name
image_ref: $.payload.image_meta.base_image_ref
image_meta: $.payload.image_meta
launched_at: $.payload.launched_at
created_at: $.payload.created_at
deleted_at: $.payload.deleted_at

View File

@@ -128,6 +128,17 @@ class Client:
ameta = image_metadata.get(attr) if image_metadata else default
setattr(instance, attr, ameta)
if image:
image_meta = {"base_image_ref": iid}
# Notifications and libvirt XML metadata return all
# image_meta values as strings. Do the same here.
image_meta.update((k, str(v))
for k, v in image.items()
if k not in ('id', 'name', 'metadata'))
else:
image_meta = {}
instance.image_meta = image_meta
@logged
def instance_get_all_by_host(self, hostname, since=None):
"""Returns list of instances on particular host.

View File

@@ -49,6 +49,15 @@ class TestPollsterBase(base.BaseTestCase):
'fqdn': 'vm_fqdn',
'metering.stack': '2cadc4b4-8789-123c-b4eg-edd2f0a9c128',
'project_cos': 'dev'}
self.instance.image = {'id': '0ff4d118-4947-49e6-963a-7a28e65f3f11'}
self.instance.image_meta = {
'base_image_ref': self.instance.image['id'],
'container_format': 'bare',
'disk_format': 'raw',
'min_disk': '1',
'min_ram': '0',
'os_distro': 'ubuntu',
'os_type': 'linux'}
self.useFixture(fixtures.MockPatch(
'ceilometer.compute.virt.inspector.get_hypervisor_inspector',

View File

@@ -67,6 +67,10 @@ class TestCPUPollster(base.TestPollsterBase):
self.assertIsNone(samples[0].resource_metadata['task_state'])
self.assertEqual(self.instance.flavor,
samples[0].resource_metadata['flavor'])
self.assertEqual(self.instance.image['id'],
samples[0].resource_metadata['image_ref'])
self.assertEqual(self.instance.image_meta,
samples[0].resource_metadata['image_meta'])
def test_get_reserved_metadata_with_keys(self):
self.CONF.set_override('reserved_metadata_keys', ['fqdn'])

View File

@@ -66,6 +66,13 @@ class TestLocationMetadata(base.BaseTestCase):
'image': {'id': 1,
'links': [{"rel": "bookmark",
'href': 2}]},
'image_meta': {'base_image_ref': 1,
'container_format': 'bare',
'disk_format': 'raw',
'min_disk': '20',
'min_ram': '0',
'os_distro': 'ubuntu',
'os_type': 'linux'},
'hostId': '1234-5678',
'OS-EXT-SRV-ATTR:host': 'host-test',
'flavor': {'name': 'm1.tiny',
@@ -105,6 +112,10 @@ class TestLocationMetadata(base.BaseTestCase):
'metadata']['metering.autoscale.group'][:256]
self.assertEqual(expected, user_metadata['autoscale_group'])
self.assertEqual(1, len(user_metadata))
self.assertEqual(self.INSTANCE_PROPERTIES['image']['id'],
md['image_ref'])
self.assertEqual(self.INSTANCE_PROPERTIES['image_meta'],
md['image_meta'])
def test_metadata_empty_image(self):
self.INSTANCE_PROPERTIES['image'] = None
@@ -121,3 +132,29 @@ class TestLocationMetadata(base.BaseTestCase):
md = util._get_metadata_from_object(self.CONF, self.instance)
self.assertEqual(1, md['image_ref'])
self.assertIsNone(md['image_ref_url'])
def test_metadata_image_meta_volume_image(self):
self.INSTANCE_PROPERTIES['image_meta']['base_image_ref'] = ''
self.instance = FauxInstance(**self.INSTANCE_PROPERTIES)
md = util._get_metadata_from_object(self.CONF, self.instance)
self.assertEqual(self.INSTANCE_PROPERTIES['image_meta'],
md['image_meta'])
def test_metadata_image_meta_volume_no_image(self):
self.INSTANCE_PROPERTIES['image_meta'] = {'base_image_ref': ''}
self.instance = FauxInstance(**self.INSTANCE_PROPERTIES)
md = util._get_metadata_from_object(self.CONF, self.instance)
self.assertEqual(self.INSTANCE_PROPERTIES['image_meta'],
md['image_meta'])
def test_metadata_image_meta_none(self):
self.INSTANCE_PROPERTIES['image_meta'] = None
self.instance = FauxInstance(**self.INSTANCE_PROPERTIES)
md = util._get_metadata_from_object(self.CONF, self.instance)
self.assertNotIn('image_meta', md)
def test_metadata_image_meta_noexist(self):
del self.INSTANCE_PROPERTIES['image_meta']
self.instance = FauxInstance(**self.INSTANCE_PROPERTIES)
md = util._get_metadata_from_object(self.CONF, self.instance)
self.assertNotIn('image_meta', md)

View File

@@ -40,6 +40,16 @@ LIBVIRT_METADATA_XML = """
<extraSpec name="hw_rng:allowed">true</extraSpec>
</extraSpecs>
</flavor>
<image uuid="bdaf114a-35e9-4163-accd-226d5944bf11">
<containerFormat>bare</containerFormat>
<diskFormat>raw</diskFormat>
<minDisk>1</minDisk>
<minRam>0</minRam>
<properties>
<property name="os_distro">ubuntu</property>
<property name="os_type">linux</property>
</properties>
</image>
<owner>
<user uuid="a1f4684e58bd4c88aefd2ecb0783b497">admin</user>
<project uuid="d99c829753f64057bc0f2030da309943">admin</project>
@@ -155,6 +165,63 @@ LIBVIRT_METADATA_XML_NO_FLAVOR_EXTRA_SPECS = """
</instance>
"""
LIBVIRT_METADATA_XML_FROM_VOLUME_IMAGE = """
<instance>
<package version="14.0.0"/>
<name>test.dom.com</name>
<creationTime>2016-11-16 07:35:06</creationTime>
<flavor name="m1.tiny" id="eba4213d-3c6c-4b5f-8158-dd0022d71d62">
<memory>512</memory>
<disk>1</disk>
<swap>0</swap>
<ephemeral>0</ephemeral>
<vcpus>1</vcpus>
<extraSpecs>
<extraSpec name="hw_rng:allowed">true</extraSpec>
</extraSpecs>
</flavor>
<image uuid="">
<containerFormat>bare</containerFormat>
<diskFormat>raw</diskFormat>
<minDisk>1</minDisk>
<minRam>0</minRam>
<properties>
<property name="os_distro">ubuntu</property>
<property name="os_type">linux</property>
</properties>
</image>
<owner>
<user uuid="a1f4684e58bd4c88aefd2ecb0783b497">admin</user>
<project uuid="d99c829753f64057bc0f2030da309943">admin</project>
</owner>
</instance>
"""
LIBVIRT_METADATA_XML_FROM_VOLUME_NO_IMAGE = """
<instance>
<package version="14.0.0"/>
<name>test.dom.com</name>
<creationTime>2016-11-16 07:35:06</creationTime>
<flavor name="m1.tiny" id="eba4213d-3c6c-4b5f-8158-dd0022d71d62">
<memory>512</memory>
<disk>1</disk>
<swap>0</swap>
<ephemeral>0</ephemeral>
<vcpus>1</vcpus>
<extraSpecs>
<extraSpec name="hw_rng:allowed">true</extraSpec>
</extraSpecs>
</flavor>
<image uuid="">
<properties></properties>
</image>
<owner>
<user uuid="a1f4684e58bd4c88aefd2ecb0783b497">admin</user>
<project uuid="d99c829753f64057bc0f2030da309943">admin</project>
</owner>
</instance>
"""
LIBVIRT_DESC_XML = """
<domain type='kvm' id='1'>
<name>instance-00000001</name>
@@ -407,6 +474,33 @@ class TestDiscovery(base.BaseTestCase):
self.assertEqual("x86_64", metadata["architecture"])
self.assertEqual({"server_group": "group1"},
metadata["user_metadata"])
self.assertEqual({"id"},
set(metadata["image"].keys()))
self.assertEqual("bdaf114a-35e9-4163-accd-226d5944bf11",
metadata["image"]["id"])
self.assertIn("image_meta", metadata)
self.assertEqual({"base_image_ref",
"container_format",
"disk_format",
"min_disk",
"min_ram",
"os_distro",
"os_type"},
set(metadata["image_meta"].keys()))
self.assertEqual("bdaf114a-35e9-4163-accd-226d5944bf11",
metadata["image_meta"]["base_image_ref"])
self.assertEqual("bare",
metadata["image_meta"]["container_format"])
self.assertEqual("raw",
metadata["image_meta"]["disk_format"])
self.assertEqual("1",
metadata["image_meta"]["min_disk"])
self.assertEqual("0",
metadata["image_meta"]["min_ram"])
self.assertEqual("ubuntu",
metadata["image_meta"]["os_distro"])
self.assertEqual("linux",
metadata["image_meta"]["os_type"])
@mock.patch.object(discovery.InstanceDiscovery, "get_server")
@mock.patch.object(discovery.InstanceDiscovery, "get_flavor_id")
@@ -472,6 +566,11 @@ class TestDiscovery(base.BaseTestCase):
self.assertEqual("x86_64", metadata["architecture"])
self.assertEqual({"server_group": "group1"},
metadata["user_metadata"])
self.assertEqual({"id"},
set(metadata["image"].keys()))
self.assertEqual("bdaf114a-35e9-4163-accd-226d5944bf11",
metadata["image"]["id"])
self.assertNotIn("image_meta", metadata)
@mock.patch.object(discovery.InstanceDiscovery, "get_server")
@mock.patch.object(discovery.InstanceDiscovery, "get_flavor_id")
@@ -730,6 +829,85 @@ class TestDiscovery(base.BaseTestCase):
"vcpus": 1},
metadata["flavor"])
@mock.patch("ceilometer.compute.virt.libvirt.utils."
"refresh_libvirt_connection")
def test_discovery_with_libvirt_from_volume_image(
self, mock_libvirt_conn):
self.CONF.set_override("instance_discovery_method",
"libvirt_metadata",
group="compute")
self.CONF.set_override("fetch_extra_metadata", False, group="compute")
mock_libvirt_conn.return_value = FakeConn(
domains=[
FakeDomain(metadata=LIBVIRT_METADATA_XML_FROM_VOLUME_IMAGE)])
dsc = discovery.InstanceDiscovery(self.CONF)
resources = dsc.discover(mock.MagicMock())
self.assertEqual(1, len(resources))
r = list(resources)[0]
s = util.make_sample_from_instance(self.CONF, r, "metric", "delta",
"carrot", 1)
self.assertEqual("a75c2fa5-6c03-45a8-bbf7-b993cfcdec27",
s.resource_id)
self.assertEqual("d99c829753f64057bc0f2030da309943",
s.project_id)
self.assertEqual("a1f4684e58bd4c88aefd2ecb0783b497",
s.user_id)
metadata = s.resource_metadata
self.assertIsNone(metadata["image"])
self.assertIn("image_meta", metadata)
self.assertEqual({"base_image_ref",
"container_format",
"disk_format",
"min_disk",
"min_ram",
"os_distro",
"os_type"},
set(metadata["image_meta"].keys()))
self.assertEqual("",
metadata["image_meta"]["base_image_ref"])
self.assertEqual("bare",
metadata["image_meta"]["container_format"])
self.assertEqual("raw",
metadata["image_meta"]["disk_format"])
self.assertEqual("1",
metadata["image_meta"]["min_disk"])
self.assertEqual("0",
metadata["image_meta"]["min_ram"])
self.assertEqual("ubuntu",
metadata["image_meta"]["os_distro"])
self.assertEqual("linux",
metadata["image_meta"]["os_type"])
@mock.patch("ceilometer.compute.virt.libvirt.utils."
"refresh_libvirt_connection")
def test_discovery_with_libvirt_from_volume_no_image(
self, mock_libvirt_conn):
self.CONF.set_override("instance_discovery_method",
"libvirt_metadata",
group="compute")
self.CONF.set_override("fetch_extra_metadata", False, group="compute")
mock_libvirt_conn.return_value = FakeConn(
domains=[
FakeDomain(
metadata=LIBVIRT_METADATA_XML_FROM_VOLUME_NO_IMAGE)])
dsc = discovery.InstanceDiscovery(self.CONF)
resources = dsc.discover(mock.MagicMock())
self.assertEqual(1, len(resources))
r = list(resources)[0]
s = util.make_sample_from_instance(self.CONF, r, "metric", "delta",
"carrot", 1)
metadata = s.resource_metadata
self.assertIsNone(metadata["image"])
self.assertIn("image_meta", metadata)
self.assertEqual({"base_image_ref"},
set(metadata["image_meta"].keys()))
self.assertEqual("",
metadata["image_meta"]["base_image_ref"])
def test_discovery_with_legacy_resource_cache_cleanup(self):
self.CONF.set_override("instance_discovery_method", "naive",
group="compute")

View File

@@ -22,6 +22,12 @@ from ceilometer import nova_client
from ceilometer import service
class FauxImage(dict):
def __getattr__(self, key):
return self[key]
class TestNovaClient(base.BaseTestCase):
def setUp(self):
@@ -51,25 +57,34 @@ class TestNovaClient(base.BaseTestCase):
def fake_images_get(self, *args, **kwargs):
self._images_count += 1
a = mock.MagicMock()
a.id = args[0]
image_id = args[0]
image_details = {
1: ('ubuntu-12.04-x86', dict(kernel_id=11, ramdisk_id=21)),
2: ('centos-5.4-x64', dict(kernel_id=12, ramdisk_id=22)),
3: ('rhel-6-x64', None),
4: ('rhel-6-x64', dict()),
5: ('rhel-6-x64', dict(kernel_id=11)),
6: ('rhel-6-x64', dict(ramdisk_id=21))
# NOTE(callumdickinson): Real image IDs are UUIDs, not integers,
# so the actual code runs assuming the IDs are strings.
1: ('ubuntu-12.04-x86',
dict(kernel_id=11, ramdisk_id=21),
dict(container_format='bare',
disk_format='raw',
min_disk=1,
min_ram=0,
os_distro='ubuntu',
os_type='linux')),
2: ('centos-5.4-x64', dict(kernel_id=12, ramdisk_id=22), dict()),
3: ('rhel-6-x64', None, dict()),
4: ('rhel-6-x64', dict(), dict()),
5: ('rhel-6-x64', dict(kernel_id=11), dict()),
6: ('rhel-6-x64', dict(ramdisk_id=21), dict()),
}
if a.id in image_details:
a.name = image_details[a.id][0]
a.metadata = image_details[a.id][1]
if image_id in image_details:
return FauxImage(
id=image_id,
name=image_details[image_id][0],
metadata=image_details[image_id][1],
**image_details[image_id][2])
else:
raise glanceclient.exc.HTTPNotFound('foobar')
return a
@staticmethod
def fake_servers_list(*args, **kwargs):
a = mock.MagicMock()
@@ -151,6 +166,14 @@ class TestNovaClient(base.BaseTestCase):
self.assertEqual('m1.tiny', instance.flavor['name'])
self.assertEqual(11, instance.kernel_id)
self.assertEqual(21, instance.ramdisk_id)
self.assertEqual({'base_image_ref': 1,
'container_format': 'bare',
'disk_format': 'raw',
'min_disk': '1',
'min_ram': '0',
'os_distro': 'ubuntu',
'os_type': 'linux'},
instance.image_meta)
def test_with_flavor_and_image_unknown_image(self):
instances = self.fake_servers_list_unknown_image()
@@ -160,6 +183,7 @@ class TestNovaClient(base.BaseTestCase):
self.assertNotEqual(instance.flavor['name'], 'unknown-id-666')
self.assertIsNone(instance.kernel_id)
self.assertIsNone(instance.ramdisk_id)
self.assertEqual({}, instance.image_meta)
def test_with_flavor_and_image_unknown_flavor(self):
instances = self.fake_servers_list_unknown_flavor()
@@ -172,6 +196,14 @@ class TestNovaClient(base.BaseTestCase):
self.assertNotEqual(instance.image['name'], 'unknown-id-666')
self.assertEqual(11, instance.kernel_id)
self.assertEqual(21, instance.ramdisk_id)
self.assertEqual({'base_image_ref': 1,
'container_format': 'bare',
'disk_format': 'raw',
'min_disk': '1',
'min_ram': '0',
'os_distro': 'ubuntu',
'os_type': 'linux'},
instance.image_meta)
def test_with_flavor_and_image_none_metadata(self):
instances = self.fake_servers_list_image_missing_metadata(3)

View File

@@ -0,0 +1,23 @@
---
features:
- |
The ``image_meta`` metadata structure for compute meters was
formerly only available via the notification meters. When using
Nova 2025.2 Flamingo or later, ``image_meta`` is now also supplied by
the compute pollsters. This is now possible due to the addition of
the relevant metadata to the libvirt guest XML.
- |
The built-in ``meters.yaml`` has been updated to publish the
``image_meta`` metadata attribute for compute notification meter
samples by default.
upgrade:
- |
``meters.yaml`` has been updated to add ``image_meta`` to compute meter
samples by default.
- |
In order for the new image metadata attributes to start being populated
from libvirt metadata in pollster samples, Nova must be upgraded to
2025.2 Flamingo or later (older versions are still backwards compatible,
but the new attributes will not be available via pollster samples).
Existing instances will need to be shelved-and-unshelved or cold migrated
for the metadata to be populated.