From 654c21b2101700285bb58ee470996bb1b869be77 Mon Sep 17 00:00:00 2001 From: Daniel Madej Date: Mon, 28 Apr 2025 09:07:01 +0200 Subject: [PATCH 1/9] Repart to default IO class after loading IO class config Update OCF and request repartitioning of all the IO classes when loading IO class configuration, so that data cached under the old classification rules is moved to the unclassified IO class and gets classified again according to the new rules on next access. It applies also to the IO classes that are removed from the configuration. Signed-off-by: Daniel Madej Signed-off-by: Robert Baldyga --- modules/cas_cache/layer_cache_management.c | 1 + ocf | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/cas_cache/layer_cache_management.c b/modules/cas_cache/layer_cache_management.c index cb9715d7a..cf42a860c 100644 --- a/modules/cas_cache/layer_cache_management.c +++ b/modules/cas_cache/layer_cache_management.c @@ -1785,6 +1785,7 @@ int cache_mngt_set_partitions(const char *cache_name, size_t name_len, for (class_id = 0; class_id < OCF_USER_IO_CLASS_MAX; class_id++) { io_class_cfg->config[class_id].class_id = class_id; + io_class_cfg->config[class_id].repart = true; if (!cfg->info[class_id].name[0]) { io_class_cfg->config[class_id].class_id = class_id; diff --git a/ocf b/ocf index aff886fcd..9107f7d9e 160000 --- a/ocf +++ b/ocf @@ -1 +1 @@ -Subproject commit aff886fcde4e0ae32c856ba2fb00529157a8ad2d +Subproject commit 9107f7d9e929680cc450c58ca84915861bf571d2 From 730bdd778df3b96f559725f87daa9d1d2f0cac42 Mon Sep 17 00:00:00 2001 From: Daniel Madej Date: Mon, 28 Apr 2025 09:07:01 +0200 Subject: [PATCH 2/9] Flag for optional reclassification By default reclassify data from all IO classes to default. With `--keep-classification` flag reclassify data only from removed IO classes. Signed-off-by: Daniel Madej Signed-off-by: Robert Baldyga --- casadm/cas_lib.c | 3 ++- casadm/cas_lib.h | 4 ++-- casadm/cas_main.c | 17 ++++++++++++++++- casadm/casadm.8 | 8 ++++++++ modules/cas_cache/layer_cache_management.c | 2 ++ modules/include/cas_ioctl_codes.h | 3 +++ 6 files changed, 33 insertions(+), 4 deletions(-) diff --git a/casadm/cas_lib.c b/casadm/cas_lib.c index a66994c55..ff43630f2 100644 --- a/casadm/cas_lib.c +++ b/casadm/cas_lib.c @@ -2707,7 +2707,7 @@ int partition_set_config(struct kcas_io_classes *cnfg) return result; } -int partition_setup(unsigned int cache_id, const char *file) +int partition_setup(unsigned int cache_id, bool reclassify, const char *file) { int result = 0; CSVFILE *in; @@ -2739,6 +2739,7 @@ int partition_setup(unsigned int cache_id, const char *file) } if (0 == partition_get_config(in, cnfg, cache_id)) { + cnfg->repart_all = reclassify; result = partition_set_config(cnfg); } else { result = FAILURE; diff --git a/casadm/cas_lib.h b/casadm/cas_lib.h index 8c1d073ea..a34848286 100644 --- a/casadm/cas_lib.h +++ b/casadm/cas_lib.h @@ -1,6 +1,6 @@ /* * Copyright(c) 2012-2022 Intel Corporation -* Copyright(c) 2024 Huawei Technologies +* Copyright(c) 2024-2025 Huawei Technologies * Copyright(c) 2026 Unvertical * SPDX-License-Identifier: BSD-3-Clause */ @@ -311,7 +311,7 @@ int flush_core(unsigned int cache_id, unsigned int core_id); int check_cache_device(const char *device_path); int partition_list(unsigned int cache_id, unsigned int output_format); -int partition_setup(unsigned int cache_id, const char *file); +int partition_setup(unsigned int cache_id, bool reclassify, const char *file); int partition_is_name_valid(const char *name); int cas_module_version(char *buff, int size); diff --git a/casadm/cas_main.c b/casadm/cas_main.c index 6e7e998ba..b243d6420 100644 --- a/casadm/cas_main.c +++ b/casadm/cas_main.c @@ -1528,6 +1528,7 @@ enum { io_class_opt_cache_id, io_class_opt_cache_file_load, + io_class_opt_keep_classification, io_class_opt_output_format, io_class_opt_io_class_id, @@ -1585,6 +1586,14 @@ static cli_option io_class_params_options[] = { .priv = (1 << io_class_opt_subcmd_configure) | (1 << io_class_opt_flag_required) }, + [io_class_opt_keep_classification] = { + .short_name = 'k', + .long_name = "keep-classification", + .desc = "Prevents reclassification of data in IO classes with IDs matching previous configuration", + .args_count = 0, + .arg = NULL, + .priv = (1 << io_class_opt_subcmd_configure) + }, [io_class_opt_output_format] = { .short_name = 'o', .long_name = "output-format", @@ -1650,6 +1659,7 @@ struct { int io_class_id; int cache_mode; int io_class_prio; + bool reclassify; int output_format; uint32_t min; uint32_t max; @@ -1659,6 +1669,7 @@ struct { .subcmd = io_class_opt_subcmd_unknown, .cache_id = 0, .file = "", + .reclassify = true, .output_format = OUTPUT_FORMAT_DEFAULT }; @@ -1695,6 +1706,10 @@ int io_class_handle_option(char *opt, const char **arg) return FAILURE; io_class_params_options[io_class_opt_output_format].priv |= (1 << io_class_opt_flag_set); + } else if (!strcmp(opt, "keep-classification")) { + io_class_params.reclassify = false; + + io_class_params_options[io_class_opt_keep_classification].priv |= (1 << io_class_opt_flag_set); } return 0; @@ -1751,7 +1766,7 @@ int io_class_handle() { switch (io_class_params.subcmd) { case io_class_opt_subcmd_configure: return partition_setup(io_class_params.cache_id, - io_class_params.file); + io_class_params.reclassify, io_class_params.file); case io_class_opt_subcmd_list: return partition_list(io_class_params.cache_id, io_class_params.output_format); diff --git a/casadm/casadm.8 b/casadm/casadm.8 index 8db731f78..9946ee30f 100644 --- a/casadm/casadm.8 +++ b/casadm/casadm.8 @@ -568,6 +568,14 @@ Identifier of cache instance <1-16384>. .B -f, --file Configuration file containing IO class definition. +.TP +.B -k, --keep-classification +Keep classification of data cached in IO classes present in the new +configuration. By default data of all the IO classes is moved to the +unclassified IO class and gets classified again on next access. Data of IO +classes removed from the configuration is reclassified regardless of this +option. + .SH Options that are valid with --io-class --list (-C -L) are: .TP .B -i, --cache-id diff --git a/modules/cas_cache/layer_cache_management.c b/modules/cas_cache/layer_cache_management.c index cf42a860c..06fbd805b 100644 --- a/modules/cas_cache/layer_cache_management.c +++ b/modules/cas_cache/layer_cache_management.c @@ -1785,6 +1785,7 @@ int cache_mngt_set_partitions(const char *cache_name, size_t name_len, for (class_id = 0; class_id < OCF_USER_IO_CLASS_MAX; class_id++) { io_class_cfg->config[class_id].class_id = class_id; + /* Data of removed IO classes is always reclassified */ io_class_cfg->config[class_id].repart = true; if (!cfg->info[class_id].name[0]) { @@ -1792,6 +1793,7 @@ int cache_mngt_set_partitions(const char *cache_name, size_t name_len, continue; } + io_class_cfg->config[class_id].repart = cfg->repart_all; io_class_info2cfg(class_id, &cfg->info[class_id], &io_class_cfg->config[class_id]); } diff --git a/modules/include/cas_ioctl_codes.h b/modules/include/cas_ioctl_codes.h index d87cc6920..4ca6a5560 100644 --- a/modules/include/cas_ioctl_codes.h +++ b/modules/include/cas_ioctl_codes.h @@ -258,6 +258,9 @@ struct kcas_io_classes { /** Cache ID */ uint16_t cache_id; + /** Reclassify data of all IO classes, not only of the removed ones */ + bool repart_all; + int ext_err_code; /** IO class info */ From 9b27cb62ff453643083f5ffda718b2c5874d787c Mon Sep 17 00:00:00 2001 From: Daniel Madej Date: Mon, 28 Apr 2025 09:07:01 +0200 Subject: [PATCH 3/9] tests: Add `--keep-classification` flag in API Signed-off-by: Daniel Madej Signed-off-by: Robert Baldyga --- test/functional/api/cas/cache.py | 7 +++++-- test/functional/api/cas/casadm.py | 12 ++++++++++-- test/functional/api/cas/cli.py | 6 +++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/test/functional/api/cas/cache.py b/test/functional/api/cas/cache.py index 8afa5195d..2484039f6 100644 --- a/test/functional/api/cas/cache.py +++ b/test/functional/api/cas/cache.py @@ -1,6 +1,7 @@ # # Copyright(c) 2019-2021 Intel Corporation # Copyright(c) 2024-2026 Huawei Technologies Co., Ltd. +# Copyright(c) 2026 Unvertical # SPDX-License-Identifier: BSD-3-Clause # @@ -174,8 +175,10 @@ def reset_counters(self) -> Output: def set_cache_mode(self, cache_mode: CacheMode, flush=None) -> Output: return casadm.set_cache_mode(cache_mode, self.cache_id, flush) - def load_io_class(self, file_path: str) -> Output: - return casadm.load_io_classes(self.cache_id, file_path) + def load_io_class(self, file_path: str, keep_classification: bool = False) -> Output: + return casadm.load_io_classes( + self.cache_id, file_path, keep_classification=keep_classification + ) def list_io_classes(self) -> list: return get_io_class_list(self.cache_id) diff --git a/test/functional/api/cas/casadm.py b/test/functional/api/cas/casadm.py index e91e37907..2a8a02880 100644 --- a/test/functional/api/cas/casadm.py +++ b/test/functional/api/cas/casadm.py @@ -1,6 +1,7 @@ # # Copyright(c) 2019-2022 Intel Corporation # Copyright(c) 2024-2026 Huawei Technologies Co., Ltd. +# Copyright(c) 2026 Unvertical # SPDX-License-Identifier: BSD-3-Clause # @@ -589,9 +590,16 @@ def flush_core(cache_id: int, core_id: int, shortcut: bool = False) -> Output: return output -def load_io_classes(cache_id: int, file: str, shortcut: bool = False) -> Output: +def load_io_classes( + cache_id: int, file: str, shortcut: bool = False, keep_classification: bool = False +) -> Output: output = TestRun.executor.run( - load_io_classes_cmd(cache_id=str(cache_id), file=file, shortcut=shortcut) + load_io_classes_cmd( + cache_id=str(cache_id), + file=file, + shortcut=shortcut, + keep_classification=keep_classification, + ) ) if output.exit_code != 0: raise CmdException("Load IO class command failed.", output) diff --git a/test/functional/api/cas/cli.py b/test/functional/api/cas/cli.py index 8b58c6c22..f284743a3 100644 --- a/test/functional/api/cas/cli.py +++ b/test/functional/api/cas/cli.py @@ -411,10 +411,14 @@ def flush_core_cmd(cache_id: str, core_id: str, shortcut: bool = False) -> str: return casadm_bin + command -def load_io_classes_cmd(cache_id: str, file: str, shortcut: bool = False) -> str: +def load_io_classes_cmd( + cache_id: str, file: str, shortcut: bool = False, keep_classification: bool = False +) -> str: command = " -C -C" if shortcut else " --io-class --load-config" command += (" -i " if shortcut else " --cache-id ") + cache_id command += (" -f " if shortcut else " --file ") + file + if keep_classification: + command += (" -k" if shortcut else " --keep-classification") return casadm_bin + command From 8c483fa92f26666abc0982bbc70dbce809be4f4c Mon Sep 17 00:00:00 2001 From: Daniel Madej Date: Mon, 28 Apr 2025 09:07:01 +0200 Subject: [PATCH 4/9] tests: Add `--keep-classification` flag for CLI help test Signed-off-by: Daniel Madej --- test/functional/api/cas/cli_help_messages.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/functional/api/cas/cli_help_messages.py b/test/functional/api/cas/cli_help_messages.py index f1062b6e9..432cfff22 100644 --- a/test/functional/api/cas/cli_help_messages.py +++ b/test/functional/api/cas/cli_help_messages.py @@ -297,6 +297,7 @@ r"Options that are valid with --load-config \(-C\) are:", r"-i --cache-id \ Identifier of cache instance \<1-16384\>", r"-f --file \ Configuration file containing IO class definition", + r"-k --keep-classification Prevents reclassification of data in IO classes with IDs matching previous configuration", r"Lists currently configured IO classes:", r"Usage: casadm --io-class --list --cache-id \ \[option\.\.\.\]", r"Options that are valid with --list \(-L\) are:", From 25e82b76b8390fb15677edfa2e037f42988681e5 Mon Sep 17 00:00:00 2001 From: Robert Baldyga Date: Fri, 16 Oct 2026 20:51:00 +0200 Subject: [PATCH 5/9] tests: Keep classification in tests that need it Signed-off-by: Robert Baldyga --- .../test_io_class_eviction_pinned_after_unpin.py | 10 +++++++--- .../tests/io_class/test_io_class_occupancy_resize.py | 6 +++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/test/functional/tests/io_class/test_io_class_eviction_pinned_after_unpin.py b/test/functional/tests/io_class/test_io_class_eviction_pinned_after_unpin.py index 6c7d5af55..72d44686c 100644 --- a/test/functional/tests/io_class/test_io_class_eviction_pinned_after_unpin.py +++ b/test/functional/tests/io_class/test_io_class_eviction_pinned_after_unpin.py @@ -65,7 +65,7 @@ def test_io_class_eviction_pinned_after_unpin(): with TestRun.step("Unpin ioclass to and set its priority to be lower than second IO class"): io_classes = [IoclassConfig(1, 11, 1, cores[0]), IoclassConfig(2, 10, 1, cores[1])] - _add_and_load_io_classes(cache.cache_id, io_classes) + _add_and_load_io_classes(cache.cache_id, io_classes, keep_classification=True) with TestRun.step("Run dd on second io ioclass "): run_io_dir(f"{io_classes[1].core.path}", usable_cache_size) @@ -89,7 +89,7 @@ def _refresh_cache(cache): cache.reset_counters() -def _add_and_load_io_classes(cache_id, io_classes: list): +def _add_and_load_io_classes(cache_id, io_classes: list, keep_classification=False): ioclass_config.remove_ioclass_config() ioclass_config.create_ioclass_config(False) @@ -102,7 +102,11 @@ def _add_and_load_io_classes(cache_id, io_classes: list): ioclass_config_path=ioclass_config.default_config_file_path, ) - casadm.load_io_classes(cache_id=cache_id, file=default_config_file_path) + casadm.load_io_classes( + cache_id=cache_id, + file=default_config_file_path, + keep_classification=keep_classification, + ) def prepare(cores_number=1, cache_size=Size(10, Unit.GibiByte), core_size=Size(5, Unit.GibiByte)): diff --git a/test/functional/tests/io_class/test_io_class_occupancy_resize.py b/test/functional/tests/io_class/test_io_class_occupancy_resize.py index 2bc7c88a2..29cfbf359 100644 --- a/test/functional/tests/io_class/test_io_class_occupancy_resize.py +++ b/test/functional/tests/io_class/test_io_class_occupancy_resize.py @@ -138,7 +138,11 @@ def test_ioclass_resize(cache_line_size, new_occupancy): f"{io_class.max_occupancy:0.2f}", ) - casadm.load_io_classes(cache_id=cache.cache_id, file=default_config_file_path) + casadm.load_io_classes( + cache_id=cache.cache_id, + file=default_config_file_path, + keep_classification=True, + ) with TestRun.step("Perform IO with size equal to cache size"): run_io_dir(f"{io_class.dir_path}/tmp_file", cache_size) From b8dc13ca9925db91a8ddda77260dfab39969bdf2 Mon Sep 17 00:00:00 2001 From: Daniel Madej Date: Mon, 28 Apr 2025 09:07:01 +0200 Subject: [PATCH 6/9] tests: Fuzzy test for `--keep-classification` flag Signed-off-by: Daniel Madej Signed-off-by: Robert Baldyga --- .../test_fuzzy_io_class_load_config_flags.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 test/functional/tests/security/fuzzy/kernel/fuzzy_with_io/fuzzy_io_class/test_fuzzy_io_class_load_config_flags.py diff --git a/test/functional/tests/security/fuzzy/kernel/fuzzy_with_io/fuzzy_io_class/test_fuzzy_io_class_load_config_flags.py b/test/functional/tests/security/fuzzy/kernel/fuzzy_with_io/fuzzy_io_class/test_fuzzy_io_class_load_config_flags.py new file mode 100644 index 000000000..f80145a62 --- /dev/null +++ b/test/functional/tests/security/fuzzy/kernel/fuzzy_with_io/fuzzy_io_class/test_fuzzy_io_class_load_config_flags.py @@ -0,0 +1,95 @@ +# +# Copyright(c) 2025 Huawei Technologies Co., Ltd. +# Copyright(c) 2026 Unvertical +# SPDX-License-Identifier: BSD-3-Clause +# + +import pytest + +from api.cas.cache_config import ( + CacheMode, + CacheLineSize, + CleaningPolicy, + UnalignedIo, + KernelParameters, + UseIoScheduler, +) +from api.cas.cli import load_io_classes_cmd +from core.test_run import TestRun +from storage_devices.disk import DiskType, DiskTypeSet, DiskTypeLowerThan +from test_tools.peach_fuzzer.peach_fuzzer import PeachFuzzer +from tests.security.fuzzy.kernel.common.common import ( + prepare_cas_instance, + get_fuzz_config, + run_cmd_and_validate, +) +from tests.security.fuzzy.kernel.fuzzy_with_io.common.common import ( + get_basic_workload, + mount_point, +) + + +@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) +@pytest.mark.require_disk("core", DiskTypeLowerThan("cache")) +@pytest.mark.parametrizex("cache_mode", CacheMode) +@pytest.mark.parametrizex("cache_line_size", CacheLineSize) +@pytest.mark.parametrizex("cleaning_policy", CleaningPolicy) +@pytest.mark.parametrizex("unaligned_io", UnalignedIo) +@pytest.mark.parametrizex("use_io_scheduler", UseIoScheduler) +def test_fuzzy_io_class_load_config_flags( + cache_mode, cache_line_size, cleaning_policy, unaligned_io, use_io_scheduler +): + """ + title: Fuzzy test for casadm 'load IO class configuration' command – flags. + description: | + Using Peach Fuzzer check Open CAS ability of handling wrong flags in + 'load IO class configuration' command. + pass_criteria: + - System did not crash + - Open CAS still works. + """ + with TestRun.step( + "Start cache with configuration and add core device, make filesystem and mount it" + ): + cache_disk = TestRun.disks["cache"] + core_disk = TestRun.disks["core"] + cache, core = prepare_cas_instance( + cache_device=cache_disk, + core_device=core_disk, + cache_mode=cache_mode, + cache_line_size=cache_line_size, + kernel_params=KernelParameters(unaligned_io, use_io_scheduler), + cleaning_policy=cleaning_policy, + mount_point=mount_point, + ) + + with TestRun.step("Run fio in background"): + fio = get_basic_workload(mount_point) + fio_pid = fio.run_in_background() + if not TestRun.executor.check_if_process_exists(fio_pid): + raise Exception("Fio is not running.") + + with TestRun.step("Prepare PeachFuzzer"): + valid_values = ["", "-k", "--keep-classification"] + valid_values = [v.encode("ascii") for v in valid_values] + PeachFuzzer.generate_config(get_fuzz_config("flags.yml")) + base_cmd = load_io_classes_cmd( + cache_id=str(cache.cache_id), + file="/etc/opencas/ioclass-config.csv", + ) + " {param}" + commands = PeachFuzzer.get_fuzzed_command( + command_template=base_cmd, count=TestRun.usr.fuzzy_iter_count + ) + + for index, cmd in TestRun.iteration( + enumerate(commands), f"Run command {TestRun.usr.fuzzy_iter_count} times" + ): + with TestRun.step(f"Iteration {index + 1}"): + if not TestRun.executor.check_if_process_exists(fio_pid): + raise Exception("Fio is not running.") + + run_cmd_and_validate( + cmd=cmd, + value_name="Flag", + is_valid=cmd.param in valid_values, + ) From cc2b3c4c611f4cb95d42714193e6d0a6e2644af6 Mon Sep 17 00:00:00 2001 From: Robert Baldyga Date: Wed, 21 Oct 2026 20:45:51 +0200 Subject: [PATCH 7/9] tests: Generate random ioclass lists with unconfigured slots Leave each non-default slot empty with 50% probability, so that the generated configurations also exercise sparse IO class tables. Make ioclass stats sections tests check only configured ioclasses. Signed-off-by: Robert Baldyga --- test/functional/api/cas/ioclass_config.py | 2 ++ test/functional/tests/io_class/test_io_class_cli.py | 2 +- .../tests/io_class/test_io_class_preserve_config.py | 2 +- test/functional/tests/stats/test_ioclass_stats.py | 8 ++++++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/test/functional/api/cas/ioclass_config.py b/test/functional/api/cas/ioclass_config.py index 38a438eea..8c670c863 100644 --- a/test/functional/api/cas/ioclass_config.py +++ b/test/functional/api/cas/ioclass_config.py @@ -160,6 +160,8 @@ def generate_random_ioclass_list(count: int, max_priority: int = MAX_IO_CLASS_PR ), ] for i in range(1, count - 1): + if random.choice([True, False]): + continue random_list.append( IoClass( i, diff --git a/test/functional/tests/io_class/test_io_class_cli.py b/test/functional/tests/io_class/test_io_class_cli.py index 7bf4c212b..f5148f56e 100644 --- a/test/functional/tests/io_class/test_io_class_cli.py +++ b/test/functional/tests/io_class/test_io_class_cli.py @@ -47,7 +47,7 @@ def test_io_class_export_configuration(): ) with TestRun.LOGGER.step( - "Create and load configuration file for 33 IO classes " + "Create and load random configuration file for up to 33 IO classes " "with random names, allocation and priority values" ): random_list = IoClass.generate_random_ioclass_list(33) diff --git a/test/functional/tests/io_class/test_io_class_preserve_config.py b/test/functional/tests/io_class/test_io_class_preserve_config.py index 4813e25b1..d0816a5da 100644 --- a/test/functional/tests/io_class/test_io_class_preserve_config.py +++ b/test/functional/tests/io_class/test_io_class_preserve_config.py @@ -52,7 +52,7 @@ def test_io_class_preserve_configuration(): cache.add_core(core_device) with TestRun.step( - "Create and load configuration file for all IO classes with random names, " + "Create and load random configuration file with random names, " "allocation and priority values." ): generated_io_classes = generate_and_load_random_io_class_config(cache) diff --git a/test/functional/tests/stats/test_ioclass_stats.py b/test/functional/tests/stats/test_ioclass_stats.py index eace7bbbc..6f7a7fe22 100644 --- a/test/functional/tests/stats/test_ioclass_stats.py +++ b/test/functional/tests/stats/test_ioclass_stats.py @@ -307,15 +307,17 @@ def test_ioclass_stats_sections_cache(stat_filter, random_cls): validate_statistics(statistics, stat_filter) with TestRun.step("Load random IO class configuration for each cache"): + io_class_ids = {} for cache in caches: random_list = IoClass.generate_random_ioclass_list(ioclass_config.MAX_IO_CLASS_ID + 1) IoClass.save_list_to_config_file(random_list, add_default_rule=False) cache.load_io_class(ioclass_config.default_config_file_path) + io_class_ids[cache.cache_id] = [io_class.id for io_class in random_list] with TestRun.group("Random IO class config statistics"): for cache in caches: with TestRun.step(f"Cache {cache.cache_id}"): - for class_id in range(ioclass_config.MAX_IO_CLASS_ID + 1): + for class_id in io_class_ids[cache.cache_id]: statistics = get_stats_dict( filter=[stat_filter], cache_id=cache.cache_id, io_class_id=class_id) validate_statistics(statistics, stat_filter) @@ -348,16 +350,18 @@ def test_ioclass_stats_sections_core(stat_filter, random_cls): validate_statistics(statistics, stat_filter) with TestRun.step("Load random IO class configuration for each cache"): + io_class_ids = {} for cache in caches: random_list = IoClass.generate_random_ioclass_list(ioclass_config.MAX_IO_CLASS_ID + 1) IoClass.save_list_to_config_file(random_list, add_default_rule=False) cache.load_io_class(ioclass_config.default_config_file_path) + io_class_ids[cache.cache_id] = [io_class.id for io_class in random_list] with TestRun.group("Random IO class config statistics"): for cache in caches: for core in cache.get_cores(): with TestRun.step(f"Core {core.cache_id}-{core.core_id}"): - for class_id in range(ioclass_config.MAX_IO_CLASS_ID + 1): + for class_id in io_class_ids[cache.cache_id]: statistics = get_stats_dict( [stat_filter], core.cache_id, core.core_id, class_id) validate_statistics(statistics, stat_filter) From 96854b13af8467310cdc8fe913b0b98c38114ef6 Mon Sep 17 00:00:00 2001 From: Robert Baldyga Date: Fri, 16 Oct 2026 21:00:12 +0200 Subject: [PATCH 8/9] tests: Stress IO class reconfiguration with and without reclassification Signed-off-by: Robert Baldyga --- .../tests/io_class/io_class_common.py | 6 ++-- .../test_stress_change_io_class_config_io.py | 32 ++++++++++++------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/test/functional/tests/io_class/io_class_common.py b/test/functional/tests/io_class/io_class_common.py index 05c93ea58..bb8a182b3 100644 --- a/test/functional/tests/io_class/io_class_common.py +++ b/test/functional/tests/io_class/io_class_common.py @@ -92,10 +92,12 @@ def get_io_class_usage(cache, io_class_id, percent=False): ).usage_stats -def generate_and_load_random_io_class_config(cache): +def generate_and_load_random_io_class_config(cache, keep_classification=False): random_list = IoClass.generate_random_ioclass_list(ioclass_config.MAX_IO_CLASS_ID + 1) IoClass.save_list_to_config_file(random_list, add_default_rule=False) - cache.load_io_class(ioclass_config.default_config_file_path) + cache.load_io_class( + ioclass_config.default_config_file_path, keep_classification=keep_classification + ) return random_list diff --git a/test/functional/tests/stress/test_stress_change_io_class_config_io.py b/test/functional/tests/stress/test_stress_change_io_class_config_io.py index acfe4b9cd..405f55756 100644 --- a/test/functional/tests/stress/test_stress_change_io_class_config_io.py +++ b/test/functional/tests/stress/test_stress_change_io_class_config_io.py @@ -27,12 +27,14 @@ @pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) @pytest.mark.require_disk("core", DiskTypeLowerThan("cache")) -def test_stress_io_class_change_config_during_io_raw(): +@pytest.mark.parametrize("keep_classification", [False, True]) +def test_stress_io_class_change_config_during_io_raw(keep_classification): """ title: Set up IO class configuration from file during IO - stress. description: | Check Open CAS ability to change IO class configuration during running IO - on small cache and core devices. + on small cache and core devices, with and without reclassification of + cached data. pass_criteria: - No system crash - IO class configuration changes successfully @@ -61,11 +63,12 @@ def test_stress_io_class_change_config_during_io_raw(): fio_task = start_async_func(run_io, cores, True) with TestRun.step("In two-second time interval change IO class configuration " - "(using random values in allowed range) and cache mode " - "(between all supported). Check if Open CAS configuration has changed."): + "(using random values in allowed range, dropping random IO classes) " + "and cache mode (between all supported). Check if Open CAS " + "configuration has changed."): change_mode_thread = threading.Thread(target=change_cache_mode, args=[cache, fio_task]) change_io_class_thread = threading.Thread(target=change_io_class_config, - args=[cache, fio_task]) + args=[cache, fio_task, keep_classification]) change_mode_thread.start() sleep(1) change_io_class_thread.start() @@ -80,13 +83,15 @@ def test_stress_io_class_change_config_during_io_raw(): @pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) @pytest.mark.require_disk("core", DiskTypeLowerThan("cache")) +@pytest.mark.parametrize("keep_classification", [False, True]) @pytest.mark.asyncio -async def test_stress_io_class_change_config_during_io_fs(): +async def test_stress_io_class_change_config_during_io_fs(keep_classification): """ title: Set up IO class configuration from file for filesystems during IO - stress. description: | Check Intel CAS ability to change IO class configuration for different filesystems - during running IO on small cache and core devices. + during running IO on small cache and core devices, with and without + reclassification of cached data. pass_criteria: - No system crash - IO class configuration changes successfully @@ -121,11 +126,12 @@ async def test_stress_io_class_change_config_during_io_fs(): fio_task = start_async_func(run_io, cores) with TestRun.step("In two-second time interval change IO class configuration " - "(using random values in allowed range) and cache mode " - "(between all supported). Check if Open CAS configuration has changed."): + "(using random values in allowed range, dropping random IO classes) " + "and cache mode (between all supported). Check if Open CAS " + "configuration has changed."): change_mode_thread = threading.Thread(target=change_cache_mode, args=[cache, fio_task]) change_io_class_thread = threading.Thread(target=change_io_class_config, - args=[cache, fio_task]) + args=[cache, fio_task, keep_classification]) change_mode_thread.start() sleep(1) change_io_class_thread.start() @@ -148,10 +154,12 @@ def change_cache_mode(cache, fio_task): cache.set_cache_mode(new_cache_mode, False) -def change_io_class_config(cache, fio_task): +def change_io_class_config(cache, fio_task, keep_classification): while fio_task.done() is False: sleep(2) - generated_io_classes = generate_and_load_random_io_class_config(cache) + generated_io_classes = generate_and_load_random_io_class_config( + cache, keep_classification=keep_classification + ) loaded_io_classes = cache.list_io_classes() if not IoClass.compare_ioclass_lists(generated_io_classes, loaded_io_classes): TestRun.LOGGER.error("IO classes not changed correctly.") From 11ccb732c52f9129ae57b1ace0a470fa05f7aafb Mon Sep 17 00:00:00 2001 From: Robert Baldyga Date: Fri, 16 Oct 2026 20:51:00 +0200 Subject: [PATCH 9/9] tests: Add tests for reclassification after loading IO class config Signed-off-by: Robert Baldyga --- .../test_io_class_reclassification.py | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 test/functional/tests/io_class/test_io_class_reclassification.py diff --git a/test/functional/tests/io_class/test_io_class_reclassification.py b/test/functional/tests/io_class/test_io_class_reclassification.py new file mode 100644 index 000000000..26e1b23bd --- /dev/null +++ b/test/functional/tests/io_class/test_io_class_reclassification.py @@ -0,0 +1,202 @@ +# +# Copyright(c) 2026 Unvertical +# SPDX-License-Identifier: BSD-3-Clause +# + +import pytest + +from api.cas import ioclass_config, casadm +from core.test_run import TestRun +from storage_devices.disk import DiskType, DiskTypeSet, DiskTypeLowerThan +from test_tools.dd import Dd +from type_def.size import Size, Unit +from tests.io_class.io_class_common import ( + prepare, + ioclass_config_path, + get_io_class_occupancy, + get_io_class_dirty, +) + +region_size = Size(16, Unit.MebiByte) +region_sectors = int(region_size.get_value(Unit.Blocks512)) + + +def lba_rule(region): + first = region * region_sectors + last = first + region_sectors - 1 + return f"lba:ge:{first}&lba:le:{last}&done" + + +def add_lba_ioclass(ioclass_id, region): + ioclass_config.add_ioclass( + ioclass_id=ioclass_id, + eviction_priority=1, + allocation="1.00", + rule=lba_rule(region), + ioclass_config_path=ioclass_config_path, + ) + + +def run_region_io(core, region, write): + dd = Dd().block_size(Size(1, Unit.MebiByte)).count(int(region_size.get_value(Unit.MebiByte))) + if write: + dd.input("/dev/urandom").output(core.path).seek( + region * int(region_size.get_value(Unit.MebiByte)) + ).oflag("direct") + else: + dd.input(core.path).output("/dev/null").skip( + region * int(region_size.get_value(Unit.MebiByte)) + ).iflag("direct") + dd.run() + + +def regions_md5(core, regions): + output = TestRun.executor.run_expect_success( + f"dd if={core.path} bs=1M count={regions * int(region_size.get_value(Unit.MebiByte))} " + f"iflag=direct | md5sum" + ) + return output.stdout.split()[0] + + +def check_usage(cache, ioclass_id, expected, check_dirty=True): + occupancy = get_io_class_occupancy(cache, ioclass_id) + if occupancy != expected: + TestRun.LOGGER.error( + f"Wrong occupancy of IO class {ioclass_id}. Expected {expected}, got {occupancy}" + ) + if check_dirty: + dirty = get_io_class_dirty(cache, ioclass_id) + if dirty != expected: + TestRun.LOGGER.error( + f"Wrong dirty data of IO class {ioclass_id}. Expected {expected}, got {dirty}" + ) + + +@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) +@pytest.mark.require_disk("core", DiskTypeLowerThan("cache")) +@pytest.mark.parametrize("keep_classification", [False, True]) +def test_io_class_reclassify_on_remove(keep_classification): + """ + title: Test reclassification of data after removing IO class. + description: | + Fill two IO classes with dirty data, remove one of them from the configuration + and load it. Check that data of all the IO classes is moved to the unclassified + IO class, unless the classification is kept, in which case only data of the + removed IO class is moved. Then check that accessed data is classified again. + pass_criteria: + - Data of all the IO classes is reclassified by default. + - With --keep-classification only data of the removed IO class is reclassified. + - Accessed data is classified according to the new configuration. + - Cache occupancy and data stay unchanged. + """ + default_id = ioclass_config.DEFAULT_IO_CLASS_ID + kept_id, removed_id = 1, 2 + zero = Size.zero() + + with TestRun.step("Prepare cache and core"): + cache, core = prepare(default_allocation="1.00") + + with TestRun.step("Load configuration with IO classes for two LBA regions"): + add_lba_ioclass(kept_id, region=0) + add_lba_ioclass(removed_id, region=1) + casadm.load_io_classes(cache_id=cache.cache_id, file=ioclass_config_path) + + with TestRun.step("Write data to both regions"): + run_region_io(core, region=0, write=True) + run_region_io(core, region=1, write=True) + md5_before = regions_md5(core, 2) + check_usage(cache, kept_id, region_size) + check_usage(cache, removed_id, region_size) + check_usage(cache, default_id, zero) + + with TestRun.step("Remove one IO class and load the configuration"): + ioclass_config.remove_ioclass(removed_id, ioclass_config_path) + casadm.load_io_classes( + cache_id=cache.cache_id, + file=ioclass_config_path, + keep_classification=keep_classification, + ) + + with TestRun.step("Check that data was reclassified"): + if keep_classification: + check_usage(cache, kept_id, region_size) + check_usage(cache, default_id, region_size) + else: + check_usage(cache, kept_id, zero) + check_usage(cache, default_id, region_size * 2) + + if cache.get_occupancy() != region_size * 2: + TestRun.LOGGER.error(f"Wrong cache occupancy: {cache.get_occupancy()}") + if cache.get_dirty_blocks() != region_size * 2: + TestRun.LOGGER.error(f"Wrong amount of dirty data: {cache.get_dirty_blocks()}") + + with TestRun.step("Read data of the remaining IO class"): + run_region_io(core, region=0, write=False) + + with TestRun.step("Check that read data was classified again"): + check_usage(cache, kept_id, region_size) + check_usage(cache, default_id, region_size) + + with TestRun.step("Flush cache and check data consistency"): + cache.flush_cache() + if cache.get_dirty_blocks() != zero: + TestRun.LOGGER.error(f"Dirty data left after flush: {cache.get_dirty_blocks()}") + if regions_md5(core, 2) != md5_before: + TestRun.LOGGER.error("Data changed after reclassification") + + +@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) +@pytest.mark.require_disk("core", DiskTypeLowerThan("cache")) +@pytest.mark.parametrize("keep_classification", [False, True]) +def test_io_class_reclassify_on_rule_change(keep_classification): + """ + title: Test reclassification of data after changing IO class rule. + description: | + Fill an IO class with data, change its classification rule and load the + configuration. Check that by default its data is moved to the unclassified + IO class immediately, and with the classification kept it stays in the IO + class until accessed. + pass_criteria: + - Data of the IO class with modified rule is reclassified by default. + - With --keep-classification data stays in the IO class until accessed. + - Accessed data is classified according to the new rule. + """ + default_id = ioclass_config.DEFAULT_IO_CLASS_ID + ioclass_id = 1 + zero = Size.zero() + + with TestRun.step("Prepare cache and core"): + cache, core = prepare(default_allocation="1.00") + + with TestRun.step("Load configuration with IO class for the first LBA region"): + add_lba_ioclass(ioclass_id, region=0) + casadm.load_io_classes(cache_id=cache.cache_id, file=ioclass_config_path) + + with TestRun.step("Write data to the first region"): + run_region_io(core, region=0, write=True) + check_usage(cache, ioclass_id, region_size) + check_usage(cache, default_id, zero) + + with TestRun.step("Change the IO class rule to the second LBA region"): + ioclass_config.remove_ioclass(ioclass_id, ioclass_config_path) + add_lba_ioclass(ioclass_id, region=1) + casadm.load_io_classes( + cache_id=cache.cache_id, + file=ioclass_config_path, + keep_classification=keep_classification, + ) + + with TestRun.step("Check that data was reclassified only if requested"): + if keep_classification: + check_usage(cache, ioclass_id, region_size) + check_usage(cache, default_id, zero) + else: + check_usage(cache, ioclass_id, zero) + check_usage(cache, default_id, region_size) + + with TestRun.step("Read data of the first region"): + run_region_io(core, region=0, write=False) + + with TestRun.step("Check that read data was classified according to the new rule"): + check_usage(cache, ioclass_id, zero) + check_usage(cache, default_id, region_size)