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
This commit is contained in:
Callum Dickinson
2025-02-13 18:43:36 +13:00
parent 0b3eca2c76
commit 36b40ed7de
6 changed files with 202 additions and 2 deletions

View File

@@ -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(),
)

View File

@@ -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']))

View File

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

View File

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

View File

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

View File

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