diff --git a/src/tests/ftest/recovery/dlck_basic.py b/src/tests/ftest/recovery/dlck_basic.py new file mode 100644 index 00000000000..416e35cb16d --- /dev/null +++ b/src/tests/ftest/recovery/dlck_basic.py @@ -0,0 +1,127 @@ +""" + Copyright 2026 Hewlett Packard Enterprise Development LP + + SPDX-License-Identifier: BSD-2-Clause-Patent +""" +import os +import re + +from dlck_test_base import DlckTestBase +from fault_config_utils import FaultInjection +from file_utils import distribute_files + + +class DlckBasicTest(DlckTestBase): + """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:") + 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.value = self.test_env.log_dir + dlck.run_user = 'daos_server' + 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() + + 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.run_user = 'daos_server' + 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") + 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() + + for test_fault in fault_list: + with open(fault_inject_file, 'w') as f: + f.write("fault_config:\n") + for count, (key, value) in enumerate(faults_dict[test_fault].items()): + if count == 0: + f.write(f"- {key}: \'{value}\'\n") + else: + f.write(f" {key}: \'{value}\'\n") + 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..fa9d806585c --- /dev/null +++ b/src/tests/ftest/recovery/dlck_basic.yaml @@ -0,0 +1,39 @@ +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: 10% + +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 + - DLCK_FAULT_ENGINE_JOIN + - DLCK_FAULT_ENGINE_STOP + - DAOS_FAULT_POOL_NVME_HEALTH + - DAOS_FAULT_POOL_OPEN_BIO + - 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 diff --git a/src/tests/ftest/util/command_utils.py b/src/tests/ftest/util/command_utils.py index f5d2885f94e..2b0ed39bc5e 100644 --- a/src/tests/ftest/util/command_utils.py +++ b/src/tests/ftest/util/command_utils.py @@ -1,6 +1,6 @@ """ (C) Copyright 2018-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 """ @@ -23,7 +23,7 @@ from file_utils import change_file_owner, create_directory, distribute_files from general_utils import (DaosTestError, check_file_exists, get_file_listing, get_job_manager_class, get_subprocess_stdout, run_command) -from run_utils import command_as_user, run_remote +from run_utils import command_as_user, run_local, run_remote, stop_processes from user_utils import get_primary_group from yaml_utils import get_yaml_data @@ -95,6 +95,9 @@ def __init__(self, namespace, command, path="", subprocess=False, check_results= if check_results: self.check_results_list = list(check_results) + # Internal flag used to indicate if cleanup is required for the command. + self.__cleanup_needed = False + def __str__(self): """Return the command with all of its defined parameters as a string. @@ -206,6 +209,7 @@ def run(self, raise_exception=None): CommandFailure: if there is an error running the command """ + self.__cleanup_needed = True if self.run_as_subprocess: self._run_subprocess() return None @@ -263,7 +267,7 @@ def check_results(self): if self.result and self.check_results_list: regex = r"({})".format("|".join(self.check_results_list)) self.log.debug("Checking the command output for any bad keywords: %s", regex) - for output in (self.result.stdout_text, self.result.stderr_text): + for output in (self._result_stdout(), self._result_stderr()): match = re.findall(regex, output) if match: self.log.info( @@ -275,6 +279,26 @@ def check_results(self): break return status + def _result_stdout(self): + """Get all the stdout from the command result. + + Returns: + str: the command result's stdout as a string + """ + if not self.result: + raise CommandFailure("No command result available to return stdout") + return self.result.stdout_text + + def _result_stderr(self): + """Get all the stderr from the command result. + + Returns: + str: the command result's stderr as a string + """ + if not self.result: + raise CommandFailure("No command result available to return stderr") + return self.result.stderr_text + def _run_subprocess(self): """Run the command as a sub process. @@ -372,6 +396,40 @@ def stop(self): self.log.info("%s stopped successfully", self.command) self._process = None + def cleanup_command(self): + """Cleanup the command. + + Returns: + list: a list of errors encountered during cleanup. + """ + if not self.__cleanup_needed: + self.log.info("No cleanup needed for %s", self.command) + return [] + + self.log.info("Cleaning up %s", self.command) + regex = self.command_regex + if self.full_command_regex: + regex = f"'{str(self)}'" + hosts = None + if hasattr(self, "hosts"): + hosts = self.hosts + detected, running = stop_processes( + self.log, hosts, regex, full_command=self.full_command_regex) + if not detected: + self.log.info( + "No remote %s processes killed on %s (none found), done.", + regex, "local host" if not hosts else hosts) + elif running: + self.log.info( + "***Unable to kill remote %s process on %s! Please investigate/report.***", + regex, running) + else: + self.log.info( + "***At least one remote %s process needed to be killed on %s! Please investigate/" + "report.***", regex, detected) + self.__cleanup_needed = False + return [] + def wait(self): """Wait for the sub process to complete. @@ -447,7 +505,7 @@ def get_output(self, method_name, regex_method=None, **kwargs): # Parse the output and return if not regex_method: regex_method = method_name - return self.parse_output(result.stdout_text, regex_method) + return self.parse_output(self._result_stdout(), regex_method) def parse_output(self, stdout, regex_method): """Parse output using findall() with supplied 'regex_method' as pattern. @@ -464,7 +522,7 @@ def parse_output(self, stdout, regex_method): """ if regex_method not in self.METHOD_REGEX: - raise CommandFailure("No pattern regex defined for '{}()'".format(regex_method)) + raise CommandFailure(f"No pattern regex defined for '{regex_method}()'") return re.findall(self.METHOD_REGEX[regex_method], stdout) def get_params(self, test): @@ -1511,3 +1569,118 @@ def __init__(self, run_user='root'): self.unit_command = BasicParameter(None, position=1) self.service = BasicParameter(None, position=2) + + +class RunCommand(ExecutableCommand): + """A class for command run with run_remote()/run_local().""" + + def __init__(self, namespace, command, path="", check_results=None, run_user=None): + """Create a RunCommand object. + + Uses run_remote()/run_local() to run a command str provided. + + Args: + namespace (str): yaml namespace (path to parameters) + command (str): string of the command to be executed. + path (str, optional): path to location of command binary file. Defaults to "". + check_results (list, optional): list of words used to mark the command as failed if + any are found in the command output. Defaults to None. + run_user (str, optional): user to run as. Defaults to None, which will run commands as + the current user. + """ + super().__init__(namespace, command, path, False, check_results, run_user) + self._hosts = None + self.register_cleanup_method = None + + @property + def hosts(self): + """Get the host(s) on which to remotely run the command via run(). + + Returns: + NodeSet: remote host(s) on which the command will run. + """ + return self._hosts + + @hosts.setter + def hosts(self, value): + """Set the host(s) on which to remotely run the command via run(). + + If the specified host is None the command will run locally w/o ssh. + + Args: + value (NodeSet): remote host(s) on which to run the command + + Raises: + TypeError: if value is not a NodeSet + """ + if not isinstance(value, NodeSet): + raise TypeError(f"Invalid {self.command} host NodeSet: {value} ({type(value)})") + self._hosts = value.copy() + + def _run_process(self, raise_exception=None): + """Run the command as a foreground process. + + Args: + raise_exception (bool, optional): whether or not to raise an exception if the command + fails. This overrides the self.exit_status_exception + setting if defined. Defaults to None. + + Raises: + CommandFailure: if there is an error running the command with raise_exception or + self.exit_status_exception (when raise_exception is None) set to True. + + Returns: + CommandResult: result from running the command + """ + if raise_exception is None: + raise_exception = self.exit_status_exception + + if callable(self.register_cleanup_method): + # Stop any running processes started by this job manager when the test completes + # pylint: disable=not-callable + self.register_cleanup_method(self.cleanup_command) + + self.result = None + if not self.hosts: + result = run_local(self.log, self.with_exports, self.verbose, self.timeout) + else: + result = run_remote(self.log, self.hosts, self.with_exports, self.verbose, self.timeout) + self.result = result + + if raise_exception and not result.passed: + raise CommandFailure(f"Error running {self.command} on: {result.failed_hosts}") + + if raise_exception and not self.check_results(): + raise CommandFailure( + f"Error running {self.command}: a {self.check_results_list} keyword was detected") + + return result + + def _result_stdout(self): + """Get all the stdout from the command result. + + Returns: + str: the command result's stdout as a string + """ + if not self.result: + raise CommandFailure("No command result available to return stdout") + return self.result.joined_stdout + + def _result_stderr(self): + """Get all the stderr from the command result. + + Returns: + str: the command result's stderr as a string + """ + if not self.result: + raise CommandFailure("No command result available to return stderr") + return self.result.joined_stderr + + def _get_new(self): + """Get a new object based upon this one. + + Returns: + RunCommand: a new RunCommand object + """ + return RunCommand( + self.namespace, self._command, self._path, self.check_results_list, self.run_user) diff --git a/src/tests/ftest/util/dlck_test_base.py b/src/tests/ftest/util/dlck_test_base.py new file mode 100644 index 00000000000..b9dfbe05a6c --- /dev/null +++ b/src/tests/ftest/util/dlck_test_base.py @@ -0,0 +1,51 @@ +""" + 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 DlckTestBase(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) + self.register_cleanup(dlck.cleanup_command) + dlck.hosts = self.server_managers[0].hosts[0:1] + dlck.log_dir = self.log_dir + 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 5dd3071271f..794ccb8e9b8 100644 --- a/src/tests/ftest/util/fault_config_utils.py +++ b/src/tests/ftest/util/fault_config_utils.py @@ -1,5 +1,6 @@ """ - (C) Copyright 2019-2024 Intel Corporation. + Copyright 2019-2024 Intel Corporation. + Copyright 2025-2026 Hewlett Packard Enterprise Development LP SPDX-License-Identifier: BSD-2-Clause-Patent """ @@ -235,6 +236,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'}, } @@ -255,6 +348,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/utils/dlck/cmds/dlck_cmd_check.c b/src/utils/dlck/cmds/dlck_cmd_check.c index b48116b6dd6..124727cf2b7 100644 --- a/src/utils/dlck/cmds/dlck_cmd_check.c +++ b/src/utils/dlck/cmds/dlck_cmd_check.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 */ @@ -8,6 +8,8 @@ #include #include +#include + #include "../dlck_args.h" #include "../dlck_bitmap.h" #include "../dlck_checker.h" @@ -146,11 +148,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; @@ -161,7 +170,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); @@ -169,7 +178,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)) { @@ -215,6 +224,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; @@ -223,6 +233,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;