diff --git a/modules/cas_cache/classifier.c b/modules/cas_cache/classifier.c index 13881a2..89bb854 100644 --- a/modules/cas_cache/classifier.c +++ b/modules/cas_cache/classifier.c @@ -221,6 +221,42 @@ static int _cas_cls_string_ctr(struct cas_classifier *cls, return 0; } +/* IO direction condition constructor. @data is expected to contain string + * translated to IO direction. + */ +static int _cas_cls_direction_ctr(struct cas_classifier *cls, + struct cas_cls_condition *c, char *data) +{ + uint64_t direction; + struct cas_cls_numeric *ctx; + + if (!data) { + CAS_CLS_MSG(KERN_ERR, "Missing IO direction specifier\n"); + return -EINVAL; + } + + if (strncmp("read", data, 5) == 0) { + direction = READ; + } else if (strncmp("write", data, 6) == 0) { + direction = WRITE; + } else { + CAS_CLS_MSG(KERN_ERR, "Invalid IO direction specifier '%s'\n" + " allowed specifiers: 'read', 'write'\n", data); + return -EINVAL; + } + + ctx = kmalloc(sizeof(*ctx), GFP_KERNEL); + if (!ctx) + return -ENOMEM; + + ctx->operator = cas_cls_numeric_eq; + ctx->v_u64 = direction; + + c->context = ctx; + + return 0; +} + /* Unsigned int numeric test function */ static cas_cls_eval_t _cas_cls_numeric_test_u( struct cas_cls_condition *c, uint64_t val) @@ -664,6 +700,14 @@ static cas_cls_eval_t _cas_cls_request_size_test( return _cas_cls_numeric_test_u(c, CAS_BIO_BISIZE(io->bio)); } +/* Request IO direction test function */ +static cas_cls_eval_t _cas_cls_request_direction_test( + struct cas_classifier *cls, struct cas_cls_condition *c, + struct cas_cls_io *io, ocf_part_id_t part_id) +{ + return _cas_cls_numeric_test_u(c, bio_data_dir(io->bio)); +} + /* Array of condition handlers */ static struct cas_cls_condition_handler _handlers[] = { { "done", _cas_cls_done_test, _cas_cls_generic_ctr }, @@ -689,6 +733,8 @@ static struct cas_cls_condition_handler _handlers[] = { _cas_cls_generic_dtr }, { "request_size", _cas_cls_request_size_test, _cas_cls_numeric_ctr, _cas_cls_generic_dtr }, + { "io_direction", _cas_cls_request_direction_test, + _cas_cls_direction_ctr, _cas_cls_generic_dtr }, #ifdef CAS_WLTH_SUPPORT { "wlth", _cas_cls_wlth_test, _cas_cls_numeric_ctr, _cas_cls_generic_dtr}, @@ -757,7 +803,7 @@ static struct cas_cls_condition * _cas_cls_create_condition( return c; } -/* Read single codnition from text input and return cas_cls_condition +/* Read single condition from text input and return cas_cls_condition * representation. *rule pointer is advanced to point to next condition. * Input @rule string is modified to speed up parsing (selected bytes are * overwritten with 0). @@ -765,7 +811,7 @@ static struct cas_cls_condition * _cas_cls_create_condition( * *l_op contains logical operator from previous condition and gets overwritten * with operator read from currently parsed condition. * - * Returns pointer to condition if successfull. + * Returns pointer to condition if successful. * Returns NULL if no more conditions in string. * Returns error pointer in case of syntax or runtime error. */ @@ -1050,9 +1096,11 @@ int cas_cls_rule_create(ocf_cache_t cache, return -ENOMEM; r = _cas_cls_rule_create(cls, part_id, _rule); - if (IS_ERR(r)) + if (IS_ERR(r)) { + CAS_CLS_DEBUG_MSG( + "Cannot create rule: %s => %d\n", rule, part_id); ret = _cas_cls_rule_err_to_cass_err(PTR_ERR(r)); - else { + } else { CAS_CLS_DEBUG_MSG("Created rule: %s => %d\n", rule, part_id); *cls_rule = r; ret = 0; diff --git a/test/functional/api/cas/ioclass_config.py b/test/functional/api/cas/ioclass_config.py index ebf3d6a..3326dab 100644 --- a/test/functional/api/cas/ioclass_config.py +++ b/test/functional/api/cas/ioclass_config.py @@ -185,6 +185,9 @@ class IoClass: elif rule in ["extension", "process_name", "file_name_prefix"]: allowed_chars = string.ascii_letters + string.digits rule += f":{''.join(random.choices(allowed_chars, k=random.randint(1, 10)))}" + elif rule == "io_direction": + direction = random.choice(["read", "write"]) + rule += f":{direction}" if random.randrange(2): rule += "&done" return rule diff --git a/test/functional/tests/io_class/test_io_class_io_direction.py b/test/functional/tests/io_class/test_io_class_io_direction.py new file mode 100644 index 0000000..1431828 --- /dev/null +++ b/test/functional/tests/io_class/test_io_class_io_direction.py @@ -0,0 +1,141 @@ +# +# Copyright(c) 2025 Huawei Technologies Co., Ltd. +# SPDX-License-Identifier: BSD-3-Clause +# + +import pytest + +from api.cas import ioclass_config, casadm +from api.cas.cache_config import CacheMode, CleaningPolicy, SeqCutOffPolicy +from core.test_run import TestRun +from storage_devices.disk import DiskType, DiskTypeSet, DiskTypeLowerThan +from test_tools.dd import Dd +from test_tools.os_tools import sync, drop_caches +from test_tools.udev import Udev +from type_def.size import Unit, Size + +dd_bs = Size(1, Unit.Blocks4096) +dd_count = 1230 +cached_mountpoint = "/tmp/ioclass_core_id_test/cached" +not_cached_mountpoint = "/tmp/ioclass_core_id_test/not_cached" + + +@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) +@pytest.mark.require_disk("core", DiskTypeLowerThan("cache")) +def test_ioclass_io_direction(): + """ + title: Test for `io_direction` classification rule + description: | + Test if IO direction rule correctly classifies IOs based on their direction. + pass_criteria: + - Reads cached to IO class with 'io_direction:read' rule + - Writes cached to IO class with 'io_direction:write' rule + """ + + with TestRun.step("Prepare cache and core devices"): + cache_device = TestRun.disks["cache"] + core_device = TestRun.disks["core"] + + cache_device.create_partitions([Size(10, Unit.GibiByte)]) + core_device.create_partitions([Size(5, Unit.GibiByte)]) + + cache_device = cache_device.partitions[0] + core_device = core_device.partitions[0] + + with TestRun.step("Start cache, set NOP cleaning policy and disable sequential cutoff"): + cache = casadm.start_cache(cache_device, cache_mode=CacheMode.WT, force=True) + casadm.set_param_cleaning(cache_id=cache.cache_id, policy=CleaningPolicy.nop) + cache.set_seq_cutoff_policy(SeqCutOffPolicy.never) + + with TestRun.step("Disable udev"): + Udev.disable() + + with TestRun.step("Add core"): + cached_volume = casadm.add_core(cache, core_dev=core_device) + + with TestRun.step("Create new IO direction based classification rules"): + ioclass_config.remove_ioclass_config() + ioclass_config.create_ioclass_config( + add_default_rule=False, + ioclass_config_path=ioclass_config.default_config_file_path, + ) + ioclass_config.add_ioclass( + ioclass_id=1, + eviction_priority=22, + allocation="0.00", + rule="metadata", + ioclass_config_path=ioclass_config.default_config_file_path, + ) + + read_ioclass_id = 11 + write_ioclass_id = 12 + + ioclass_config.add_ioclass( + ioclass_id=read_ioclass_id, + eviction_priority=22, + allocation="1.00", + rule="io_direction:read&done", + ioclass_config_path=ioclass_config.default_config_file_path, + ) + ioclass_config.add_ioclass( + ioclass_id=write_ioclass_id, + eviction_priority=22, + allocation="1.00", + rule="io_direction:write&done", + ioclass_config_path=ioclass_config.default_config_file_path, + ) + + with TestRun.step("Load ioclass config file"): + casadm.load_io_classes( + cache_id=cache.cache_id, file=ioclass_config.default_config_file_path + ) + + with TestRun.step("Reset counters"): + sync() + drop_caches() + cache.purge_cache() + cache.reset_counters() + + with TestRun.step("Trigger IO read requests"): + (Dd() + .input(cached_volume.path) + .output("/dev/null") + .count(dd_count) + .block_size(dd_bs) + .iflag("direct") + .run()) + + with TestRun.step("Check IO class reads"): + read_io_class_stat = cached_volume.get_io_class_statistics(io_class_id=read_ioclass_id).request_stats.read.total + write_io_class_stat = cached_volume.get_io_class_statistics(io_class_id=write_ioclass_id).request_stats.read.total + + if read_io_class_stat != dd_count: + TestRun.LOGGER.error( + f"Wrong 'read' IO class stats! Expected {dd_count} total reads, actual: {read_io_class_stat}" + ) + if write_io_class_stat != 0: + TestRun.LOGGER.error( + f"Wrong 'write' IO class stats! Expected 0 total reads, actual: {write_io_class_stat}" + ) + + with TestRun.step("Trigger IO write requests"): + (Dd() + .input("/dev/zero") + .output(cached_volume.path) + .count(dd_count) + .block_size(dd_bs) + .oflag("direct") + .run()) + + with TestRun.step("Check IO class writes"): + read_io_class_stat = cached_volume.get_io_class_statistics(io_class_id=read_ioclass_id).request_stats.write.total + write_io_class_stat = cached_volume.get_io_class_statistics(io_class_id=write_ioclass_id).request_stats.write.total + + if read_io_class_stat != 0: + TestRun.LOGGER.error( + f"Wrong 'read' IO class stats! Expected 0 total writes, actual: {read_io_class_stat}" + ) + if write_io_class_stat != dd_count: + TestRun.LOGGER.error( + f"Wrong 'write' IO class stats! Expected {dd_count} total writes, actual: {write_io_class_stat}" + ) diff --git a/test/functional/tests/security/fuzzy/kernel/fuzzy_with_io/fuzzy_io_class/test_fuzzy_io_class_config_io_class_name.py b/test/functional/tests/security/fuzzy/kernel/fuzzy_with_io/fuzzy_io_class/test_fuzzy_io_class_config_io_class_name.py index 5b7d3ab..ccb19ce 100644 --- a/test/functional/tests/security/fuzzy/kernel/fuzzy_with_io/fuzzy_io_class/test_fuzzy_io_class_config_io_class_name.py +++ b/test/functional/tests/security/fuzzy/kernel/fuzzy_with_io/fuzzy_io_class/test_fuzzy_io_class_config_io_class_name.py @@ -1,6 +1,6 @@ # # Copyright(c) 2022 Intel Corporation -# Copyright(c) 2024 Huawei Technologies Co., Ltd. +# Copyright(c) 2024-2025 Huawei Technologies Co., Ltd. # SPDX-License-Identifier: BSD-3-Clause # @@ -44,6 +44,7 @@ parametrized_keywords = [ "pid", "file_offset", "request_size", # number - based + "io_direction", ] parameterless_keywords = ["metadata", "direct", "done"] @@ -125,7 +126,7 @@ def __get_fuzzed_parameters(parameters): for index, param in enumerate(parameters): if index < TestRun.usr.fuzzy_iter_count // 10: param = param.decode("ascii", "ignore") - i = index % len(parametrized_keywords) + len(parameterless_keywords) + 1 + i = index % (len(parametrized_keywords) + len(parameterless_keywords) + 1) match i: case 1: fuzzed_parameters.append(f"directory:{param}") @@ -153,6 +154,8 @@ def __get_fuzzed_parameters(parameters): fuzzed_parameters.append(f"direct{'' if i % 10 == 0 else param}") case 13: fuzzed_parameters.append(f"done{'' if i % 10 == 0 else param}") + case 14: + fuzzed_parameters.append(f"io_direction:{param}") case _: fuzzed_parameters.append(param) else: @@ -218,6 +221,8 @@ def __validate_single_condition(value: str): if condition_key in ["directory", "file_name_prefix", "extension", "process_name"]: if 0 < len(condition_value) <= 255: return True + elif condition_key == "io_direction": + return condition_value in ["read", "write"] elif condition_key in [ "file_size", "io_class",