diff --git a/src/tests/ftest/recovery/dlck_basic.py b/src/tests/ftest/recovery/dlck_basic.py new file mode 100644 index 00000000000..073ebdcc10d --- /dev/null +++ b/src/tests/ftest/recovery/dlck_basic.py @@ -0,0 +1,152 @@ +""" + Copyright 2026 Hewlett Packard Enterprise Development LP + + SPDX-License-Identifier: BSD-2-Clause-Patent +""" +import os +import re + +import yaml +from dlck_utils import TestDlck +from fault_config_utils import FaultInjection +from file_utils import distribute_files +from run_utils import run_remote + + +class DlckBasicTest(TestDlck): + """Test class for dlck command line utility. + + :avocado: recursive + """ + def check_dlck_result(self, result, fault_name): + """Check that dlck reported the expected error for an injected fault. + Args: + result (dict): The result object returned by the dlck command. + fault_name (str): The name of the injected fault. + """ + expected_errors = ("DER_INVAL", "DER_NONEXIST", "DER_ID_MISMATCH", + "DER_DF_INCOMPT", "DER_DF_INVAL", "WARNING:", "DER_PROTO") + warning_pattern = r"\b[1-9]\d*\s+warning(?:\(s\)|s?)\b" + output = f"{result.joined_stdout}\n{result.joined_stderr}" + expected_error_found = any(error in output for error in expected_errors) + warning_found = re.search(warning_pattern, output) + if not (expected_error_found or warning_found): + return f"{fault_name}: no expected error found in dlck output: {output}" + return None + + def test_dlck_basic(self): + """Basic Test: Run 'dlck' command + + :avocado: tags=all,daily_regression + :avocado: tags=hw,medium + :avocado: tags=recovery,dlck_cmd + :avocado: tags=DlckBasicTest,test_dlck_basic + """ + dmg = self.get_dmg_command() + self.log_step("Create a pool to run dlck") + pool = self.get_pool() + dlck = self.get_dlck_command() + dlck.pool_uuid.value = pool.uuid + dlck.log_dir = self.test_env.log_dir + dlck.storage.value = self.server_managers[0].get_config_value("scm_mount") + if self.server_managers[0].manager.job.using_control_metadata: + dlck.nvme.value = os.path.join( + self.server_managers[0].get_config_value("path"), "daos_control", "engine0", + "daos_nvme.conf") + + self.log_step("Perform dmg system stop to run dlck command") + dmg.system_stop() + + if self.server_managers[0].manager.job.using_control_metadata: + dlck.nvme.value = os.path.join( + self.server_managers[0].get_config_value("path"), "daos_control", "engine0", + "daos_nvme.conf") + # Mount 128G of memory-backed tmpfs for testing purposes + cmd = 'sudo "mkdir -p /mnt/dlck_test && ' \ + 'mount -t tmpfs -o size=128G tmpfs /mnt/dlck_test"' + run_remote(log=self.log, hosts=self.hostlist_servers, command=cmd) + # Display the tmpfs mount + run_remote(log=self.log, hosts=self.hostlist_servers, command="mount") + run_remote(log=self.log, hosts=self.hostlist_servers, command="df -h") + dlck.storage.value = "/mnt/dlck_test" + + self.log_step("Run dlck command to check the health of the pool and storage") + result = dlck.run() + + self.log_step("Perform dmg system start after running dlck command") + dmg.system_start() + if not result.passed: + self.fail(f"dlck failed on {result.failed_hosts}") + self.log.info("dlck basic test passed with no errors") + + def test_dlck_basic_fault(self): + """Basic Fault Test: Run 'dlck' injecting basic faults. + + :avocado: tags=all,full_regression + :avocado: tags=hw,medium + :avocado: tags=recovery,dlck_cmd,faults + :avocado: tags=DlckBasicTest,test_dlck_basic_fault + """ + errors = [] + faults_object = FaultInjection() + faults_dict = faults_object.get_faults_dict() + fault_list = self.params.get("fault_list", '/run/dlck_test_faults/*') + self.log.info("Test log dir %s", self.test_env.log_dir) + fault_inject_file = os.path.join(self.test_env.log_dir, "fi.yaml") + self.log.info("Fault injection file: %s", fault_inject_file) + self.log.info("Faults: %s", fault_list) + self.log.info("Faults dict: %s", faults_dict) + dmg = self.get_dmg_command() + pool = self.get_pool() + dlck = self.get_dlck_command() + dlck.pool_uuid.value = pool.uuid + dlck.log_dir.value = self.test_env.log_dir + dlck.exit_status_exception = False + dlck.env["D_FI_CONFIG"] = fault_inject_file + dlck.storage.value = self.server_managers[0].get_config_value("scm_mount") + + self.log_step("Perform dmg system stop to run dlck command") + dmg.system_stop() + + if self.server_managers[0].manager.job.using_control_metadata: + dlck.nvme.value = os.path.join( + self.server_managers[0].get_config_value("path"), "daos_control", "engine0", + "daos_nvme.conf") + # Mount 128G of memory-backed tmpfs for testing purposes + cmd = 'sudo "mkdir -p /mnt/dlck_test && ' \ + 'mount -t tmpfs -o size=128G tmpfs /mnt/dlck_test"' + run_remote(log=self.log, hosts=self.hostlist_servers, command=cmd) + # Display the tmpfs mount + run_remote(log=self.log, hosts=self.hostlist_servers, command="mount") + run_remote(log=self.log, hosts=self.hostlist_servers, command="df -h") + dlck.storage.value = "/mnt/dlck_test" + + for test_fault in fault_list: + if test_fault == "DAOS_FAULT_POOL_OPEN_BIO" and \ + not self.server_managers[0].manager.job.using_control_metadata: + self.log.info("Skipping test fault %s as control metadata is not used", test_fault) + continue + fault_config = { + 'fault_config': [ + {key: str(value) for key, value in faults_dict[test_fault].items()}]} + with open(fault_inject_file, 'w') as f: + yaml.dump(fault_config, f) + self.log.info("Reading the updated fault injection file contents") + + with open(fault_inject_file, 'r') as f: + file_data = f.read() + self.log.info("\n %s", file_data) + distribute_files(self.log, self.hostlist_servers, fault_inject_file, + fault_inject_file) + self.log_step("Run dlck command after injecting fault") + result = dlck.run() + error = self.check_dlck_result(result, test_fault) + if error: + errors.append(error) + + self.log_step("Perform dmg system start after running dlck command") + dmg.system_start() + + if errors: + self.fail(f"dlck basic test failed with errors: {errors}") + self.log.info("dlck basic fault test passed with no errors") diff --git a/src/tests/ftest/recovery/dlck_basic.yaml b/src/tests/ftest/recovery/dlck_basic.yaml new file mode 100644 index 00000000000..d2ed654876b --- /dev/null +++ b/src/tests/ftest/recovery/dlck_basic.yaml @@ -0,0 +1,37 @@ +hosts: + test_servers: 1 + test_clients: 1 + +timeout: 420 +server_config: + name: daos_server + engines_per_host: 1 + engines: + 0: + targets: 4 + storage: auto + +pool: + size: 30% + +dlck_test_faults: + fault_list: + - DLCK_FAULT_GETGRNAM + - DLCK_MOCK_NO_DAOS_SERVER_GROUP + - DLCK_FAULT_CREATE_POOL_DIR + - DLCK_FAULT_CREATE_LOG_DIR + - DLCK_FAULT_ENGINE_START + - DAOS_FAULT_POOL_NVME_HEALTH + - DAOS_FAULT_POOL_OPEN_UMEM + - DAOS_FAULT_POOL_OPEN_MAGIC + - DAOS_FAULT_POOL_OPEN_VERSION + - DAOS_FAULT_POOL_OPEN_UUID + - DAOS_FAULT_BTREE_OPEN_INV_CLASS_28 + - DAOS_FAULT_BTREE_OPEN_INV_CLASS_29 + - DAOS_FAULT_BTREE_OPEN_UNREG_CLASS_28 + - DAOS_FAULT_BTREE_OPEN_UNREG_CLASS_29 + - DAOS_FAULT_BTREE_FEATURES_28 + - DAOS_FAULT_BTREE_FEATURES_29 + - DAOS_FAULT_POOL_EXT_PADDING + - DAOS_FAULT_POOL_EXT_RESERVED + - DAOS_FAULT_POOL_OPEN_BIO diff --git a/src/tests/ftest/util/collection_utils.py b/src/tests/ftest/util/collection_utils.py index 7c0d3ccf08d..62a6397fa6b 100644 --- a/src/tests/ftest/util/collection_utils.py +++ b/src/tests/ftest/util/collection_utils.py @@ -1,6 +1,6 @@ """ (C) Copyright 2022-2024 Intel Corporation. - (C) Copyright 2025 Hewlett Packard Enterprise Development LP + (C) Copyright 2025-2026 Hewlett Packard Enterprise Development LP SPDX-License-Identifier: BSD-2-Clause-Patent """ @@ -30,7 +30,8 @@ FAILURE_TRIGGER = "00_trigger-launch-failure_00" TEST_EXPECT_CORE_FILES = ["./harness/core_files.py"] TEST_RESULTS_DIRS = ( - "daos_configs", "daos_logs", "cart_logs", "daos_dumps", "valgrind_logs", "stacktraces") + "daos_configs", "daos_logs", "cart_logs", "daos_dumps", "valgrind_logs", "stacktraces", + "dlck_check") def stop_daos_agent_services(logger, test): @@ -225,7 +226,7 @@ def check_server_storage(logger, test, test_result, stage): def archive_files(logger, summary, hosts, source, pattern, destination, depth, threshold, timeout, - test_result, test=None): + test_result, test=None, file_type="f"): # pylint: disable=too-many-arguments """Archive the files from the source to the destination. @@ -241,6 +242,7 @@ def archive_files(logger, summary, hosts, source, pattern, destination, depth, t timeout (int): number of seconds to wait for the command to complete. test_result (TestResult): the test result used to update the status of the test test (TestInfo, optional): the test information. Defaults to None. + file_type (str, optional): type of file to search for. Defaults to "f". Returns: int: status code: 0 = success, 16 = failure @@ -255,7 +257,8 @@ def archive_files(logger, summary, hosts, source, pattern, destination, depth, t logger.debug(" Local host: %s", hosts.intersection(get_local_host())) # List any remote files and their sizes and determine which hosts contain these files - return_code, file_hosts = list_files(logger, hosts, source, pattern, depth, test_result) + return_code, file_hosts = list_files(logger, hosts, source, pattern, depth, + test_result, file_type) if not file_hosts: # If no files are found then there is nothing else to do logger.debug("No %s files found on %s", os.path.join(source, pattern), hosts) @@ -281,7 +284,7 @@ def archive_files(logger, summary, hosts, source, pattern, destination, depth, t # Move the test files to the test-results directory on this host return_code |= move_files( - logger, file_hosts, source, pattern, destination, depth, timeout, test_result) + logger, file_hosts, source, pattern, destination, depth, timeout, test_result, file_type) if test and "core files" in summary: # Process the core files @@ -290,16 +293,17 @@ def archive_files(logger, summary, hosts, source, pattern, destination, depth, t return return_code -def list_files(logger, hosts, source, pattern, depth, test_result): +def list_files(logger, hosts, source, pattern, depth, test_result, file_type="f"): """List the files in source with that match the pattern. Args: logger (Logger): logger for the messages produced by this method - hosts (NodSet): hosts on which the files are located + hosts (NodeSet): hosts on which the files are located source (str): where the files are currently located pattern (str): pattern used to limit which files are processed depth (int): max depth for find command test_result (TestResult): the test result used to update the status of the test + file_type (str, optional): type of file to search for. Defaults to "f". Returns: tuple: a tuple containing: @@ -313,7 +317,8 @@ def list_files(logger, hosts, source, pattern, depth, test_result): logger.debug("-" * 80) logger.debug("Listing any %s files on %s", source_files, hosts) other = ["-printf", "'%M %n %-12u %-12g %12k %t %p\n'"] - result = run_remote(logger, hosts, find_command(source, pattern, depth, other)) + result = run_remote(logger, hosts, find_command(source, pattern, depth, + other, file_type)) if not result.passed: message = f"Error determining if {source_files} files exist on {result.failed_hosts}" test_result.fail_test(logger, "Process", message) @@ -321,7 +326,7 @@ def list_files(logger, hosts, source, pattern, depth, test_result): else: for data in result.output: for line in data.stdout: - if source in line: + if (source in line) or re.search(source, line): logger.debug("Found at least one file match on %s: %s", data.hosts, line) hosts_with_files.add(data.hosts) break @@ -466,18 +471,20 @@ def compress_files(logger, hosts, source, pattern, depth, test_result): return 0 -def move_files(logger, hosts, source, pattern, destination, depth, timeout, test_result): +def move_files(logger, hosts, source, pattern, destination, depth, + timeout, test_result, file_type="f"): """Move files from the source to the destination. Args: logger (Logger): logger for the messages produced by this method - hosts (NodSet): hosts on which the files are located + hosts (NodeSet): hosts on which the files are located source (str): where the files are currently located pattern (str): pattern used to limit which files are processed destination (str): where the files should be moved to on this host depth (int): max depth for find command timeout (int): number of seconds to wait for the command to complete. test_result (TestResult): the test result used to update the status of the test + file_type (str, optional): type of file to search for. Defaults to "f". Returns: int: status code: 0 = success, 16 = failure @@ -491,7 +498,8 @@ def move_files(logger, hosts, source, pattern, destination, depth, timeout, test if "stacktrace" in destination or "daos_dumps" in destination: # pylint: disable=import-outside-toplevel other = ["-print0", "|", "xargs", "-0", "-r0", "sudo", "-n", get_chown_command()] - result = run_remote(logger, hosts, find_command(source, pattern, depth, other)) + result = run_remote(logger, hosts, find_command(source, pattern, depth, + other, file_type)) if not result.passed: message = (f"Error changing {os.path.join(source, pattern)} file permissions on " f"{result.failed_hosts}") @@ -528,8 +536,8 @@ def move_files(logger, hosts, source, pattern, destination, depth, timeout, test return return_code # Move all the source files matching the pattern into the temporary remote directory - other = f"-print0 | xargs -0 -r0 -I '{{}}' {sudo_command}mv '{{}}' '{tmp_copy_dir}'/" - result = run_remote(logger, hosts, find_command(source, pattern, depth, other)) + other = f"-print0 | xargs -0 -r0 -I '{{}}' {sudo_command} mv '{{}}' '{tmp_copy_dir}'/" + result = run_remote(logger, hosts, find_command(source, pattern, depth, other, file_type)) if not result.passed: message = (f"Error moving files to temporary remote copy directory '{tmp_copy_dir}' on " f"{result.failed_hosts}") @@ -997,6 +1005,15 @@ def collect_test_result(logger, test, test_result, job_results_dir, stop_daos, a # size exceeding the threshold. test_env = TestEnvironment() if archive: + logger.debug("Making all '%s' files accessible for archiving", test_env.log_dir) + result = run_remote(logger, test.host_info.all_hosts, + "sudo -n {}".format(get_chown_command(options="-R", + file=test_env.log_dir))) + if not result.passed: + message = f"Error making {test_env.log_dir} files accessible for archiving" + test_result.fail_test(logger, "Process", message) + return_code = 16 + remote_files = OrderedDict() remote_files["local configuration files"] = { "source": test_env.log_dir, @@ -1047,6 +1064,15 @@ def collect_test_result(logger, test, test_result, job_results_dir, stop_daos, a "depth": 1, "timeout": 900, } + remote_files["dlck files"] = { + "source": test_env.log_dir, + "destination": os.path.join(job_results_dir, "latest", TEST_RESULTS_DIRS[6]), + "pattern": "dlck_check_*", + "hosts": test.host_info.all_hosts, + "depth": 1, + "file_type": "d", + "timeout": 900, + } for index, hosts in enumerate(core_files): remote_files[f"core files {index + 1}/{len(core_files)}"] = { "source": core_files[hosts]["path"], @@ -1062,7 +1088,7 @@ def collect_test_result(logger, test, test_result, job_results_dir, stop_daos, a return_code |= archive_files( logger, summary, data["hosts"].copy(), data["source"], data["pattern"], data["destination"], data["depth"], threshold, data["timeout"], - test_result, test) + test_result, test, file_type=data.get("file_type", "f")) # Generate a steps.log file return_code |= create_steps_log(logger, job_results_dir, test_result) diff --git a/src/tests/ftest/util/dlck_utils.py b/src/tests/ftest/util/dlck_utils.py new file mode 100644 index 00000000000..02e569947bf --- /dev/null +++ b/src/tests/ftest/util/dlck_utils.py @@ -0,0 +1,52 @@ +""" + Copyright 2026 Hewlett Packard Enterprise Development LP + + SPDX-License-Identifier: BSD-2-Clause-Patent +""" +from apricot import TestWithServers +from command_utils import RunCommand +from command_utils_base import FormattedParameter + + +class TestDlck(TestWithServers): + # pylint: disable=too-few-public-methods + """Base class for Dlck tests. + + :avocado: recursive + """ + + def get_dlck_command(self, path="", namespace="/run/dlck/*"): + """Get a DlckCommand object with parameters from the test yaml file. + + Args: + path (str, optional): path to location of command binary file. Defaults to "". + namespace (str, optional): path to yaml parameters. Defaults to "/run/dlck/*". + + Returns: + DlckCommand: a DlckCommand object with parameters from the test yaml file + """ + dlck = DlckCommand(path, namespace) + dlck.register_cleanup_method = self.register_cleanup + dlck.hosts = self.server_managers[0].hosts[0:1] + dlck.log_dir.value = self.log_dir + dlck.run_user = 'daos_server' + dlck.get_params(self) + return dlck + + +class DlckCommand(RunCommand): + """Defines a object representing a dlck command.""" + + def __init__(self, path="", namespace="/run/dlck/*"): + """Create a DlckCommand object. + + Args: + path (str, optional): path to location of command binary file. Defaults to "". + namespace (str, optional): path to yaml parameters. Defaults to "/run/dlck/*". + """ + super().__init__(namespace, "dlck", path) + self.pool_uuid = FormattedParameter("--file={}", None) + self.nvme = FormattedParameter("--nvme={}", None) + self.storage = FormattedParameter("--storage={}", None) + self.log_dir = FormattedParameter("--log_dir={}", None) + self.verbose = FormattedParameter("--verbose", True) diff --git a/src/tests/ftest/util/fault_config_utils.py b/src/tests/ftest/util/fault_config_utils.py index 9fd1f231f94..900638bf000 100644 --- a/src/tests/ftest/util/fault_config_utils.py +++ b/src/tests/ftest/util/fault_config_utils.py @@ -1,6 +1,6 @@ """ - (C) Copyright 2019-2024 Intel Corporation. - (C) Copyright 2026 Hewlett Packard Enterprise Development LP + Copyright 2019-2024 Intel Corporation. + Copyright 2025-2026 Hewlett Packard Enterprise Development LP SPDX-License-Identifier: BSD-2-Clause-Patent """ @@ -248,6 +248,98 @@ 'probability_y': '100', 'interval': '1', 'max_faults': '1'}, + 'DLCK_MOCK_ROOT': { + 'id': '131328', + 'interval': '1', + 'max_faults': '1'}, + 'DLCK_FAULT_GETGRNAM': { + 'id': '131329', + 'interval': '1', + 'max_faults': '1'}, + 'DLCK_MOCK_NO_DAOS_SERVER_GROUP': { + 'id': '131330', + 'interval': '1', + 'max_faults': '1'}, + 'DLCK_FAULT_GETGROUPS': { + 'id': '131331', + 'interval': '1', + 'max_faults': '1'}, + 'DLCK_MOCK_NOT_IN_DAOS_SERVER_GROUP': { + 'id': '131332', + 'interval': '1', + 'max_faults': '1'}, + 'DLCK_FAULT_CREATE_LOG_DIR': { + 'id': '131333', + 'interval': '1', + 'max_faults': '1'}, + 'DLCK_FAULT_CREATE_POOL_DIR': { + 'id': '131334', + 'interval': '1', + 'max_faults': '1'}, + 'DLCK_FAULT_ENGINE_START': { + 'id': '131335', + 'interval': '1', + 'max_faults': '1'}, + 'DLCK_FAULT_ENGINE_EXEC': { + 'id': '131336', + 'interval': '1', + 'max_faults': '1'}, + 'DLCK_FAULT_ENGINE_JOIN': { + 'id': '131337', + 'interval': '1', + 'max_faults': '1'}, + 'DLCK_FAULT_ENGINE_STOP': { + 'id': '131338', + 'interval': '1', + 'max_faults': '1'}, + 'DAOS_FAULT_POOL_NVME_HEALTH': { + 'id': '131584', + 'interval': '2', # skip sys_db + 'max_faults': '1'}, + 'DAOS_FAULT_POOL_OPEN_BIO': { + 'id': '131585'}, + 'DAOS_FAULT_POOL_OPEN_UMEM': { + 'id': '131586', + 'interval': '2'}, # skip sys_db + 'DAOS_FAULT_POOL_OPEN_MAGIC': { + 'id': '131587', + 'interval': '2'}, # skip sys_db + 'DAOS_FAULT_POOL_OPEN_VERSION': { + 'id': '131588', + 'interval': '2'}, # skip sys_db + 'DAOS_FAULT_POOL_OPEN_UUID': { + 'id': '131589', + 'interval': '2'}, # skip sys_db + 'DAOS_FAULT_BTREE_OPEN_INV_CLASS_28': { + 'id': '131590', + 'interval': '28', # containers tree fine-tuned + 'max_faults': '1'}, + 'DAOS_FAULT_BTREE_OPEN_INV_CLASS_29': { + 'id': '131590', + 'interval': '29', # gc tree fine-tuned + 'max_faults': '1'}, + 'DAOS_FAULT_BTREE_OPEN_UNREG_CLASS_28': { + 'id': '131591', + 'interval': '28', # containers tree fine-tuned + 'max_faults': '1'}, + 'DAOS_FAULT_BTREE_OPEN_UNREG_CLASS_29': { + 'id': '131591', + 'interval': '29', # gc tree fine-tuned + 'max_faults': '1'}, + 'DAOS_FAULT_BTREE_FEATURES_28': { + 'id': '131592', + 'interval': '28', # containers tree fine-tuned + 'max_faults': '1'}, + 'DAOS_FAULT_BTREE_FEATURES_29': { + 'id': '131592', + 'interval': '29', # gc tree fine-tuned + 'max_faults': '1'}, + 'DAOS_FAULT_POOL_EXT_PADDING': { + 'id': '131593', + 'interval': '1'}, + 'DAOS_FAULT_POOL_EXT_RESERVED': { + 'id': '131594', + 'interval': '1'}, } @@ -268,6 +360,12 @@ def __init__(self): self._test_dir = None self._fault_list = [] + def get_faults_dict(self): + """Get the predefined fault dictionary. + Returns: dict: The fault dictionary + """ + return FAULTS + def write_fault_file(self, on_the_fly_fault=None): """Write out a fault injection config file. diff --git a/src/tests/ftest/util/run_utils.py b/src/tests/ftest/util/run_utils.py index e4785743912..33bf1c1649d 100644 --- a/src/tests/ftest/util/run_utils.py +++ b/src/tests/ftest/util/run_utils.py @@ -519,7 +519,7 @@ def command_as_user(command, user, env=None): return " ".join(cmd_list) -def find_command(source, pattern, depth, other=None): +def find_command(source, pattern, depth, other=None, file_type="f"): """Get the find command. Args: @@ -528,11 +528,12 @@ def find_command(source, pattern, depth, other=None): depth (int): max depth for find command other (object, optional): other commands, as a list or str, to include at the end of the base find command. Defaults to None. + file_type (str, optional): type of file to search for. Defaults to "f". Returns: str: the find command """ - command = ["find", source, "-maxdepth", str(depth), "-type", "f", "-name", f"'{pattern}'"] + command = ["find", source, "-maxdepth", str(depth), "-type", file_type, "-name", f"'{pattern}'"] if isinstance(other, list): command.extend(other) elif isinstance(other, str): diff --git a/src/utils/dlck/cmds/dlck_cmd_check.c b/src/utils/dlck/cmds/dlck_cmd_check.c index af7156246d2..5a61b2050cd 100644 --- a/src/utils/dlck/cmds/dlck_cmd_check.c +++ b/src/utils/dlck/cmds/dlck_cmd_check.c @@ -8,6 +8,8 @@ #include #include +#include + #include "../dlck_args.h" #include "../dlck_bitmap.h" #include "../dlck_checker.h" @@ -259,11 +261,18 @@ dlck_cmd_check(struct dlck_control *ctrl) D_ASSERT(ctrl != NULL); struct checker *ck = &ctrl->checker; - char log_dir_template[] = "/tmp/dlck_check_XXXXXX"; + char *log_dir_template = NULL; struct dlck_engine *engine = NULL; int *rcs; int rc; + /** generate the log directory path template */ + D_ASPRINTF(log_dir_template, "%s/dlck_check_XXXXXX", ctrl->common.log_dir); + if (log_dir_template == NULL) { + rc = -DER_NOMEM; + CK_PRINTL_RC(ck, rc, "Cannot allocate log directory path"); + return rc; + } /** create a log directory */ if (DAOS_FAIL_CHECK(DLCK_FAULT_CREATE_LOG_DIR)) { /** fault injection */ ctrl->log_dir = NULL; @@ -274,7 +283,7 @@ dlck_cmd_check(struct dlck_control *ctrl) if (ctrl->log_dir == NULL) { rc = daos_errno2der(errno); CK_PRINTL_RC(ck, rc, "Cannot create log directory"); - return rc; + goto err_free_template; } CK_PRINTF(ck, "Log directory: %s\n", ctrl->log_dir); @@ -282,7 +291,7 @@ dlck_cmd_check(struct dlck_control *ctrl) rc = dlck_engine_start(&ctrl->engine, &engine); CK_APPENDL_RC(ck, rc); if (rc != DER_SUCCESS) { - return rc; + goto err_free_template; } if (d_list_empty(&ctrl->files.list)) { @@ -328,6 +337,7 @@ dlck_cmd_check(struct dlck_control *ctrl) /** Ignore an error for now to print the collected results. */ dlck_report_results(rcs, ctrl->engine.targets, ctrl->warnings_num, ck); D_FREE(rcs); + D_FREE(log_dir_template); /** Return the first encountered error. */ return rc; @@ -336,6 +346,8 @@ dlck_cmd_check(struct dlck_control *ctrl) D_FREE(rcs); err_stop_engine: (void)dlck_engine_stop(engine); +err_free_template: + D_FREE(log_dir_template); return rc; } diff --git a/src/utils/dlck/dlck_args.h b/src/utils/dlck/dlck_args.h index 4cac4865659..1af24ffbb0a 100644 --- a/src/utils/dlck/dlck_args.h +++ b/src/utils/dlck/dlck_args.h @@ -1,5 +1,5 @@ /** - * (C) Copyright 2025 Hewlett Packard Enterprise Development LP + * (C) Copyright 2025-2026 Hewlett Packard Enterprise Development LP * * SPDX-License-Identifier: BSD-2-Clause-Patent */ @@ -27,6 +27,7 @@ #define KEY_COMMON_OPTIONS 'o' #define KEY_COMMON_WRITE_MODE 'w' #define KEY_COMMON_VERBOSE 'v' +#define KEY_COMMON_LOG_DIR 'l' #define KEY_FILES 'f' /** the options below follow the daos_engine options */ #define KEY_ENGINE_NUMA_NODE 'p' @@ -40,6 +41,7 @@ #define DLCK_DEFAULT_MAX_DMA_BUF_SIZE 5120 #define DLCK_DEFAULT_NVME_HUGEPAGE_SIZE 2 #define DLCK_DEFAULT_TARGETS 4 +#define DLCK_DEFAULT_LOG_DIR "/tmp" #define DLCK_TARGET_MAX 31 @@ -47,8 +49,9 @@ struct dlck_args_common { struct checker_options options; - bool write_mode; /** false by default (dry run) */ - bool verbose; /** false by default */ + bool write_mode; + bool verbose; + char *log_dir; }; /** diff --git a/src/utils/dlck/dlck_args_common.c b/src/utils/dlck/dlck_args_common.c index d11a56239c6..4169b2f0d2c 100644 --- a/src/utils/dlck/dlck_args_common.c +++ b/src/utils/dlck/dlck_args_common.c @@ -1,5 +1,5 @@ /** - * (C) Copyright 2025 Hewlett Packard Enterprise Development LP + * (C) Copyright 2025-2026 Hewlett Packard Enterprise Development LP * * SPDX-License-Identifier: BSD-2-Clause-Patent */ @@ -16,6 +16,7 @@ static struct argp_option args_common_options[] = { OPT_HEADER("Options:", GROUP_OPTIONS), /** entries below inherits the group number of the header entry */ {"write_mode", KEY_COMMON_WRITE_MODE, 0, 0, "Make changes persistent."}, + {"log_dir", KEY_COMMON_LOG_DIR, "PATH", 0, "Log directory. Default: " DLCK_DEFAULT_LOG_DIR}, {"options", KEY_COMMON_OPTIONS, "OPTIONS", 0, "Set options. Options are comma-separated and may include arguments using the equals sign " "('='). Please see available options below."}, @@ -43,6 +44,7 @@ args_common_init(struct dlck_args_common *args) /** set defaults */ args->write_mode = false; /** dry run */ args->verbose = false; + args->log_dir = DLCK_DEFAULT_LOG_DIR; args->options.cko_non_zero_padding = CHECKER_EVENT_WARNING; } @@ -95,6 +97,9 @@ args_common_parser(int key, char *arg, struct argp_state *state) case KEY_COMMON_VERBOSE: args->verbose = true; break; + case KEY_COMMON_LOG_DIR: + args->log_dir = arg; + break; case KEY_COMMON_OPTIONS: rc = args_common_options_parse(arg, &args->options, state); break;