From 36b40ed7deec6fcf81f8a9bfeead03bdb48362a8 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 13 Feb 2025 18:43:36 +1300 Subject: [PATCH] Add the disk.ephemeral.size and disk.root.size pollsters The disk.ephemeral.size meter publishes the size of an instance's ephemeral disk (in GB). The disk.root.size meter publishes the size of an instance's root volume (also in GB). Both of these meters are currently available via notifications only. This information is also available in the instance's local VM metadata, so add new pollsters to publish them. This allows for publishing the these two meters with measures at regular intervals if required, rather than relying solely on Nova to send notifications correctly. If a given instance does not change very often, the compute.instance.exists notification would only publish values to these metrics just once per hour, which might not always be sufficient to reliably supply them. These pollsters can be disabled if required by adding "!disk.ephemeral.size" and "!disk.root.size" to the applicable catch-all source in polling.yaml. Change-Id: I1edce6a4f366192346e9400065f5890dde1fc588 --- ceilometer/compute/pollsters/__init__.py | 41 ++++++ ceilometer/compute/pollsters/disk.py | 17 +++ .../tests/unit/compute/pollsters/test_disk.py | 130 ++++++++++++++++++ doc/source/admin/telemetry-measurements.rst | 6 +- ...-disk-size-pollsters-6b819d067f9cf736.yaml | 8 ++ setup.cfg | 2 + 6 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 ceilometer/tests/unit/compute/pollsters/test_disk.py create mode 100644 releasenotes/notes/add-disk-size-pollsters-6b819d067f9cf736.yaml diff --git a/ceilometer/compute/pollsters/__init__.py b/ceilometer/compute/pollsters/__init__.py index 000a0222fb..9450802a6a 100644 --- a/ceilometer/compute/pollsters/__init__.py +++ b/ceilometer/compute/pollsters/__init__.py @@ -170,3 +170,44 @@ class GenericComputePollster(plugin_base.PollsterBase): 'Could not get %(name)s events for %(id)s: %(e)s', { 'name': self.sample_name, 'id': instance.id, 'e': err}, exc_info=True) + + +class InstanceMetadataPollster(plugin_base.PollsterBase): + """A base class for implementing a pollster using instance metadata. + + This metadata is originally supplied by Nova, but if + instance_discovery_method is set to libvirt_metadata, + metadata is fetched from the local libvirt socket, + just like with the standard compute pollsters. + """ + + sample_name = None + sample_unit = '' + sample_type = sample.TYPE_GAUGE + + @property + def default_discovery(self): + return 'local_instances' + + def get_resource_id(self, instance): + return instance.id + + def get_volume(self, instance): + raise ceilometer.NotImplementedError + + def get_additional_metadata(self, instance): + return {} + + def get_samples(self, manager, cache, resources): + for instance in resources: + yield util.make_sample_from_instance( + self.conf, + instance, + name=self.sample_name, + unit=self.sample_unit, + type=self.sample_type, + resource_id=self.get_resource_id(instance), + volume=self.get_volume(instance), + additional_metadata=self.get_additional_metadata(instance), + monotonic_time=now(), + ) diff --git a/ceilometer/compute/pollsters/disk.py b/ceilometer/compute/pollsters/disk.py index 90e8fcfca1..0ae9adc8e0 100644 --- a/ceilometer/compute/pollsters/disk.py +++ b/ceilometer/compute/pollsters/disk.py @@ -91,3 +91,20 @@ class PerDeviceDiskWriteLatencyPollster(PerDeviceDiskPollster): sample_type = sample.TYPE_CUMULATIVE sample_unit = 'ns' sample_stats_key = 'wr_total_times' + + +class EphemeralSizePollster(pollsters.InstanceMetadataPollster): + sample_name = 'disk.ephemeral.size' + sample_unit = 'GB' + + def get_volume(self, instance): + return int(instance.flavor['ephemeral']) + + +class RootSizePollster(pollsters.InstanceMetadataPollster): + sample_name = 'disk.root.size' + sample_unit = 'GB' + + def get_volume(self, instance): + return (int(instance.flavor['disk']) + - int(instance.flavor['ephemeral'])) diff --git a/ceilometer/tests/unit/compute/pollsters/test_disk.py b/ceilometer/tests/unit/compute/pollsters/test_disk.py new file mode 100644 index 0000000000..831a35bfc5 --- /dev/null +++ b/ceilometer/tests/unit/compute/pollsters/test_disk.py @@ -0,0 +1,130 @@ +# Copyright 2025 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from ceilometer.compute.pollsters import disk +from ceilometer.polling import manager +from ceilometer.tests.unit.compute.pollsters import base + + +class TestDiskPollsterBase(base.TestPollsterBase): + + TYPE = 'gauge' + + def setUp(self): + super().setUp() + self.instances = self._get_fake_instances() + + def _get_fake_instances(self, ephemeral=0): + instances = [] + for i in [1, 2]: + instance = mock.MagicMock() + instance.name = f'instance-{i}' + setattr(instance, 'OS-EXT-SRV-ATTR:instance_name', + instance.name) + instance.id = i + instance.flavor = {'name': 'm1.small', 'id': 2, 'vcpus': 1, + 'ram': 512, 'disk': 20, 'ephemeral': ephemeral} + instance.status = 'active' + instances.append(instance) + return instances + + def _check_get_samples(self, + factory, + name, + instances=None, + expected_count=2): + pollster = factory(self.CONF) + mgr = manager.AgentManager(0, self.CONF) + samples = list(pollster.get_samples(mgr, + {}, + instances or self.instances)) + self.assertGreater(len(samples), 0) + self.assertEqual({name}, set(s.name for s in samples), + (f"Only samples for meter {name} " + "should be published")) + self.assertEqual(expected_count, len(samples)) + return samples + + +class TestDiskSizePollsters(TestDiskPollsterBase): + + TYPE = 'gauge' + + def test_ephemeral_disk_zero(self): + samples = { + sample.resource_id: sample + for sample in self._check_get_samples( + disk.EphemeralSizePollster, + 'disk.ephemeral.size', + expected_count=len(self.instances))} + for instance in self.instances: + with self.subTest(instance.name): + self.assertIn(instance.id, samples) + sample = samples[instance.id] + self.assertEqual(instance.flavor['ephemeral'], + sample.volume) + self.assertEqual(self.TYPE, sample.type) + + def test_ephemeral_disk_nonzero(self): + instances = self._get_fake_instances(ephemeral=10) + samples = { + sample.resource_id: sample + for sample in self._check_get_samples( + disk.EphemeralSizePollster, + 'disk.ephemeral.size', + instances=instances, + expected_count=len(instances))} + for instance in instances: + with self.subTest(instance.name): + self.assertIn(instance.id, samples) + sample = samples[instance.id] + self.assertEqual(instance.flavor['ephemeral'], + sample.volume) + self.assertEqual(self.TYPE, sample.type) + + def test_root_disk(self): + samples = { + sample.resource_id: sample + for sample in self._check_get_samples( + disk.RootSizePollster, + 'disk.root.size', + expected_count=len(self.instances))} + for instance in self.instances: + with self.subTest(instance.name): + self.assertIn(instance.id, samples) + sample = samples[instance.id] + self.assertEqual((instance.flavor['disk'] + - instance.flavor['ephemeral']), + sample.volume) + self.assertEqual(self.TYPE, sample.type) + + def test_root_disk_ephemeral_nonzero(self): + instances = self._get_fake_instances(ephemeral=10) + samples = { + sample.resource_id: sample + for sample in self._check_get_samples( + disk.RootSizePollster, + 'disk.root.size', + instances=instances, + expected_count=len(instances))} + for instance in instances: + with self.subTest(instance.name): + self.assertIn(instance.id, samples) + sample = samples[instance.id] + self.assertEqual((instance.flavor['disk'] + - instance.flavor['ephemeral']), + sample.volume) + self.assertEqual(self.TYPE, sample.type) diff --git a/doc/source/admin/telemetry-measurements.rst b/doc/source/admin/telemetry-measurements.rst index 661cf1fe1d..e116a35949 100644 --- a/doc/source/admin/telemetry-measurements.rst +++ b/doc/source/admin/telemetry-measurements.rst @@ -124,10 +124,12 @@ The following meters are collected for OpenStack Compute. | .bytes | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | disk.root\| Gauge | GB | instance | Notific\ | Libvirt | Size of root disk| -| .size | | | ID | ation | | | +| .size | | | ID | ation, \ | | | +| | | | | Pollster | | | +-----------+-------+------+----------+----------+---------+------------------+ | disk.ephe\| Gauge | GB | instance | Notific\ | Libvirt | Size of ephemeral| -| meral.size| | | ID | ation | | disk | +| meral.size| | | ID | ation, \ | | disk | +| | | | | Pollster | | | +-----------+-------+------+----------+----------+---------+------------------+ | disk.dev\ | Gauge | B | disk ID | Pollster | Libvirt | The amount of d\ | | ice.capa\ | | | | | | isk per device | diff --git a/releasenotes/notes/add-disk-size-pollsters-6b819d067f9cf736.yaml b/releasenotes/notes/add-disk-size-pollsters-6b819d067f9cf736.yaml new file mode 100644 index 0000000000..f3490a1772 --- /dev/null +++ b/releasenotes/notes/add-disk-size-pollsters-6b819d067f9cf736.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The ``disk.ephemeral.size`` meter is now published as a compute pollster, + in addition to the existing notification meter. + - | + The ``disk.root.size`` meter is now published as a compute pollster, + in addition to the existing notification meter. diff --git a/setup.cfg b/setup.cfg index 1f84d13c65..ea4995995d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -94,6 +94,8 @@ ceilometer.poll.compute = disk.device.capacity = ceilometer.compute.pollsters.disk:PerDeviceCapacityPollster disk.device.allocation = ceilometer.compute.pollsters.disk:PerDeviceAllocationPollster disk.device.usage = ceilometer.compute.pollsters.disk:PerDevicePhysicalPollster + disk.ephemeral.size = ceilometer.compute.pollsters.disk:EphemeralSizePollster + disk.root.size = ceilometer.compute.pollsters.disk:RootSizePollster perf.cpu.cycles = ceilometer.compute.pollsters.instance_stats:PerfCPUCyclesPollster perf.instructions = ceilometer.compute.pollsters.instance_stats:PerfInstructionsPollster perf.cache.references = ceilometer.compute.pollsters.instance_stats:PerfCacheReferencesPollster