diff --git a/ceilometer/compute/pollsters/disk.py b/ceilometer/compute/pollsters/disk.py index 85cb28153..54937c0f4 100644 --- a/ceilometer/compute/pollsters/disk.py +++ b/ceilometer/compute/pollsters/disk.py @@ -46,6 +46,10 @@ DiskLatencyData = collections.namedtuple('DiskLatencyData', ['disk_latency', 'per_disk_latency']) +DiskIOPSData = collections.namedtuple('DiskIOPSData', + ['iops_count', + 'per_disk_iops']) + @six.add_metaclass(abc.ABCMeta) class _Base(pollsters.BaseComputePollster): @@ -550,3 +554,84 @@ class PerDeviceDiskLatencyPollster(_DiskLatencyPollsterBase): resource_id="%s-%s" % (instance.id, disk) )) return samples + + +class _DiskIOPSPollsterBase(pollsters.BaseComputePollster): + + CACHE_KEY_DISK_IOPS = 'disk-iops' + + def _populate_cache(self, inspector, cache, instance): + i_cache = cache.setdefault(self.CACHE_KEY_DISK_IOPS, {}) + if instance.id not in i_cache: + iops = 0 + per_device_iops = {} + disk_iops_count = inspector.inspect_disk_iops(instance) + for disk, stats in disk_iops_count: + iops += stats.iops_count + per_device_iops[disk.device] = (stats.iops_count) + per_disk_iops = { + 'iops_count': per_device_iops + } + i_cache[instance.id] = DiskIOPSData( + iops, + per_disk_iops + ) + return i_cache[instance.id] + + @abc.abstractmethod + def _get_samples(self, instance, disk_rates_info): + """Return one or more Sample.""" + + def get_samples(self, manager, cache, resources): + for instance in resources: + try: + disk_iops_info = self._populate_cache( + self.inspector, + cache, + instance, + ) + for disk_iops in self._get_samples(instance, + disk_iops_info): + yield disk_iops + except virt_inspector.InstanceNotFoundException as err: + # Instance was deleted while getting samples. Ignore it. + LOG.debug(_('Exception while getting samples %s'), err) + except ceilometer.NotImplementedError: + # Selected inspector does not implement this pollster. + LOG.debug(_('%(inspector)s does not provide data for ' + '%(pollster)s'), + {'inspector': self.inspector.__class__.__name__, + 'pollster': self.__class__.__name__}) + except Exception as err: + instance_name = util.instance_name(instance) + LOG.exception(_('Ignoring instance %(name)s: %(error)s'), + {'name': instance_name, 'error': err}) + + +class DiskIOPSPollster(_DiskIOPSPollsterBase): + + def _get_samples(self, instance, disk_iops_info): + return [util.make_sample_from_instance( + instance, + name='disk.iops', + type=sample.TYPE_GAUGE, + unit='count/s', + volume=disk_iops_info.iops_count + )] + + +class PerDeviceDiskIOPSPollster(_DiskIOPSPollsterBase): + + def _get_samples(self, instance, disk_iops_info): + samples = [] + for disk, value in six.iteritems(disk_iops_info.per_disk_iops[ + 'iops_count']): + samples.append(util.make_sample_from_instance( + instance, + name='disk.device.iops', + type=sample.TYPE_GAUGE, + unit='count/s', + volume=value, + resource_id="%s-%s" % (instance.id, disk) + )) + return samples diff --git a/ceilometer/compute/virt/hyperv/inspector.py b/ceilometer/compute/virt/hyperv/inspector.py index 0edfbfc0c..d2100a877 100644 --- a/ceilometer/compute/virt/hyperv/inspector.py +++ b/ceilometer/compute/virt/hyperv/inspector.py @@ -90,3 +90,12 @@ class HyperVInspector(virt_inspector.Inspector): disk_latency=disk_metrics['disk_latency']) yield (disk, stats) + + def inspect_disk_iops(self, instance): + instance_name = util.instance_name(instance) + for disk_metrics in self._utils.get_disk_iops_count(instance_name): + disk = virt_inspector.Disk(device=disk_metrics['instance_id']) + stats = virt_inspector.DiskIOPSStats( + iops_count=disk_metrics['iops_count']) + + yield (disk, stats) diff --git a/ceilometer/compute/virt/hyperv/utilsv2.py b/ceilometer/compute/virt/hyperv/utilsv2.py index 26b3f75dc..208073fb9 100644 --- a/ceilometer/compute/virt/hyperv/utilsv2.py +++ b/ceilometer/compute/virt/hyperv/utilsv2.py @@ -58,6 +58,7 @@ class UtilsV2(object): _DISK_RD_METRIC_NAME = 'Disk Data Read' _DISK_WR_METRIC_NAME = 'Disk Data Written' _DISK_LATENCY_METRIC_NAME = 'Average Disk Latency' + _DISK_IOPS_METRIC_NAME = 'Average Normalized Disk Throughput' def __init__(self, host='.'): if sys.platform == 'win32': @@ -167,6 +168,20 @@ class UtilsV2(object): 'instance_id': disk.InstanceID, } + def get_disk_iops_count(self, vm_name): + vm = self._lookup_vm(vm_name) + metric_def_iops = self._get_metric_def(self._DISK_IOPS_METRIC_NAME) + disks = self._get_vm_resources(vm, self._STORAGE_ALLOC) + + for disk in disks: + metric_values = self._get_metric_values( + disk, [metric_def_iops]) + + yield { + 'iops_count': metric_values[0], + 'instance_id': disk.InstanceID, + } + @staticmethod def _sum_metric_values(metrics): tot_metric_val = 0 diff --git a/ceilometer/compute/virt/inspector.py b/ceilometer/compute/virt/inspector.py index e52661392..55f060c96 100644 --- a/ceilometer/compute/virt/inspector.py +++ b/ceilometer/compute/virt/inspector.py @@ -135,6 +135,13 @@ DiskRateStats = collections.namedtuple('DiskRateStats', DiskLatencyStats = collections.namedtuple('DiskLatencyStats', ['disk_latency']) +# Named tuple representing disk iops statistics. +# +# iops: number of iops per second +# +DiskIOPSStats = collections.namedtuple('DiskIOPSStats', + ['iops_count']) + # Exception types # @@ -235,6 +242,14 @@ class Inspector(object): """ raise ceilometer.NotImplementedError + def inspect_disk_iops(self, instance): + """Inspect the disk statistics as rates for an instance. + + :param instance: the target instance + :return: for each disk, the number of iops per second + """ + raise ceilometer.NotImplementedError + def get_hypervisor_inspector(): try: diff --git a/ceilometer/tests/agent/test_manager.py b/ceilometer/tests/agent/test_manager.py index 9255801de..34b9de152 100644 --- a/ceilometer/tests/agent/test_manager.py +++ b/ceilometer/tests/agent/test_manager.py @@ -35,8 +35,8 @@ class TestManager(base.BaseTestCase): def test_load_plugins_pollster_list(self): mgr = manager.AgentManager(pollster_list=['disk.*']) - # currently we do have 18 disk-related pollsters - self.assertEqual(18, len(list(mgr.extensions))) + # currently we do have 20 disk-related pollsters + self.assertEqual(20, len(list(mgr.extensions))) def test_load_plugins_no_intersection(self): # Let's test nothing will be polled if namespace and pollsters diff --git a/ceilometer/tests/compute/pollsters/test_diskio.py b/ceilometer/tests/compute/pollsters/test_diskio.py index df8a7964a..93f29630e 100644 --- a/ceilometer/tests/compute/pollsters/test_diskio.py +++ b/ceilometer/tests/compute/pollsters/test_diskio.py @@ -318,3 +318,51 @@ class TestDiskLatencyPollsters(TestBaseDiskIO): self._check_per_device_samples(disk.PerDeviceDiskLatencyPollster, 'disk.device.latency', 2, 'disk2') + + +class TestDiskIOPSPollsters(TestBaseDiskIO): + + DISKS = [ + (virt_inspector.Disk(device='disk1'), + virt_inspector.DiskIOPSStats(10)), + + (virt_inspector.Disk(device='disk2'), + virt_inspector.DiskIOPSStats(20)), + ] + TYPE = 'gauge' + + def setUp(self): + super(TestDiskIOPSPollsters, self).setUp() + self.inspector.inspect_disk_iops = mock.Mock(return_value=self.DISKS) + + @mock.patch('ceilometer.pipeline.setup_pipeline', mock.MagicMock()) + def _check_get_samples(self, factory, sample_name, + expected_count=2): + pollster = factory() + + mgr = manager.AgentManager() + cache = {} + samples = list(pollster.get_samples(mgr, cache, self.instance)) + self.assertIsNotNone(samples) + self.assertIsNotEmpty(samples) + self.assertIn(pollster.CACHE_KEY_DISK_IOPS, cache) + for instance in self.instance: + self.assertIn(instance.id, cache[pollster.CACHE_KEY_DISK_IOPS]) + + self.assertEqual(set([sample_name]), set([s.name for s in samples])) + + match = [s for s in samples if s.name == sample_name] + self.assertEqual(expected_count, len(match), + 'missing counter %s' % sample_name) + return match + + def test_disk_iops(self): + self._check_aggregate_samples(disk.DiskIOPSPollster, + 'disk.iops', 30L) + + def test_per_device_iops(self): + self._check_per_device_samples(disk.PerDeviceDiskIOPSPollster, + 'disk.device.iops', 10L, 'disk1') + + self._check_per_device_samples(disk.PerDeviceDiskIOPSPollster, + 'disk.device.iops', 20L, 'disk2') diff --git a/ceilometer/tests/compute/virt/hyperv/test_inspector.py b/ceilometer/tests/compute/virt/hyperv/test_inspector.py index ee930701e..780143a5d 100644 --- a/ceilometer/tests/compute/virt/hyperv/test_inspector.py +++ b/ceilometer/tests/compute/virt/hyperv/test_inspector.py @@ -134,3 +134,23 @@ class TestHyperVInspection(base.BaseTestCase): self.assertEqual(fake_instance_id, inspected_disk.device) self.assertEqual(fake_disk_latency, inspected_stats.disk_latency) + + def test_inspect_disk_iops_count(self): + fake_instance_name = mock.sentinel.INSTANCE_NAME + fake_disk_iops_count = mock.sentinel.DISK_IOPS_COUNT + fake_instance_id = mock.sentinel.INSTANCE_ID + + self._inspector._utils.get_disk_iops_count.return_value = [{ + 'iops_count': fake_disk_iops_count, + 'instance_id': fake_instance_id}] + + inspected_disks = list(self._inspector.inspect_disk_iops( + fake_instance_name)) + + self.assertEqual(1, len(inspected_disks)) + self.assertEqual(2, len(inspected_disks[0])) + + inspected_disk, inspected_stats = inspected_disks[0] + + self.assertEqual(fake_instance_id, inspected_disk.device) + self.assertEqual(fake_disk_iops_count, inspected_stats.iops_count) diff --git a/ceilometer/tests/compute/virt/hyperv/test_utilsv2.py b/ceilometer/tests/compute/virt/hyperv/test_utilsv2.py index d85847f34..a2ce6fd83 100644 --- a/ceilometer/tests/compute/virt/hyperv/test_utilsv2.py +++ b/ceilometer/tests/compute/virt/hyperv/test_utilsv2.py @@ -193,6 +193,27 @@ class TestUtilsV2(base.BaseTestCase): self.assertEqual(fake_latency, disk_metrics[0]['disk_latency']) self.assertEqual(fake_instance_id, disk_metrics[0]['instance_id']) + def test_get_disk_iops_metrics(self): + fake_vm_name = mock.sentinel.VM_NAME + fake_instance_id = mock.sentinel.FAKE_INSTANCE_ID + fake_iops_count = mock.sentinel.FAKE_IOPS_COUNT + + self._utils._lookup_vm = mock.MagicMock() + + mock_disk = mock.MagicMock() + mock_disk.InstanceID = fake_instance_id + self._utils._get_vm_resources = mock.MagicMock( + return_value=[mock_disk]) + + self._utils._get_metric_values = mock.MagicMock( + return_value=[fake_iops_count]) + + disk_metrics = list(self._utils.get_disk_iops_count(fake_vm_name)) + + self.assertEqual(1, len(disk_metrics)) + self.assertEqual(fake_iops_count, disk_metrics[0]['iops_count']) + self.assertEqual(fake_instance_id, disk_metrics[0]['instance_id']) + def test_get_metric_value_instances(self): mock_el1 = mock.MagicMock() mock_associator = mock.MagicMock() diff --git a/doc/source/measurements.rst b/doc/source/measurements.rst index 78be36d72..466d6997d 100644 --- a/doc/source/measurements.rst +++ b/doc/source/measurements.rst @@ -84,6 +84,7 @@ disk.read.bytes.rate g B/s inst ID p 1, 2, 3, disk.write.bytes c B inst ID p 1, 2 Volume of writes disk.write.bytes.rate g B/s inst ID p 1, 2, 3, 4 Average volume of writes disk.latency g ms inst ID p 2 Average disk latency +disk.iops g count/s inst ID p 2 Average disk iops disk.device.read.requests c request disk ID p 1, 2 Number of read requests disk.device.read.requests.rate g request/s disk ID p 1, 2, 3 Average rate of read requests disk.device.write.requests c request disk ID p 1, 2 Number of write requests @@ -93,6 +94,7 @@ disk.device.read.bytes.rate g B/s disk ID p 1, 2, 3 disk.device.write.bytes c B disk ID p 1, 2 Volume of writes disk.device.write.bytes.rate g B/s disk ID p 1, 2, 3 Average volume of writes disk.device.latency g ms disk ID p 2 Average disk latency per device +disk.device.iops g count/s disk ID p 2 Average disk iops per device disk.root.size g GB inst ID n 1, 2 Size of root disk disk.ephemeral.size g GB inst ID n 1, 2 Size of ephemeral disk network.incoming.bytes c B iface ID p 1, 2 Number of incoming bytes diff --git a/setup.cfg b/setup.cfg index a3cc4069d..e83025a11 100644 --- a/setup.cfg +++ b/setup.cfg @@ -126,6 +126,8 @@ ceilometer.poll.compute = disk.device.write.bytes.rate = ceilometer.compute.pollsters.disk:PerDeviceWriteBytesRatePollster disk.latency = ceilometer.compute.pollsters.disk:DiskLatencyPollster disk.device.latency = ceilometer.compute.pollsters.disk:PerDeviceDiskLatencyPollster + disk.iops = ceilometer.compute.pollsters.disk:DiskIOPSPollster + disk.device.iops = ceilometer.compute.pollsters.disk:PerDeviceDiskIOPSPollster cpu = ceilometer.compute.pollsters.cpu:CPUPollster cpu_util = ceilometer.compute.pollsters.cpu:CPUUtilPollster network.incoming.bytes = ceilometer.compute.pollsters.net:IncomingBytesPollster