From a495181adbca5e41fa64910423a05d7bf7122bd2 Mon Sep 17 00:00:00 2001 From: Katarzyna Lapinska Date: Fri, 29 Apr 2022 09:54:02 +0200 Subject: [PATCH] Add IO class test for loading wrong IO class configuration Signed-off-by: Katarzyna Lapinska --- test/functional/api/cas/cli_messages.py | 62 ++++++ test/functional/api/cas/ioclass_config.py | 19 +- ...st_io_class_prevent_wrong_configuration.py | 188 ++++++++++++++++++ 3 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 test/functional/tests/io_class/test_io_class_prevent_wrong_configuration.py diff --git a/test/functional/api/cas/cli_messages.py b/test/functional/api/cas/cli_messages.py index 99e7580..5d7a1ff 100644 --- a/test/functional/api/cas/cli_messages.py +++ b/test/functional/api/cas/cli_messages.py @@ -169,6 +169,68 @@ cache_line_size_mismatch = [ r"Cache line size mismatch" ] +headerless_io_class_config = [ + r'Cannot parse configuration file - unknown column "1"\.\n' + r'Failed to parse I/O classes configuration file header\. It is either malformed or missing\.\n' + r'Please consult Admin Guide to check how columns in configuration file should be named\.' +] + +illegal_io_class_config_L2C1 = [ + r"Cannot parse configuration file - error in line 2 in column 1 \(IO class id\)\." +] + +illegal_io_class_config_L2C2 = [ + r"Empty or too long IO class name\n" + r"Cannot parse configuration file - error in line 2 in column 2 \(IO class name\)\." +] + +illegal_io_class_config_L2C4 = [ + r"Cannot parse configuration file - error in line 2 in column 4 \(Allocation\)\." +] + +illegal_io_class_config_L2 = [ + r"Cannot parse configuration file - error in line 2\." +] + +double_io_class_config = [ + r"Double configuration for IO class id \d+\n" + r"Cannot parse configuration file - error in line \d+ in column \d+ \(IO class id\)\." +] + +illegal_io_class_invalid_id = [ + r"Invalid id, must be a correct unsigned decimal integer\.\n" + r"Cannot parse configuration file - error in line 2 in column 1 \(IO class id\)\." +] + +illegal_io_class_invalid_id_number = [ + r"Invalid id, must be in the range 0-32\.\n" + r"Cannot parse configuration file - error in line 2 in column 1 \(IO class id\)\." +] + +illegal_io_class_invalid_priority = [ + r"Invalid prio, must be a correct unsigned decimal integer\.\n" + r"Cannot parse configuration file - error in line 2 in column 3 \(Eviction priority\)" +] + +illegal_io_class_invalid_priority_number = [ + r"Invalid prio, must be in the range 0-255\.\n" + r"Cannot parse configuration file - error in line 2 in column 3 \(Eviction priority\)" +] + +illegal_io_class_invalid_allocation = [ + r"Cannot parse configuration file - error in line 2 in column 4 \(Allocation\)\." +] + +illegal_io_class_invalid_allocation_number = [ + r"Cannot parse configuration file - error in line 2 in column 4 \(Allocation\)\." +] + +malformed_io_class_header = [ + r'Cannot parse configuration file - unknown column \"value_template\"\.\n' + r'Failed to parse I/O classes configuration file header\. It is either malformed or missing\.\n' + r'Please consult Admin Guide to check how columns in configuration file should be named\.' +] + def check_stderr_msg(output: Output, expected_messages): return __check_string_msg(output.stderr, expected_messages) diff --git a/test/functional/api/cas/ioclass_config.py b/test/functional/api/cas/ioclass_config.py index 89bf82c..354e20e 100644 --- a/test/functional/api/cas/ioclass_config.py +++ b/test/functional/api/cas/ioclass_config.py @@ -1,5 +1,5 @@ # -# Copyright(c) 2019-2021 Intel Corporation +# Copyright(c) 2019-2022 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # @@ -87,6 +87,19 @@ class IoClass: def default(priority=DEFAULT_IO_CLASS_PRIORITY, allocation="1.00"): return IoClass(DEFAULT_IO_CLASS_ID, DEFAULT_IO_CLASS_RULE, priority, allocation) + @staticmethod + def default_header_dict(): + return { + "id": "IO class id", + "name": "IO class name", + "eviction_prio": "Eviction priority", + "allocation": "Allocation" + } + + @staticmethod + def default_header(): + return ','.join(IoClass.default_header_dict().values()) + @staticmethod def compare_ioclass_lists(list1: [], list2: []): return sorted(list1) == sorted(list2) @@ -94,10 +107,10 @@ class IoClass: @staticmethod def generate_random_ioclass_list(count: int, max_priority: int = MAX_IO_CLASS_PRIORITY): random_list = [IoClass.default(priority=random.randint(0, max_priority), - allocation=f"{random.randint(0,100)/100:0.2f}")] + allocation=f"{random.randint(0, 100) / 100:0.2f}")] for i in range(1, count): random_list.append(IoClass(i, priority=random.randint(0, max_priority), - allocation=f"{random.randint(0,100)/100:0.2f}") + allocation=f"{random.randint(0, 100) / 100:0.2f}") .set_random_rule()) return random_list diff --git a/test/functional/tests/io_class/test_io_class_prevent_wrong_configuration.py b/test/functional/tests/io_class/test_io_class_prevent_wrong_configuration.py new file mode 100644 index 0000000..ae45aa5 --- /dev/null +++ b/test/functional/tests/io_class/test_io_class_prevent_wrong_configuration.py @@ -0,0 +1,188 @@ +# +# Copyright(c) 2022 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# + +import pytest +from api.cas import ioclass_config, cli_messages +from core.test_run import TestRun +from test_utils.output import CmdException +from test_utils.size import Unit, Size +from tests.io_class.io_class_common import prepare, ioclass_config_path +from api.cas.ioclass_config import IoClass +from storage_devices.disk import DiskType, DiskTypeSet, DiskTypeLowerThan +from test_tools import fs_utils + +headerless_configuration = "1,unclassified,22,1.00" +double_io_class_configuration = "2,file_size:le:4096,1,1.00\n2,file_size:le:4096,1,1.00" +malformed_io_class_allocation = "malformed allocation" +malformed_io_class_eviction_priority = "malformed eviction priority" +malformed_io_class_id = "malformed IO class id" +malformed_io_class_name = "malformed IO class name" + +# Illegal configurations and expected error messages: +illegal_io_class_configurations = { + # Use only 0, 1, 2, 3 and 5 parameters number in one line as integer values + # 1 parameter + ",,,1": cli_messages.illegal_io_class_config_L2C1, + ",,1,": cli_messages.illegal_io_class_config_L2C1, + ",1,,": cli_messages.illegal_io_class_config_L2C1, + "1,,,": cli_messages.illegal_io_class_config_L2C2, + + # 2 parameters + ",,1,1": cli_messages.illegal_io_class_config_L2C1, + ",1,,1": cli_messages.illegal_io_class_config_L2C1, + ",1,1,": cli_messages.illegal_io_class_config_L2C1, + "1,,1,": cli_messages.illegal_io_class_config_L2C2, + "1,,,1": cli_messages.illegal_io_class_config_L2C2, + "1,1,,": cli_messages.illegal_io_class_config_L2C4, + + # 3 parameters + ",1,1,1": cli_messages.illegal_io_class_config_L2C1, + "1,,1,1": cli_messages.illegal_io_class_config_L2C2, + "1,1,1,": cli_messages.illegal_io_class_config_L2C4, + + # 5 parameters + "1,1,1,1,1": cli_messages.illegal_io_class_config_L2, + + # Try to configure IO class ID as: string, negative value or 33 + "IllegalInput,Superblock,22,1": cli_messages.illegal_io_class_invalid_id, + "-2,Superblock,22,1": cli_messages.illegal_io_class_invalid_id_number, + "33,Superblock,22,1": cli_messages.illegal_io_class_invalid_id_number, + + # Try to use semicolon, dots or new line as csv delimiters + "1;1;1;1": cli_messages.illegal_io_class_config_L2, + "1.1.1.1": cli_messages.illegal_io_class_config_L2, + "1\n1\n1\n1": cli_messages.illegal_io_class_config_L2, + + # Try to configure eviction priority as: string, negative value or 256 + "1,Superblock,IllegalInput,1": cli_messages.illegal_io_class_invalid_priority, + "1,Superblock,-2,1": cli_messages.illegal_io_class_invalid_priority_number, + "1,Superblock,256,1": cli_messages.illegal_io_class_invalid_priority_number, + + # Try to configure allocation as: string, negative value or 2 + "1,Superblock,22,IllegalInput": cli_messages.illegal_io_class_invalid_allocation, + "1,Superblock,255,-2": cli_messages.illegal_io_class_invalid_allocation_number, + "1,Superblock,255,2": cli_messages.illegal_io_class_invalid_allocation_number +} + + +@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) +@pytest.mark.require_disk("core", DiskTypeLowerThan("cache")) +def test_io_class_prevent_wrong_configuration(): + """ + title: Open CAS ability to prevent loading wrong configuration. + description: | + Check Open CAS ability to prevent loading configuration from wrong configuration file + like: illegal number of parameters in line, IO class values, using semicolon + instead of comma, wrong value of eviction, and priority. + pass_criteria: + - Wrong configuration must not be loaded + - There is an appropriate message about wrong io class configuration + """ + with TestRun.step("Prepare CAS device"): + cache, core = prepare(cache_size=Size(150, Unit.MiB), core_size=Size(300, Unit.MiB)) + + with TestRun.step("Display IO class configuration – shall be default"): + create_and_load_default_io_class_config(cache) + loaded_io_classes = cache.list_io_classes() + loaded_io_classes_str = '\n'.join(str(i) for i in loaded_io_classes) + TestRun.LOGGER.info(f"Loaded IO class configuration is:\n" + f"{IoClass.default_header()}\n{loaded_io_classes_str}") + + config_io_classes = IoClass.csv_to_list(fs_utils.read_file(ioclass_config_path)) + if not IoClass.compare_ioclass_lists(config_io_classes, loaded_io_classes): + TestRun.fail("Default IO class configuration not loaded correctly.") + + with TestRun.step("Create illegal configuration file containing IO configuration " + "without header and check if it can not be loaded."): + TestRun.LOGGER.info(f"Preparing headerless configuration file with following content:\n" + f"{headerless_configuration}") + fs_utils.write_file(ioclass_config_path, headerless_configuration) + try_load_malformed_config(cache, config_io_classes, + expected_err_msg=cli_messages.headerless_io_class_config) + + with TestRun.step("Create illegal configuration file containing IO configuration with " + "malformed header and check if it can not be loaded."): + for header, err_message in setup_headers().items(): + config_content = f"{header}\n{IoClass.default()}" + TestRun.LOGGER.info(f"Testing following header with default IO class:\n" + f"{config_content}") + fs_utils.write_file(ioclass_config_path, config_content) + try_load_malformed_config(cache, config_io_classes, + expected_err_msg=err_message) + + with TestRun.step("Create illegal configuration file containing double IO class configuration " + "and check if it can not be loaded."): + config_content = f"{IoClass.default_header()}\n{double_io_class_configuration}" + TestRun.LOGGER.info(f"Testing following configuration file:\n{config_content}") + fs_utils.write_file(ioclass_config_path, config_content) + try_load_malformed_config(cache, config_io_classes, + expected_err_msg=cli_messages.double_io_class_config) + + with TestRun.step("Create illegal configuration file containing malformed IO configuration " + "with correct header and check if it can not be loaded."): + for io_config, err_message in illegal_io_class_configurations.items(): + config_content = f"{IoClass.default_header()}\n{io_config}" + TestRun.LOGGER.info( + f"Testing following header with default IO class:\n{config_content}") + fs_utils.write_file(ioclass_config_path, config_content) + try_load_malformed_config(cache, config_io_classes, + expected_err_msg=err_message) + + +def try_load_malformed_config(cache, config_io_classes, expected_err_msg): + try: + cache.load_io_class(ioclass_config_path) + TestRun.LOGGER.error("Open CAS accepts malformed configuration.") + create_and_load_default_io_class_config(cache) + except CmdException as e: + TestRun.LOGGER.info(f"Open CAS did not load malformed config file as expected.") + cli_messages.check_stderr_msg(e.output, expected_err_msg) + output_io_classes = cache.list_io_classes() + if not IoClass.compare_ioclass_lists(output_io_classes, config_io_classes): + output_str = '\n'.join(str(i) for i in output_io_classes) + TestRun.LOGGER.error( + f"Loaded IO class config should be default but it is different:\n{output_str}") + + +def create_and_load_default_io_class_config(cache): + ioclass_config.create_ioclass_config(ioclass_config_path=ioclass_config_path) + cache.load_io_class(ioclass_config_path) + + +def setup_headers(): + default_header = IoClass.default_header_dict() + correct_id_header = default_header['id'] + correct_name_header = default_header['name'] + correct_eviction_priority_header = default_header['eviction_prio'] + correct_allocation_header = default_header['allocation'] + + malformed_io_class_id_header = f"{malformed_io_class_id}," \ + f"{correct_name_header}," \ + f"{correct_eviction_priority_header}," \ + f"{correct_allocation_header}" + malformed_io_class_name_header = f"{correct_id_header}," \ + f"{malformed_io_class_name}," \ + f"{correct_eviction_priority_header}," \ + f"{correct_allocation_header}" + malformed_eviction_priority_header = f"{correct_id_header}," \ + f"{correct_name_header}," \ + f"{malformed_io_class_eviction_priority}," \ + f"{correct_allocation_header}" + malformed_allocation_header = f"{correct_id_header}," \ + f"{correct_name_header}," \ + f"{correct_eviction_priority_header}," \ + f"{malformed_io_class_allocation}" + + return { + malformed_io_class_id_header: [m.replace("value_template", malformed_io_class_id) + for m in cli_messages.malformed_io_class_header], + malformed_io_class_name_header: [m.replace("value_template", malformed_io_class_name) + for m in cli_messages.malformed_io_class_header], + malformed_eviction_priority_header: [m.replace("value_template", + malformed_io_class_eviction_priority) + for m in cli_messages.malformed_io_class_header], + malformed_allocation_header: [m.replace("value_template", malformed_io_class_allocation) + for m in cli_messages.malformed_io_class_header] + }