From b9518583414bc95dc31ba66c173b6787aab2f839 Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Wed, 2 Sep 2026 14:12:14 +0200 Subject: [PATCH] dm: Add bd_dm_get_thin_pool_stats for generic thin pools Adds a way to query physical space usage of a device mapper thin pool directly via libdevmapper, without requiring the pool to be managed by LVM. The new BDDMThinPoolStats struct reports used/total data and metadata block counts together with the pool status flags. Resolves: https://github.com/storaged-project/libblockdev/issues/444 Co-Authored-By: Claude Opus 4.8 --- docs/libblockdev-sections.txt | 4 + src/lib/plugin_apis/dm.api | 90 ++++++++++++++++++++++ src/plugins/dm.c | 140 ++++++++++++++++++++++++++++++++++ src/plugins/dm.h | 15 ++++ tests/dm_test.py | 58 +++++++++++++- 5 files changed, 306 insertions(+), 1 deletion(-) diff --git a/docs/libblockdev-sections.txt b/docs/libblockdev-sections.txt index 2a62a1b7f..2e57e75a4 100644 --- a/docs/libblockdev-sections.txt +++ b/docs/libblockdev-sections.txt @@ -162,6 +162,10 @@ bd_dm_name_from_node bd_dm_node_from_name bd_dm_map_exists bd_dm_get_subsystem_from_name +bd_dm_get_thin_pool_stats +BDDMThinPoolStats +bd_dm_thin_pool_stats_copy +bd_dm_thin_pool_stats_free BDDMTech BDDMTechMode bd_dm_is_tech_avail diff --git a/src/lib/plugin_apis/dm.api b/src/lib/plugin_apis/dm.api index ed128bfa7..b0283d64a 100644 --- a/src/lib/plugin_apis/dm.api +++ b/src/lib/plugin_apis/dm.api @@ -30,6 +30,83 @@ typedef enum { BD_DM_TECH_MODE_QUERY = 1 << 2, } BDDMTechMode; +#define BD_DM_TYPE_THIN_POOL_STATS (bd_dm_thin_pool_stats_get_type ()) +GType bd_dm_thin_pool_stats_get_type (); + +/** + * BDDMThinPoolStats: + * @used_metadata_blocks: number of used metadata blocks + * @total_metadata_blocks: total number of metadata blocks + * @used_data_blocks: number of used data blocks + * @total_data_blocks: total number of data blocks + * @read_only: whether the metadata may not be changed + * @fail: whether the pool fails all I/O + * @out_of_data_space: whether the pool ran out of data space + * @needs_check: whether the metadata needs a check + * + * The block sizes are not reported by the device mapper thin pool status, only + * the numbers of blocks. To get the space usage as a percentage divide the used + * blocks by the total blocks. + */ +typedef struct BDDMThinPoolStats { + guint64 used_metadata_blocks; + guint64 total_metadata_blocks; + guint64 used_data_blocks; + guint64 total_data_blocks; + gboolean read_only; + gboolean fail; + gboolean out_of_data_space; + gboolean needs_check; +} BDDMThinPoolStats; + +/** + * bd_dm_thin_pool_stats_copy: (skip) + * @data: (nullable): %BDDMThinPoolStats to copy + * + * Creates a new copy of @data. + */ +BDDMThinPoolStats* bd_dm_thin_pool_stats_copy (BDDMThinPoolStats *data) { + if (data == NULL) + return NULL; + + BDDMThinPoolStats *new = g_new0 (BDDMThinPoolStats, 1); + + new->used_metadata_blocks = data->used_metadata_blocks; + new->total_metadata_blocks = data->total_metadata_blocks; + new->used_data_blocks = data->used_data_blocks; + new->total_data_blocks = data->total_data_blocks; + new->read_only = data->read_only; + new->fail = data->fail; + new->out_of_data_space = data->out_of_data_space; + new->needs_check = data->needs_check; + + return new; +} + +/** + * bd_dm_thin_pool_stats_free: (skip) + * @data: (nullable): %BDDMThinPoolStats to free + * + * Frees @data. + */ +void bd_dm_thin_pool_stats_free (BDDMThinPoolStats *data) { + if (data == NULL) + return; + g_free (data); +} + +GType bd_dm_thin_pool_stats_get_type () { + static GType type = 0; + + if (G_UNLIKELY(type == 0)) { + type = g_boxed_type_register_static("BDDMThinPoolStats", + (GBoxedCopyFunc) bd_dm_thin_pool_stats_copy, + (GBoxedFreeFunc) bd_dm_thin_pool_stats_free); + } + + return type; +} + /** * bd_dm_is_tech_avail: * @tech: the queried tech @@ -117,4 +194,17 @@ gchar* bd_dm_get_subsystem_from_name (const gchar *device_name, GError **error); */ gboolean bd_dm_map_exists (const gchar *map_name, gboolean live_only, gboolean active_only, GError **error); +/** + * bd_dm_get_thin_pool_stats: + * @map_name: name of the DM map (thin pool) to get stats for + * @error: (out) (optional): place to store error (if any) + * + * Returns: (transfer full): physical usage stats for the thin pool @map_name or + * %NULL in case of error (@error is set). This works for any device mapper thin + * pool, it doesn't have to be managed by LVM. + * + * Tech category: %BD_DM_TECH_MAP-%BD_DM_TECH_MODE_QUERY + */ +BDDMThinPoolStats* bd_dm_get_thin_pool_stats (const gchar *map_name, GError **error); + #endif /* BD_DM_API */ diff --git a/src/plugins/dm.c b/src/plugins/dm.c index bdc50103a..44e54b7cc 100644 --- a/src/plugins/dm.c +++ b/src/plugins/dm.c @@ -49,6 +49,42 @@ GQuark bd_dm_error_quark (void) return g_quark_from_static_string ("g-bd-dm-error-quark"); } +/** + * bd_dm_thin_pool_stats_copy: (skip) + * @data: (nullable): %BDDMThinPoolStats to copy + * + * Creates a new copy of @data. + */ +BDDMThinPoolStats* bd_dm_thin_pool_stats_copy (BDDMThinPoolStats *data) { + if (data == NULL) + return NULL; + + BDDMThinPoolStats *new = g_new0 (BDDMThinPoolStats, 1); + + new->used_metadata_blocks = data->used_metadata_blocks; + new->total_metadata_blocks = data->total_metadata_blocks; + new->used_data_blocks = data->used_data_blocks; + new->total_data_blocks = data->total_data_blocks; + new->read_only = data->read_only; + new->fail = data->fail; + new->out_of_data_space = data->out_of_data_space; + new->needs_check = data->needs_check; + + return new; +} + +/** + * bd_dm_thin_pool_stats_free: (skip) + * @data: (nullable): %BDDMThinPoolStats to free + * + * Frees @data. + */ +void bd_dm_thin_pool_stats_free (BDDMThinPoolStats *data) { + if (data == NULL) + return; + g_free (data); +} + static volatile guint avail_deps = 0; static GMutex deps_check_lock; @@ -393,3 +429,107 @@ gboolean bd_dm_map_exists (const gchar *map_name, gboolean live_only, gboolean a return ret; } + +/** + * bd_dm_get_thin_pool_stats: + * @map_name: name of the DM map (thin pool) to get stats for + * @error: (out) (optional): place to store error (if any) + * + * Returns: (transfer full): physical usage stats for the thin pool @map_name or + * %NULL in case of error (@error is set). This works for any device mapper thin + * pool, it doesn't have to be managed by LVM. + * + * Tech category: %BD_DM_TECH_MAP-%BD_DM_TECH_MODE_QUERY + */ +BDDMThinPoolStats* bd_dm_get_thin_pool_stats (const gchar *map_name, GError **error) { + struct dm_pool *pool = NULL; + struct dm_task *task = NULL; + struct dm_info info; + struct dm_status_thin_pool *status = NULL; + guint64 start = 0; + guint64 length = 0; + gchar *type = NULL; + gchar *params = NULL; + BDDMThinPoolStats *ret = NULL; + + if (geteuid () != 0) { + g_set_error_literal (error, BD_DM_ERROR, BD_DM_ERROR_NOT_ROOT, + "Not running as root, cannot query DM maps"); + return NULL; + } + + task = dm_task_create (DM_DEVICE_STATUS); + if (!task) { + g_set_error (error, BD_DM_ERROR, BD_DM_ERROR_TASK, + "Failed to create DM task for the thin pool map '%s'", map_name); + return NULL; + } + + if (dm_task_set_name (task, map_name) == 0) { + g_set_error (error, BD_DM_ERROR, BD_DM_ERROR_TASK, + "Failed to set name for the DM task for the thin pool map '%s'", map_name); + dm_task_destroy (task); + return NULL; + } + + if (dm_task_run (task) == 0) { + g_set_error (error, BD_DM_ERROR, BD_DM_ERROR_TASK, + "Failed to run the DM task for the thin pool map '%s'", map_name); + dm_task_destroy (task); + return NULL; + } + + if (dm_task_get_info (task, &info) == 0) { + g_set_error (error, BD_DM_ERROR, BD_DM_ERROR_TASK, + "Failed to get task info for the thin pool map '%s'", map_name); + dm_task_destroy (task); + return NULL; + } + + if (!info.exists) { + g_set_error (error, BD_DM_ERROR, BD_DM_ERROR_DEVICE_NOEXIST, + "The thin pool map '%s' doesn't exist", map_name); + dm_task_destroy (task); + return NULL; + } + + dm_get_next_target (task, NULL, &start, &length, &type, ¶ms); + + if (g_strcmp0 (type, "thin-pool") != 0) { + g_set_error (error, BD_DM_ERROR, BD_DM_ERROR_TASK, + "The map '%s' is not a thin pool", map_name); + dm_task_destroy (task); + return NULL; + } + + pool = dm_pool_create ("bd-pool", 20); + if (!pool) { + g_set_error (error, BD_DM_ERROR, BD_DM_ERROR_TASK, + "Failed to create memory pool for parsing the status of '%s'", map_name); + dm_task_destroy (task); + return NULL; + } + + if (dm_get_status_thin_pool (pool, params, &status) == 0) { + g_set_error (error, BD_DM_ERROR, BD_DM_ERROR_TASK, + "Failed to get status of the thin pool map '%s'", map_name); + dm_task_destroy (task); + dm_pool_destroy (pool); + return NULL; + } + + ret = g_new0 (BDDMThinPoolStats, 1); + ret->used_metadata_blocks = status->used_metadata_blocks; + ret->total_metadata_blocks = status->total_metadata_blocks; + ret->used_data_blocks = status->used_data_blocks; + ret->total_data_blocks = status->total_data_blocks; + ret->read_only = status->read_only != 0; + ret->fail = status->fail != 0; + ret->out_of_data_space = status->out_of_data_space != 0; + ret->needs_check = status->needs_check != 0; + + dm_task_destroy (task); + dm_pool_destroy (pool); + + return ret; +} diff --git a/src/plugins/dm.h b/src/plugins/dm.h index f009dbfd2..4b9d30a08 100644 --- a/src/plugins/dm.h +++ b/src/plugins/dm.h @@ -26,6 +26,20 @@ typedef enum { BD_DM_TECH_MODE_QUERY = 1 << 2, } BDDMTechMode; +typedef struct BDDMThinPoolStats { + guint64 used_metadata_blocks; + guint64 total_metadata_blocks; + guint64 used_data_blocks; + guint64 total_data_blocks; + gboolean read_only; + gboolean fail; + gboolean out_of_data_space; + gboolean needs_check; +} BDDMThinPoolStats; + +void bd_dm_thin_pool_stats_free (BDDMThinPoolStats *data); +BDDMThinPoolStats* bd_dm_thin_pool_stats_copy (BDDMThinPoolStats *data); + /* * If using the plugin as a standalone library, the following functions should * be called to: @@ -45,5 +59,6 @@ gboolean bd_dm_map_exists (const gchar *map_name, gboolean live_only, gboolean a gchar* bd_dm_name_from_node (const gchar *dm_node, GError **error); gchar* bd_dm_node_from_name (const gchar *map_name, GError **error); gchar* bd_dm_get_subsystem_from_name (const gchar *device_name, GError **error); +BDDMThinPoolStats* bd_dm_get_thin_pool_stats (const gchar *map_name, GError **error); #endif /* BD_DM */ diff --git a/tests/dm_test.py b/tests/dm_test.py index 00da45013..71e175269 100644 --- a/tests/dm_test.py +++ b/tests/dm_test.py @@ -2,7 +2,7 @@ import os import overrides_hack -from utils import run, create_sparse_tempfile, create_lio_device, delete_lio_device, fake_utils, fake_path, TestTags, tag_test, required_plugins +from utils import run, run_command, create_sparse_tempfile, create_lio_device, delete_lio_device, fake_utils, fake_path, TestTags, tag_test, required_plugins import gi gi.require_version('GLib', '2.0') @@ -160,6 +160,62 @@ def test_name_node_bijection(self): BlockDev.dm_name_from_node("") +class DevMapperThinPoolStats(DevMapperTestCase): + def _clean_up(self): + for map_name in ("testPool", "testData", "testMeta"): + run("dmsetup remove %s >/dev/null 2>&1" % map_name) + super()._clean_up() + + def _create_thin_pool(self): + # loop_dev is 1 GiB = 2097152 sectors; carve out a metadata and a data + # device using linear maps and build a thin pool on top of them + ret, _out, err = run_command("dmsetup create testMeta --table '0 16384 linear %s 0'" % self.loop_dev) + self.assertEqual(ret, 0, msg="Failed to create metadata device: %s" % err) + + ret, _out, err = run_command("dmsetup create testData --table '0 2080768 linear %s 16384'" % self.loop_dev) + self.assertEqual(ret, 0, msg="Failed to create data device: %s" % err) + + # a fresh thin pool needs its metadata superblock zeroed + ret, _out, err = run_command("dd if=/dev/zero of=/dev/mapper/testMeta bs=4096 count=1") + self.assertEqual(ret, 0, msg="Failed to zero the metadata device: %s" % err) + + # data block size 128 sectors (64 KiB), low water mark 0 + ret, _out, err = run_command("dmsetup create testPool " + "--table '0 2080768 thin-pool /dev/mapper/testMeta /dev/mapper/testData 128 0'") + self.assertEqual(ret, 0, msg="Failed to create the thin pool: %s" % err) + + def test_thin_pool_stats(self): + """Verify that it is possible to get stats for a device mapper thin pool""" + + self._create_thin_pool() + + stats = BlockDev.dm_get_thin_pool_stats("testPool") + self.assertIsNotNone(stats) + + self.assertGreater(stats.total_data_blocks, 0) + self.assertGreater(stats.total_metadata_blocks, 0) + self.assertLessEqual(stats.used_data_blocks, stats.total_data_blocks) + self.assertLessEqual(stats.used_metadata_blocks, stats.total_metadata_blocks) + + self.assertFalse(stats.fail) + self.assertFalse(stats.out_of_data_space) + self.assertFalse(stats.needs_check) + + def test_thin_pool_stats_errors(self): + """Verify that getting stats for an invalid thin pool fails as expected""" + + # non-existing map + with self.assertRaisesRegex(GLib.GError, "doesn't exist"): + BlockDev.dm_get_thin_pool_stats("testPool") + + # existing map that is not a thin pool + succ = BlockDev.dm_create_linear("testMap", self.loop_dev, 100, None) + self.assertTrue(succ) + + with self.assertRaisesRegex(GLib.GError, "not a thin pool"): + BlockDev.dm_get_thin_pool_stats("testMap") + + class DMNoStorageTest(DevMapperTest): @tag_test(TestTags.NOSTORAGE) def test_plugin_version(self):