diff --git a/Jenkinsfile b/Jenkinsfile index dd086e88a0e..38f267a8d0a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -476,7 +476,7 @@ void rpm_test_post(String stageName, String node) { script: 'hostname; ssh -i ci_key jenkins@' + firstNode + ' ls -ltar /tmp; mkdir -p "' + env.STAGE_NAME + '/" && ' + 'scp -i ci_key jenkins@' + firstNode + - ':/tmp/{{suite_dmg,daos_{server_helper,{control,agent}}}.log,daos_server.log.*} "' + + ':/tmp/{{libdaos_control,daos_{server_helper,{control,agent}}}.log,daos_server.log.*} "' + stageName + '/"' archiveArtifacts artifacts: env.STAGE_NAME + '/**' job_status_update() @@ -1125,6 +1125,10 @@ pipeline { label cachedCommitPragma(pragma: 'VM1-label', def_val: params.CI_UNIT_VM1_LABEL) } steps { + // Memcheck the valgrind-tagged build so the Go runtime's + // valgrind client requests suppress the resident-runtime + // noise from libdaos_control.so, like the NLT stage does. + unstash 'opt-daos-valgrind' job_step_update( unitTest(timeout_time: 160, unstash_opt: true, @@ -1151,6 +1155,8 @@ pipeline { label params.CI_UNIT_VM1_NVME_LABEL } steps { + // Memcheck the valgrind-tagged build (see 'Unit Test with memcheck'). + unstash 'opt-daos-valgrind' job_step_update( unitTest(timeout_time: 180, unstash_opt: true, diff --git a/ci/unit/test_main_node.sh b/ci/unit/test_main_node.sh index 38dace0cb3e..c91ded0549a 100755 --- a/ci/unit/test_main_node.sh +++ b/ci/unit/test_main_node.sh @@ -43,7 +43,12 @@ fi cd "${SL_SRC_DIR}" mkdir new_dir sudo cp -a new_dir /opt/daos -tar --strip-components=2 --directory /opt/daos -xf opt-daos.tar +# The memcheck stages ship the valgrind-tagged build (opt-daos-valgrind.tar) so +# the Go runtime's valgrind client requests suppress the resident-runtime noise; +# other stages ship the standard opt-daos.tar. Use whichever was unstashed. +opt_tar=opt-daos.tar +[ -f opt-daos-valgrind.tar ] && opt_tar=opt-daos-valgrind.tar +tar --strip-components=2 --directory /opt/daos -xf "$opt_tar" sudo bash -c ". ./utils/sl/setup_local.sh; ./utils/setup_daos_server_helper.sh" diff --git a/site_scons/site_tools/go_builder.py b/site_scons/site_tools/go_builder.py index fbba4ad2d80..038a4e7f31e 100644 --- a/site_scons/site_tools/go_builder.py +++ b/site_scons/site_tools/go_builder.py @@ -58,7 +58,11 @@ def _scan_go_file(node, env, _path): if dep[-1] == '"': includes.append(File(os.path.join(src_dir, header))) else: - includes.append(f'../../../include/{header}') + # For angle-bracket includes, only track DAOS headers (in src/include/). + # Skip system headers like , , etc. + daos_hdr = os.path.join(Dir('#').abspath, 'src', 'include', header) + if os.path.exists(daos_hdr): + includes.append(daos_hdr) return includes diff --git a/src/SConscript b/src/SConscript index d192ceac4c0..16cf64604fb 100644 --- a/src/SConscript +++ b/src/SConscript @@ -104,6 +104,7 @@ def scons(): # Generate common libraries used by multiple components SConscript('gurt/SConscript') SConscript('cart/SConscript') + SConscript('control/lib/control/c/SConscript') SConscript('common/SConscript') SConscript('bio/SConscript') SConscript('vea/SConscript') diff --git a/src/cart/utils/SConscript b/src/cart/utils/SConscript index 7475b7f4ef3..8b1475bed5f 100644 --- a/src/cart/utils/SConscript +++ b/src/cart/utils/SConscript @@ -1,4 +1,5 @@ # (C) Copyright 2021-2023 Intel Corporation. +# (C) Copyright 2026 Hewlett Packard Enterprise Development LP # # SPDX-License-Identifier: BSD-2-Clause-Patent # @@ -22,7 +23,7 @@ def scons(): cart_utils_objs = cenv.SharedObject(['crt_utils.c'], SHOBJPREFIX='s_') Export('cart_utils_objs') - env.Install(conf_dir, ['memcheck-cart.supp', 'fault-inject-cart.yaml']) + env.Install(conf_dir, ['memcheck-cart.supp', 'memcheck-go.supp', 'fault-inject-cart.yaml']) if __name__ == "SCons.Script": diff --git a/src/cart/utils/memcheck-cart.supp b/src/cart/utils/memcheck-cart.supp index 20b7bba8541..7208ccac249 100644 --- a/src/cart/utils/memcheck-cart.supp +++ b/src/cart/utils/memcheck-cart.supp @@ -1,137 +1,5 @@ -# Go runtime -{ - Go runtime: GC memory (valgrind integration entry point) - Memcheck:Leak - match-leak-kinds: definite,indirect,possible,reachable - fun:runtime.valgrindClientRequest* - ... -} -{ - Go runtime: persistentalloc (cgo malloc, never freed) - Memcheck:Leak - match-leak-kinds: reachable - fun:malloc - fun:_cgo_*_Cfunc__Cmalloc - fun:runtime.asmcgocall.abi0 - ... - fun:runtime.persistentalloc -} -{ - Go runtime: newproc (cgo malloc, never freed) - Memcheck:Leak - match-leak-kinds: reachable - fun:malloc - fun:_cgo_*_Cfunc__Cmalloc - fun:runtime.asmcgocall.abi0 - ... - fun:runtime.newproc.abi0 -} -{ - Go runtime: bootstrap (cgo malloc, never freed) - Memcheck:Leak - match-leak-kinds: reachable - fun:malloc - ... - fun:runtime.asmcgocall.abi0 - ... - fun:runtime.rt0_go.abi0 -} -# bytealg over-reads buffer ends by design; golang/go#27610 -# recommends this suppression -{ - golang/go#27610: bytealg indexbytebody over-read - Memcheck:Addr8 - ... - fun:indexbytebody -} -{ - golang/go#27610: bytealg indexbytebody over-read - Memcheck:Addr16 - ... - fun:indexbytebody -} -{ - golang/go#27610: bytealg indexbytebody over-read - Memcheck:Addr32 - ... - fun:indexbytebody -} -{ - golang/go#27610: bytealg indexbytebody over-read - Memcheck:Cond - ... - fun:indexbytebody -} - -# glibc / loader / NSS -{ - glibc: per-thread TLS (dtv) - Memcheck:Leak - match-leak-kinds: possible,reachable - fun:calloc - ... - fun:_dl_allocate_tls - ... - fun:pthread_create* - ... -} -{ - glibc: dl_init - Memcheck:Leak - ... - fun:_dl_init -} -{ - glibc: dl_open - Memcheck:Leak - ... - fun:_dl_open -} -{ - glibc: dl_fini - Memcheck:Leak - match-leak-kinds: reachable - ... - fun:_dl_fini - ... -} -{ - glibc: dlerror_run - Memcheck:Leak - match-leak-kinds: reachable - ... - fun:_dlerror_run - ... -} -{ - glibc: dl_fixup - Memcheck:Leak - match-leak-kinds: reachable - ... - fun:_dl_fixup - ... -} -{ - glibc: getpwnam_r (NSS) - Memcheck:Leak - fun:*alloc - ... - fun:getpwnam_r* -} -{ - glibc: getpwuid_r (NSS) - Memcheck:Leak - fun:calloc - ... - fun:getpwuid_r* -} -{ - glibc: localtime / tz data - Memcheck:Leak - fun:malloc - ... - fun:__tz_convert -} +# Go-runtime, bytealg, and glibc/loader rules have moved to memcheck-go.supp. +# Load that file alongside this one for any valgrind-tagged Go binary. # libfabric (OFI) { diff --git a/src/cart/utils/memcheck-go.supp b/src/cart/utils/memcheck-go.supp new file mode 100644 index 00000000000..f8e245e664f --- /dev/null +++ b/src/cart/utils/memcheck-go.supp @@ -0,0 +1,149 @@ +# (C) Copyright 2026 Hewlett Packard Enterprise Development LP +# +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +# Shared Go-runtime memcheck suppressions for binaries and c-shared libraries +# built with the Go "valgrind" tag (BUILD_GO_VALGRIND=1). These rules are +# consumed by both NLT (node_local_test.py) and run_utest.py so that any Go +# binary or c-shared library (e.g. libdaos_control.so) tagged for valgrind +# gets the same baseline set of suppressions without duplicating them. +# +# Rules here cover: +# - Go runtime GC / cgo malloc patterns +# - bytealg over-reads (golang/go#27610) +# - glibc / loader / NSS patterns triggered by Go cgo-TLS startup + +# Go runtime +{ + Go runtime: GC memory (valgrind integration entry point) + Memcheck:Leak + match-leak-kinds: definite,indirect,possible,reachable + fun:runtime.valgrindClientRequest* + ... +} +{ + Go runtime: persistentalloc (cgo malloc, never freed) + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:_cgo_*_Cfunc__Cmalloc + fun:runtime.asmcgocall.abi0 + ... + fun:runtime.persistentalloc +} +{ + Go runtime: newproc (cgo malloc, never freed) + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:_cgo_*_Cfunc__Cmalloc + fun:runtime.asmcgocall.abi0 + ... + fun:runtime.newproc.abi0 +} +{ + Go runtime: bootstrap (cgo malloc, never freed) + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + ... + fun:runtime.asmcgocall.abi0 + ... + fun:runtime.rt0_go.abi0 +} +# bytealg over-reads buffer ends by design; golang/go#27610 +# recommends this suppression +{ + golang/go#27610: bytealg indexbytebody over-read + Memcheck:Addr8 + ... + fun:indexbytebody +} +{ + golang/go#27610: bytealg indexbytebody over-read + Memcheck:Addr16 + ... + fun:indexbytebody +} +{ + golang/go#27610: bytealg indexbytebody over-read + Memcheck:Addr32 + ... + fun:indexbytebody +} +{ + golang/go#27610: bytealg indexbytebody over-read + Memcheck:Cond + ... + fun:indexbytebody +} + +# glibc / loader / NSS +{ + glibc: per-thread TLS (dtv) + Memcheck:Leak + match-leak-kinds: possible,reachable + fun:calloc + ... + fun:_dl_allocate_tls + ... + fun:pthread_create* + ... +} +{ + glibc: dl_init + Memcheck:Leak + ... + fun:_dl_init +} +{ + glibc: dl_open + Memcheck:Leak + ... + fun:_dl_open +} +{ + glibc: dl_fini + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:_dl_fini + ... +} +{ + glibc: dlerror_run + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:_dlerror_run + ... +} +{ + glibc: dl_fixup + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:_dl_fixup + ... +} +{ + glibc: getpwnam_r (NSS) + Memcheck:Leak + fun:*alloc + ... + fun:getpwnam_r* +} +{ + glibc: getpwuid_r (NSS) + Memcheck:Leak + fun:calloc + ... + fun:getpwuid_r* +} +{ + glibc: localtime / tz data + Memcheck:Leak + fun:malloc + ... + fun:__tz_convert +} diff --git a/src/chk/chk_internal.h b/src/chk/chk_internal.h index a6caa726539..90c59b5bc77 100644 --- a/src/chk/chk_internal.h +++ b/src/chk/chk_internal.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -328,7 +329,7 @@ CRT_RPC_DECLARE(chk_set_policy, DAOS_ISEQ_CHK_SET_POLICY, DAOS_OSEQ_CHK_SET_POLI #define CHK_MSG_BUFLEN 320 -#define CHK_INTERACT_OPTION_MAX 3 +#define CHK_INTERACT_OPTION_MAX DAOS_CHECK_INTERACT_OPTION_MAX /* * NOTE: Please be careful when change CHK__CHECK_INCONSIST_CLASS__CIC_UNKNOWN diff --git a/src/common/SConscript b/src/common/SConscript index 83f248e7d31..9f0c2807716 100644 --- a/src/common/SConscript +++ b/src/common/SConscript @@ -75,11 +75,20 @@ def scons(): if not prereqs.test_requested(): return + # The daos_tests test library links against daos_control, so tests cannot + # be built when the client is not also being built. + if not prereqs.client_requested(): + return + + Import('daos_control_tgts') + tlibenv = env.Clone(LIBS=[]) tlibenv.require('argobots', 'isal', 'isal_crypto', 'protobufc') - tlibenv.AppendUnique(LIBS=['cart', 'gurt', 'lz4', 'json-c']) + tlibenv.AppendUnique(LIBS=['cart', 'gurt', 'lz4', 'json-c', 'daos_control']) + tlibenv.AppendUnique(RPATH_FULL=['$PREFIX/lib64']) tests_lib = tlibenv.d_library('daos_tests', ['tests_lib.c', 'tests_dmg_helpers.c']) + Depends(tests_lib, daos_control_tgts) tlibenv.Install('$PREFIX/lib64/', tests_lib) tenv = denv.Clone() diff --git a/src/common/tests/btree.sh b/src/common/tests/btree.sh index 1bc85b21ccb..897bdb9552e 100755 --- a/src/common/tests/btree.sh +++ b/src/common/tests/btree.sh @@ -9,8 +9,10 @@ VCMD="" BAT_NUM=${BAT_NUM:-"20000"} if [ "$USE_VALGRIND" = "memcheck" ]; then BAT_NUM="200" + supp_args="--suppressions=${VALGRIND_SUPP}" + [ -n "${VALGRIND_GO_SUPP:-}" ] && supp_args="${supp_args} --suppressions=${VALGRIND_GO_SUPP}" VCMD="valgrind --leak-check=full --show-reachable=yes --error-limit=no \ - --suppressions=${VALGRIND_SUPP} --error-exitcode=42 --xml=yes \ + ${supp_args} --error-exitcode=42 --xml=yes \ --xml-file=unit-test-btree-%p.memcheck.xml" elif [ "$USE_VALGRIND" = "pmemcheck" ]; then VCMD="valgrind --tool=pmemcheck" diff --git a/src/common/tests_dmg_helpers.c b/src/common/tests_dmg_helpers.c index 7e688514812..f5eedbff64a 100644 --- a/src/common/tests_dmg_helpers.c +++ b/src/common/tests_dmg_helpers.c @@ -7,995 +7,284 @@ #include #include -#include -#include #include -#include -#include #include #include #include #include +#include /* - * D_LOG_DMG_STDERR_ENV provides the environment variable that can be used to enable test binaries - * to log the stderr output from executing dmg. This can be useful for debugging. + * control_types.h declares DAOS_MAX_TARGETS_PER_DEVICE as a standalone copy of + * the BIO target-per-device cap so it can live in a client-side public header + * without pulling in daos_srv/bio.h. This TU is one of the few places that + * sees both headers; catch drift at compile time. */ -#define D_LOG_DMG_STDERR_ENV "D_TEST_LOG_DMG_STDERR" +D_CASSERT(DAOS_MAX_TARGETS_PER_DEVICE == BIO_MAX_VOS_TGT_CNT); +D_CASSERT(sizeof(((device_list *)0)->dl_state) == DAOS_DEV_STATE_MAX_LEN); -static bool -is_stderr_logging_enabled(void) -{ - int rc; - bool enabled = false; - - rc = d_getenv_bool(D_LOG_DMG_STDERR_ENV, &enabled); - if (rc == 0) - return enabled; - return false; -} - -static void -cmd_free_args(char **args, int argcount) -{ - int i; - - for (i = 0; i < argcount; i++) - D_FREE(args[i]); - - D_FREE(args); -} - -static char ** -cmd_push_arg(char *args[], int *argcount, const char *fmt, ...) -{ - char **tmp = NULL; - char *arg = NULL; - va_list ap; - int rc; - - va_start(ap, fmt); - rc = vasprintf(&arg, fmt, ap); - va_end(ap); - if (arg == NULL || rc < 0) { - D_ERROR("failed to create arg\n"); - cmd_free_args(args, *argcount); - return NULL; - } - - D_REALLOC_ARRAY(tmp, args, *argcount, *argcount + 1); - if (tmp == NULL) { - D_ERROR("realloc failed\n"); - D_FREE(arg); - cmd_free_args(args, *argcount); - return NULL; - } - - tmp[*argcount] = arg; - (*argcount)++; - - return tmp; -} - -static char * -cmd_string(const char *cmd_base, char *args[], int argcount) -{ - char *tmp = NULL; - char *cmd_str = NULL; - size_t size, old; - int i; - void *addr; - - if (cmd_base == NULL) - return NULL; - - old = size = strnlen(cmd_base, ARG_MAX - 1) + 1; - D_STRNDUP(cmd_str, cmd_base, size); - if (cmd_str == NULL) - return NULL; - - for (i = 0; i < argcount; i++) { - size += strnlen(args[i], ARG_MAX - 1) + 1; - if (size >= ARG_MAX) { - D_ERROR("arg list too long\n"); - D_FREE(cmd_str); - return NULL; - } - - D_REALLOC(tmp, cmd_str, old, size); - if (tmp == NULL) { - D_FREE(cmd_str); - return NULL; - } - strncat(tmp, args[i], size); - cmd_str = tmp; - old = size; - } - - addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); - if (addr == MAP_FAILED) { - D_ERROR("mmap() failed : %s\n", strerror(errno)); - D_FREE(cmd_str); - return NULL; - } - memcpy(addr, cmd_str, size); - D_FREE(cmd_str); - - return (char *)addr; -} - -static void -log_stderr_pipe(int fd) -{ - char buf[512]; - char *full_msg = NULL; - ssize_t len = 0; - - D_DEBUG(DB_TEST, "reading from stderr pipe\n"); - while (1) { - ssize_t n; - ssize_t old_len = len; - char *tmp; - - n = read(fd, buf, sizeof(buf)); - if (n == 0) - break; - if (n < 0) { - D_ERROR("read from stderr pipe failed: %s\n", strerror(errno)); - break; - } - - len = len + n; - D_REALLOC(tmp, full_msg, old_len, len); - if (tmp == NULL) { - D_ERROR("reading from stderr pipe: can't realloc tmp with size %ld\n", - len); - break; - } - - full_msg = tmp; - strncpy(&full_msg[old_len], buf, n); - } - - - D_DEBUG(DB_TEST, "done reading stderr pipe\n"); - close(fd); - - if (full_msg == NULL) { - D_INFO("no stderr output\n"); - return; - } - - D_DEBUG(DB_TEST, "stderr: %s\n", full_msg); - D_FREE(full_msg); -} - -static int -run_cmd(const char *command, int *outputfd) -{ - int rc = 0; - int child_rc = 0; - int child_pid; - int stdoutfd[2]; - int stderrfd[2]; - bool log_stderr; - - D_DEBUG(DB_TEST, "dmg cmd: %s\n", command); - - log_stderr = is_stderr_logging_enabled(); - if (log_stderr) - D_DEBUG(DB_TEST, "dmg stderr output will be logged\n"); - - /* Create pipes */ - if (pipe(stdoutfd) == -1) { - rc = daos_errno2der(errno); - D_ERROR("failed to create stdout pipe: %s\n", strerror(errno)); - return rc; - } - - if (pipe(stderrfd) == -1) { - rc = daos_errno2der(errno); - D_ERROR("failed to create stderr pipe: %s\n", strerror(errno)); - close(stdoutfd[0]); - close(stdoutfd[1]); - return rc; - } - - D_DEBUG(DB_TEST, "forking to run dmg command\n"); - - child_pid = fork(); - if (child_pid == -1) { - rc = daos_errno2der(errno); - D_ERROR("failed to fork: %s\n", strerror(errno)); - return rc; - } else if (child_pid == 0) { - /* child doesn't need the read end of the pipes */ - close(stdoutfd[0]); - close(stderrfd[0]); - - if (dup2(stdoutfd[1], STDOUT_FILENO) == -1) - _exit(errno); - - if (dup2(stderrfd[1], STDERR_FILENO) == -1) - _exit(errno); - - close(stdoutfd[1]); - close(stderrfd[1]); - - rc = system(command); - if (rc == -1) - _exit(errno); - _exit(rc); - } - - /* parent doesn't need the write end of the pipes */ - close(stdoutfd[1]); - close(stderrfd[1]); - - D_DEBUG(DB_TEST, "waiting for dmg to finish executing\n"); - if (wait(&child_rc) == -1) { - D_ERROR("wait failed: %s\n", strerror(errno)); - return daos_errno2der(errno); - } - D_DEBUG(DB_TEST, "dmg command finished\n"); - - if (child_rc != 0) { - D_ERROR("child process failed, rc=%d (%s)\n", child_rc, strerror(child_rc)); - close(stdoutfd[0]); - if (log_stderr) - log_stderr_pipe(stderrfd[0]); /* closes the pipe after reading */ - else - close(stderrfd[0]); - return daos_errno2der(child_rc); - } - - close(stderrfd[0]); - *outputfd = stdoutfd[0]; - return 0; -} - -#ifndef HAVE_JSON_TOKENER_GET_PARSE_END -#define json_tokener_get_parse_end(tok) ((tok)->char_offset) -#endif - -#define JSON_CHUNK_SIZE 4096 -#define JSON_MAX_INPUT (1 << 20) /* 1MB is plenty */ - -/* JSON output handling for dmg command */ -static int -daos_dmg_json_pipe(const char *dmg_cmd, const char *dmg_config_file, - char *args[], int argcount, - struct json_object **json_out) +/* + * Log a non-zero rc from a daos_control_* call to the cart log so it shows + * up alongside server-side errors. The Go library also logs the underlying + * error string to stderr (see libdaos_control's logger setup), but logging + * here keeps the failure grep-able in the cart log too. Returns rc so the + * helper can be wrapped inline around the daos_control_* call. + */ +static inline int +dmg_log_rc(int rc, const char *call_name) { - char *cmd_str = NULL; - char *cmd_base = NULL; - struct json_object *obj = NULL; - int parse_depth = JSON_TOKENER_DEFAULT_DEPTH; - json_tokener *tok = NULL; - FILE *fp = NULL; - int stdoutfd = 0; - int rc = 0; - const char *debug_flags = "-d --log-file=/tmp/suite_dmg.log"; - - if (dmg_config_file == NULL) - D_ASPRINTF(cmd_base, "dmg -j -i %s %s ", debug_flags, dmg_cmd); - else - D_ASPRINTF(cmd_base, "dmg -j %s -o %s %s ", debug_flags, - dmg_config_file, dmg_cmd); - if (cmd_base == NULL) - return -DER_NOMEM; - cmd_str = cmd_string(cmd_base, args, argcount); - D_FREE(cmd_base); - if (cmd_str == NULL) - return -DER_NOMEM; - - rc = run_cmd(cmd_str, &stdoutfd); if (rc != 0) - goto out; - - /* If the caller doesn't care about output, don't bother parsing it. */ - if (json_out == NULL) - goto out_close; - - fp = fdopen(stdoutfd, "r"); - if (fp == NULL) { - D_ERROR("fdopen failed: %s\n", strerror(errno)); - D_GOTO(out_close, rc = daos_errno2der(errno)); - } - - char *jbuf = NULL, *temp; - size_t size = 0; - size_t total = 0; - size_t n; - - D_DEBUG(DB_TEST, "reading json from stdout\n"); - while (1) { - if (total + JSON_CHUNK_SIZE + 1 > size) { - size = total + JSON_CHUNK_SIZE + 1; - - if (size >= JSON_MAX_INPUT) { - D_ERROR("JSON input too large (size=%lu)\n", size); - D_GOTO(out_jbuf, rc = -DER_REC2BIG); - } - - D_REALLOC(temp, jbuf, total, size); - if (temp == NULL) - D_GOTO(out_jbuf, rc = -DER_NOMEM); - jbuf = temp; - } - - n = fread(jbuf + total, 1, JSON_CHUNK_SIZE, fp); - if (n == 0) - break; - - total += n; - } - D_DEBUG(DB_TEST, "read %lu bytes\n", total); - - if (total == 0) { - D_ERROR("dmg output is empty\n"); - D_GOTO(out_jbuf, rc = -DER_INVAL); - } - - D_REALLOC(temp, jbuf, total, total + 1); - if (temp == NULL) - D_GOTO(out_jbuf, rc = -DER_NOMEM); - jbuf = temp; - jbuf[total] = '\0'; - - tok = json_tokener_new_ex(parse_depth); - if (tok == NULL) - D_GOTO(out_jbuf, rc = -DER_NOMEM); - - obj = json_tokener_parse_ex(tok, jbuf, total); - if (obj == NULL) { - enum json_tokener_error jerr = json_tokener_get_error(tok); - int fail_off = json_tokener_get_parse_end(tok); - char *aterr = &jbuf[fail_off]; - - D_ERROR("failed to parse JSON at offset %d: %s (failed character: %c)\n", - fail_off, json_tokener_error_desc(jerr), aterr[0]); - D_GOTO(out_tokener, rc = -DER_INVAL); - } - -out_tokener: - json_tokener_free(tok); -out_jbuf: - D_FREE(jbuf); - - if (fclose(fp) == -1) { - D_ERROR("failed to close fp: %s\n", strerror(errno)); - if (rc == 0) - rc = daos_errno2der(errno); - } -out_close: - close(stdoutfd); -out: - if (munmap(cmd_str, strlen(cmd_str) + 1) == -1) - D_ERROR("munmap() failed : %s\n", strerror(errno)); - - if (obj != NULL) { - struct json_object *tmp; - int flags = JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_SPACED; - - D_DEBUG(DB_TEST, "parsed output:\n%s\n", - json_object_to_json_string_ext(obj, flags)); - - json_object_object_get_ex(obj, "error", &tmp); - - if (tmp && !json_object_is_type(tmp, json_type_null)) { - const char *err_str; - - err_str = json_object_get_string(tmp); - D_ERROR("dmg error: %s\n", err_str); - *json_out = json_object_get(tmp); - - if (json_object_object_get_ex(obj, "status", &tmp)) - rc = json_object_get_int(tmp); - } else { - if (json_object_object_get_ex(obj, "response", &tmp)) - *json_out = json_object_get(tmp); - } - - json_object_put(obj); - } - + D_ERROR("%s failed, " DF_RC "\n", call_name, DP_RC(rc)); return rc; } +/* Initialize a per-call control context. */ static int -parse_pool_info(struct json_object *json_pool, daos_mgmt_pool_info_t *pool_info) -{ - struct json_object *tmp, *rank; - int n_svcranks; - const char *uuid_str; - int i, rc; - - if (json_pool == NULL || pool_info == NULL) - return -DER_INVAL; - - if (!json_object_object_get_ex(json_pool, "uuid", &tmp)) { - D_ERROR("unable to extract pool UUID from JSON\n"); - return -DER_INVAL; - } - uuid_str = json_object_get_string(tmp); - if (uuid_str == NULL) { - D_ERROR("unable to extract UUID string from JSON\n"); - return -DER_INVAL; - } - rc = uuid_parse(uuid_str, pool_info->mgpi_uuid); - if (rc != 0) { - D_ERROR("failed parsing uuid_str\n"); - return -DER_INVAL; - } - - if (!json_object_object_get_ex(json_pool, "svc_ldr", &tmp)) { - D_ERROR("unable to extract pool leader from JSON\n"); - return -DER_INVAL; - } - pool_info->mgpi_ldr = json_object_get_int(tmp); - - if (!json_object_object_get_ex(json_pool, "svc_reps", &tmp)) { - D_ERROR("unable to parse pool svcreps from JSON\n"); - return -DER_INVAL; - } - - n_svcranks = json_object_array_length(tmp); - if (n_svcranks <= 0) { - D_ERROR("unexpected svc_reps length: %d\n", n_svcranks); - return -DER_INVAL; - } - if (pool_info->mgpi_svc == NULL) { - pool_info->mgpi_svc = d_rank_list_alloc(n_svcranks); - if (pool_info->mgpi_svc == NULL) { - D_ERROR("failed to allocate rank list\n"); - return -DER_NOMEM; - } - } - - for (i = 0; i < n_svcranks; i++) { - rank = json_object_array_get_idx(tmp, i); - pool_info->mgpi_svc->rl_ranks[i] = - json_object_get_int(rank); - } - - return 0; -} - -static char * -rank_list_to_string(const d_rank_list_t *rank_list) -{ - char *ranks_str = NULL; - int width; - int i; - int idx = 0; - - if (rank_list == NULL) - return NULL; - - width = 0; - for (i = 0; i < rank_list->rl_nr; i++) - width += snprintf(NULL, 0, "%d,", rank_list->rl_ranks[i]); - width++; - D_ALLOC(ranks_str, width); - if (ranks_str == NULL) - return NULL; - for (i = 0; i < rank_list->rl_nr; i++) - idx += sprintf(&ranks_str[idx], "%d,", rank_list->rl_ranks[i]); - ranks_str[width - 1] = '\0'; - ranks_str[width - 2] = '\0'; - - return ranks_str; -} - -static int -print_acl_entry(FILE *outstream, struct daos_prop_entry *acl_entry) -{ - struct daos_acl *acl = NULL; - char **acl_str = NULL; - size_t nr_acl_str = 0; - size_t i; - int rc = 0; - - if (outstream == NULL || acl_entry == NULL) - return -DER_INVAL; - - /* - * Validate the ACL before we start printing anything out. - */ - if (acl_entry->dpe_val_ptr != NULL) { - acl = acl_entry->dpe_val_ptr; - rc = daos_acl_to_strs(acl, &acl_str, &nr_acl_str); - if (rc != 0) { - D_ERROR("invalid ACL\n"); - goto out; - } - } - - for (i = 0; i < nr_acl_str; i++) - fprintf(outstream, "%s\n", acl_str[i]); - - for (i = 0; i < nr_acl_str; i++) - D_FREE(acl_str[i]); - - D_FREE(acl_str); +dmg_ctx_init(const char *cfg, uintptr_t *ctx) +{ + const char *log_file = getenv("DAOS_TEST_CONTROL_LOG_FILE"); + const char *log_level = getenv("DAOS_TEST_CONTROL_LOG_LEVEL"); + struct daos_control_init_args args = { + .dcia_config_file = cfg, + .dcia_log_file = log_file ? log_file : "/tmp/libdaos_control.log", + .dcia_log_level = log_level ? log_level : "debug", + }; + int rc; + + rc = daos_control_init(&args, ctx); + if (rc != 0) + *ctx = 0; -out: - return rc; + return dmg_log_rc(rc, "daos_control_init"); } -static int -parse_dmg_string(struct json_object *obj, const char *key, char **tgt) +/* Finalize a per-call control context. */ +static void +dmg_ctx_fini(uintptr_t ctx) { - struct json_object *tmp; - const char *str; - - if (!json_object_object_get_ex(obj, key, &tmp)) { - D_ERROR("Unable to extract %s from check query result\n", key); - return -DER_INVAL; - } - - str = json_object_get_string(tmp); - if (str == NULL) { - D_ERROR("Got empty %s from check query result\n", key); - return -DER_INVAL; - } - - D_STRNDUP(*tgt, str, strlen(str)); - if (*tgt == NULL) { - D_ERROR("Failed to dup %s from check query result\n", key); - return -DER_NOMEM; - } - - return 0; + if (ctx != 0) + daos_control_fini(ctx); } -static int -parse_dmg_uuid(struct json_object *obj, const char *key, uuid_t uuid) +int +dmg_pool_set_prop(const char *dmg_config_file, const char *prop_name, const char *prop_value, + const uuid_t pool_uuid) { - struct json_object *tmp; - const char *str; - int rc; - - if (!json_object_object_get_ex(obj, key, &tmp)) { - D_ERROR("Unable to extract %s from check query result\n", key); - return -DER_INVAL; - } + uintptr_t ctx = 0; + int rc; - str = json_object_get_string(tmp); - if (str == NULL) { - D_ERROR("Got empty %s from check query result\n", key); - return -DER_INVAL; - } - - rc = uuid_parse(str, uuid); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) - D_ERROR("Failed to parse uuid %s from check query result\n", str); + return rc; - return rc; -} + rc = dmg_log_rc(daos_control_pool_set_prop(ctx, NULL, (uuid_t *)pool_uuid, + (char *)prop_name, (char *)prop_value), + "daos_control_pool_set_prop"); -int -dmg_pool_set_prop(const char *dmg_config_file, - const char *prop_name, const char *prop_value, - const uuid_t pool_uuid) -{ - char uuid_str[DAOS_UUID_STR_SIZE]; - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - uuid_unparse_lower(pool_uuid, uuid_str); - args = cmd_push_arg(args, &argcount, "%s ", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - args = cmd_push_arg(args, &argcount, "%s:%s", - prop_name, prop_value); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - rc = daos_dmg_json_pipe("pool set-prop", dmg_config_file, - args, argcount, &dmg_out); - if (rc != 0) { - D_ERROR("dmg failed\n"); - goto out_json; - } - -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: + dmg_ctx_fini(ctx); return rc; } int -dmg_pool_get_prop(const char *dmg_config_file, const char *label, - const uuid_t uuid, const char *name, char **value) +dmg_pool_get_prop(const char *dmg_config_file, const char *label, const uuid_t uuid, + const char *name, char **value) { - char uuid_str[DAOS_UUID_STR_SIZE]; - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int len; - int rc = 0; + uintptr_t ctx = 0; + int rc; D_ASSERT(name != NULL); D_ASSERT(value != NULL); - if (label != NULL) { - args = cmd_push_arg(args, &argcount, "%s %s", label, name); - } else { - uuid_unparse_lower(uuid, uuid_str); - args = cmd_push_arg(args, &argcount, "%s %s", uuid_str, name); - } - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - rc = daos_dmg_json_pipe("pool get-prop", dmg_config_file, args, argcount, &dmg_out); - if (rc != 0) { - D_ERROR("pool get-prop for %s failed: %d\n", label != NULL ? label : uuid_str, rc); - goto out_json; - } - - D_ASSERT(dmg_out != NULL); - - if (json_object_is_type(dmg_out, json_type_null)) { - D_ERROR("Cannot find the property %s for %s\n", - name, label != NULL ? label : uuid_str); - D_GOTO(out_json, rc = -DER_ENOENT); - } - - len = json_object_array_length(dmg_out); - D_ASSERTF(len >= 1, "Invalid prop entries count: %d\n", len); - - rc = parse_dmg_string(json_object_array_get_idx(dmg_out, 0), "value", value); - -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; - cmd_free_args(args, argcount); + rc = dmg_log_rc( + daos_control_pool_get_prop(ctx, (char *)label, (uuid_t *)uuid, (char *)name, value), + "daos_control_pool_get_prop"); -out: + dmg_ctx_fini(ctx); return rc; } int -dmg_pool_create(const char *dmg_config_file, - uid_t uid, gid_t gid, const char *grp, - const d_rank_list_t *tgts, - daos_size_t scm_size, daos_size_t nvme_size, - daos_prop_t *prop, - d_rank_list_t *svc, uuid_t uuid) -{ - int argcount = 0; - char **args = NULL; - struct passwd *passwd = NULL; - struct group *group = NULL; - struct daos_prop_entry *entry; - char tmp_name[] = "/tmp/acl_XXXXXX"; - FILE *tmp_file = NULL; - daos_mgmt_pool_info_t pool_info = {}; - struct json_object *dmg_out = NULL; - bool has_label = false; - int fd = -1, rc = 0; - - if (grp != NULL) { - args = cmd_push_arg(args, &argcount, - "--sys=%s ", grp); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (tgts != NULL) { - char *ranks_str = rank_list_to_string(tgts); +dmg_pool_create(const char *dmg_config_file, uid_t uid, gid_t gid, const char *grp, + const d_rank_list_t *tgts, daos_size_t scm_size, daos_size_t nvme_size, + daos_prop_t *prop, d_rank_list_t *svc, uuid_t uuid) +{ + daos_prop_t *new_prop = NULL; + char label[DAOS_PROP_LABEL_MAX_LEN + 1] = {0}; + bool need_label = true; + bool need_rd_fac = true; + bool need_space_rb = true; + uint32_t orig_nr = 0; + uint32_t extra = 0; + uint32_t idx; + uintptr_t ctx = 0; + d_rank_list_t *svc_out = NULL; + int rc; + + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; - if (ranks_str == NULL) { - D_ERROR("failed to create rank string\n"); - D_GOTO(out_cmd, rc = -DER_NOMEM); - } - args = cmd_push_arg(args, &argcount, - "--ranks=%s ", ranks_str); - D_FREE(ranks_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); + /* + * Preserve pre-DAOS-17946 pool property defaults for the daos_test suite. + * Post-17946 defaults (RF3, non-zero space_rb) break tests that create + * containers with OC_S1 and assume no rebuild reserve. If the caller + * hasn't set rd_fac or space_rb, force them to 0 here. + */ + if (prop != NULL && prop->dpp_nr > 0) { + orig_nr = prop->dpp_nr; + if (daos_prop_entry_get(prop, DAOS_PROP_PO_LABEL) != NULL) + need_label = false; + if (daos_prop_entry_get(prop, DAOS_PROP_PO_REDUN_FAC) != NULL) + need_rd_fac = false; + if (daos_prop_entry_get(prop, DAOS_PROP_PO_SPACE_RB) != NULL) + need_space_rb = false; } - passwd = getpwuid(uid); - if (passwd == NULL) { - D_ERROR("unable to resolve %d to passwd entry\n", uid); - D_GOTO(out_cmd, rc = -DER_INVAL); - } + if (need_label) { + char path[] = "/tmp/test_XXXXXX"; + int tmp_fd; - args = cmd_push_arg(args, &argcount, - "--user=%s ", passwd->pw_name); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); + /* pool label is required, generate a unique one randomly */ + tmp_fd = mkstemp(path); + if (tmp_fd < 0) { + D_ERROR("failed to generate unique label: %s\n", strerror(errno)); + D_GOTO(out, rc = d_errno2der(errno)); + } + close(tmp_fd); + unlink(path); - group = getgrgid(gid); - if (group == NULL) { - D_ERROR("unable to resolve %d to group name\n", gid); - D_GOTO(out_cmd, rc = -DER_INVAL); + /* Copy label portion (after /tmp/) to properly sized buffer */ + strncpy(label, &path[5], sizeof(label) - 1); + label[sizeof(label) - 1] = '\0'; + extra++; } + if (need_rd_fac) + extra++; + if (need_space_rb) + extra++; - args = cmd_push_arg(args, &argcount, - "--group=%s ", group->gr_name); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - args = cmd_push_arg(args, &argcount, - "--scm-size=%"PRIu64"b ", scm_size); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - if (nvme_size > 0) { - args = cmd_push_arg(args, &argcount, - "--nvme-size=%"PRIu64"b ", nvme_size); - if (args == NULL) + if (extra > 0) { + new_prop = daos_prop_alloc(orig_nr + extra); + if (new_prop == NULL) D_GOTO(out, rc = -DER_NOMEM); - } - - if (prop != NULL) { - entry = daos_prop_entry_get(prop, DAOS_PROP_PO_ACL); - if (entry != NULL) { - fd = mkstemp(tmp_name); - if (fd < 0) { - D_ERROR("failed to create tmpfile file\n"); - D_GOTO(out_cmd, rc = -DER_NOMEM); - } - tmp_file = fdopen(fd, "w"); - if (tmp_file == NULL) { - D_ERROR("failed to associate stream: %s\n", - strerror(errno)); - close(fd); - D_GOTO(out_cmd, rc = -DER_MISC); - } - - rc = print_acl_entry(tmp_file, entry); - fclose(tmp_file); + if (orig_nr > 0) { + rc = daos_prop_copy(new_prop, prop); if (rc != 0) { - D_ERROR("failed to write ACL to tmpfile\n"); - goto out_cmd; + daos_prop_free(new_prop); + new_prop = NULL; + D_GOTO(out, rc); } - args = cmd_push_arg(args, &argcount, - "--acl-file=%s ", tmp_name); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); } - entry = daos_prop_entry_get(prop, DAOS_PROP_PO_LABEL); - if (entry != NULL) { - args = cmd_push_arg(args, &argcount, "%s ", - entry->dpe_str); - if (args == NULL) + idx = orig_nr; + if (need_label) { + new_prop->dpp_entries[idx].dpe_type = DAOS_PROP_PO_LABEL; + D_STRNDUP(new_prop->dpp_entries[idx].dpe_str, label, + DAOS_PROP_LABEL_MAX_LEN); + if (new_prop->dpp_entries[idx].dpe_str == NULL) { + daos_prop_free(new_prop); + new_prop = NULL; D_GOTO(out, rc = -DER_NOMEM); - has_label = true; - } - - entry = daos_prop_entry_get(prop, DAOS_PROP_PO_SCRUB_MODE); - if (entry != NULL) { - const char *scrub_str = NULL; - - switch (entry->dpe_val) { - case DAOS_SCRUB_MODE_OFF: - scrub_str = "off"; - break; - case DAOS_SCRUB_MODE_LAZY: - scrub_str = "lazy"; - break; - case DAOS_SCRUB_MODE_TIMED: - scrub_str = "timed"; - break; - default: - break; } - - if (scrub_str) { - args = cmd_push_arg(args, &argcount, "--properties=scrub:%s ", - scrub_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); + idx++; + } + if (need_rd_fac) { + new_prop->dpp_entries[idx].dpe_type = DAOS_PROP_PO_REDUN_FAC; + new_prop->dpp_entries[idx].dpe_val = 0; + idx++; + } + if (need_space_rb) { + new_prop->dpp_entries[idx].dpe_type = DAOS_PROP_PO_SPACE_RB; + new_prop->dpp_entries[idx].dpe_val = 0; + idx++; + } + prop = new_prop; + } + + { + /* + * NB: The original helper was written with a caller-supplied svc list, + * but the new pool create wrapper was written to handle the allocations, as it's + * a better API. New tests should use the wrapper API directly. + */ + struct daos_control_pool_create_args args = { + .dcpa_uid = uid, + .dcpa_gid = gid, + .dcpa_grp = grp, + .dcpa_tgts = (d_rank_list_t *)tgts, + .dcpa_scm_size = scm_size, + .dcpa_nvme_size = nvme_size, + .dcpa_prop = prop, + .dcpa_nsvc = (svc != NULL) ? svc->rl_nr : 0, + }; + + rc = dmg_log_rc(daos_control_pool_create( + ctx, &args, (svc != NULL) ? &svc_out : NULL, (uuid_t *)uuid), + "daos_control_pool_create"); + if (rc == 0 && svc != NULL) { + if (svc_out == NULL) { + /* A successful create must report its service replicas. */ + D_ERROR("pool create returned no svc replicas\n"); + rc = -DER_INVAL; + } else { + uint32_t cap = svc->rl_nr; + uint32_t n = svc_out->rl_nr; + uint32_t i; + + if (n > cap) + n = cap; + for (i = 0; i < n; i++) + svc->rl_ranks[i] = svc_out->rl_ranks[i]; + svc->rl_nr = n; } } - - entry = daos_prop_entry_get(prop, DAOS_PROP_PO_SVC_OPS_ENABLED); - if (entry != NULL) { - args = cmd_push_arg(args, &argcount, "--properties=svc_ops_enabled:%zu ", - entry->dpe_val); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - entry = daos_prop_entry_get(prop, DAOS_PROP_PO_SVC_OPS_ENTRY_AGE); - if (entry != NULL) { - args = cmd_push_arg(args, &argcount, "--properties=svc_ops_entry_age:%zu ", - entry->dpe_val); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - entry = daos_prop_entry_get(prop, DAOS_PROP_PO_SPACE_RB); - if (entry != NULL) { - args = cmd_push_arg(args, &argcount, "--properties=space_rb:%zu ", - entry->dpe_val); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - entry = daos_prop_entry_get(prop, DAOS_PROP_PO_SVC_REDUN_FAC); - if (entry != NULL) { - args = cmd_push_arg(args, &argcount, "--properties=svc_rf:%zu ", - entry->dpe_val); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - } - - /* Temporarily use old pool property defaults due to DAOS-17946 */ - /* Set default rd_fac:0 if --properties=rd_fac is not already defined in args */ - bool has_rd_fac = false; - for (int i = 0; i < argcount; i++) { - if (args[i] && strstr(args[i], "--properties=rd_fac") != NULL) { - has_rd_fac = true; - break; - } - } - if (!has_rd_fac) { - args = cmd_push_arg(args, &argcount, "--properties=rd_fac:0 "); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - /* Set default space_rb:0 if --properties=space_rb is not already defined in args */ - bool has_space_rb = false; - for (int i = 0; i < argcount; i++) { - if (args[i] && strstr(args[i], "--properties=space_rb") != NULL) { - has_space_rb = true; - break; - } - } - if (!has_space_rb) { - args = cmd_push_arg(args, &argcount, "--properties=space_rb:0 "); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (!has_label) { - char path[] = "/tmp/test_XXXXXX"; - int tmp_fd; - char *label = &path[5]; - - /* pool label is required, generate a unique one randomly */ - tmp_fd = mkstemp(path); - if (tmp_fd < 0) { - D_ERROR("failed to generate unique label: %s\n", - strerror(errno)); - D_GOTO(out_cmd, rc = d_errno2der(errno)); - } - close(tmp_fd); - unlink(path); - - args = cmd_push_arg(args, &argcount, "%s ", label); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (svc != NULL) { - args = cmd_push_arg(args, &argcount, - "--nsvc=%d", svc->rl_nr); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - rc = daos_dmg_json_pipe("pool create", dmg_config_file, - args, argcount, &dmg_out); - if (rc != 0) { - D_ERROR("dmg failed\n"); - goto out_json; - } - - rc = parse_pool_info(dmg_out, &pool_info); - if (rc != 0) { - D_ERROR("failed to parse pool info\n"); - goto out_json; - } - - uuid_copy(uuid, pool_info.mgpi_uuid); - if (svc == NULL) - goto out_svc; - - if (pool_info.mgpi_svc->rl_nr == 0) { - D_ERROR("unexpected zero-length pool svc ranks list\n"); - rc = -DER_INVAL; - goto out_svc; - } - rc = d_rank_list_copy(svc, pool_info.mgpi_svc); - if (rc != 0) { - D_ERROR("failed to dup svc rank list\n"); - goto out_svc; + if (svc_out != NULL) + d_rank_list_free(svc_out); } -out_svc: - d_rank_list_free(pool_info.mgpi_svc); -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); -out_cmd: - cmd_free_args(args, argcount); out: - if (fd >= 0) - unlink(tmp_name); + if (new_prop != NULL) + daos_prop_free(new_prop); + dmg_ctx_fini(ctx); return rc; } int dmg_pool_destroy(const char *dmg_config_file, const uuid_t uuid, const char *grp, int force) { - char uuid_str[DAOS_UUID_STR_SIZE]; - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - uuid_unparse_lower(uuid, uuid_str); - args = cmd_push_arg(args, &argcount, "%s ", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - /* Always perform recursive destroy. */ - args = cmd_push_arg(args, &argcount, " --recursive "); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - if (force != 0) { - args = cmd_push_arg(args, &argcount, " --force "); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + uintptr_t ctx = 0; + int rc; - rc = daos_dmg_json_pipe("pool destroy", dmg_config_file, - args, argcount, &dmg_out); - if (rc != 0) { - D_ERROR("dmg failed\n"); - goto out_json; - } + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: + rc = dmg_log_rc(daos_control_pool_destroy(ctx, (uuid_t *)uuid, (char *)grp, force), + "daos_control_pool_destroy"); + + dmg_ctx_fini(ctx); return rc; } int dmg_pool_evict(const char *dmg_config_file, const uuid_t uuid, const char *grp) { - char uuid_str[DAOS_UUID_STR_SIZE]; - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - uuid_unparse_lower(uuid, uuid_str); - args = cmd_push_arg(args, &argcount, "%s ", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - rc = daos_dmg_json_pipe("pool evict", dmg_config_file, args, argcount, &dmg_out); - if (rc != 0) { - DL_ERROR(rc, "dmg failed"); - goto out_json; - } + uintptr_t ctx = 0; + int rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; + + rc = dmg_log_rc(daos_control_pool_evict(ctx, (uuid_t *)uuid, (char *)grp), + "daos_control_pool_evict"); + + dmg_ctx_fini(ctx); return rc; } @@ -1003,36 +292,17 @@ int dmg_pool_update_ace(const char *dmg_config_file, const uuid_t uuid, const char *grp, const char *ace) { - char uuid_str[DAOS_UUID_STR_SIZE]; - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - uuid_unparse_lower(uuid, uuid_str); - args = cmd_push_arg(args, &argcount, "%s ", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - args = cmd_push_arg(args, &argcount, "%s", "--entry="); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - args = cmd_push_arg(args, &argcount, "%s", ace); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - rc = daos_dmg_json_pipe("pool update-acl", dmg_config_file, args, argcount, &dmg_out); - if (rc != 0) { - DL_ERROR(rc, "dmg failed"); - goto out_json; - } + uintptr_t ctx = 0; + int rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; + + rc = dmg_log_rc(daos_control_pool_update_ace(ctx, (uuid_t *)uuid, (char *)grp, (char *)ace), + "daos_control_pool_update_ace"); + + dmg_ctx_fini(ctx); return rc; } @@ -1040,570 +310,215 @@ int dmg_pool_delete_ace(const char *dmg_config_file, const uuid_t uuid, const char *grp, const char *principal) { - char uuid_str[DAOS_UUID_STR_SIZE]; - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - uuid_unparse_lower(uuid, uuid_str); - args = cmd_push_arg(args, &argcount, "%s ", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - args = cmd_push_arg(args, &argcount, "%s", "--principal="); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - args = cmd_push_arg(args, &argcount, "%s", principal); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - rc = daos_dmg_json_pipe("pool delete-acl", dmg_config_file, args, argcount, &dmg_out); - if (rc != 0) { - DL_ERROR(rc, "dmg failed"); - goto out_json; - } - -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: - return rc; -} - -static int -dmg_pool_target(const char *cmd, const char *dmg_config_file, const uuid_t uuid, - const char *grp, d_rank_t rank, int tgt_idx) -{ - char uuid_str[DAOS_UUID_STR_SIZE]; - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - uuid_unparse_lower(uuid, uuid_str); - args = cmd_push_arg(args, &argcount, "%s ", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - if (grp != NULL) { - args = cmd_push_arg(args, &argcount, "--sys=%s ", grp); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + uintptr_t ctx = 0; + int rc; - if (tgt_idx >= 0) { - args = cmd_push_arg(args, &argcount, "--target-idx=%d ", tgt_idx); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; - // Exclude, drain and reintegrate take ranks option which can be either a rank-list range or - // a single rank identifier. - args = cmd_push_arg(args, &argcount, "--ranks=%d ", rank); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - rc = daos_dmg_json_pipe(cmd, dmg_config_file, - args, argcount, &dmg_out); - if (rc != 0) { - D_ERROR("dmg failed\n"); - goto out_json; - } + rc = dmg_log_rc( + daos_control_pool_delete_ace(ctx, (uuid_t *)uuid, (char *)grp, (char *)principal), + "daos_control_pool_delete_ace"); -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: + dmg_ctx_fini(ctx); return rc; } int -dmg_pool_exclude(const char *dmg_config_file, const uuid_t uuid, - const char *grp, d_rank_t rank, int tgt_idx) -{ - return dmg_pool_target("pool exclude", dmg_config_file, uuid, grp, rank, tgt_idx); -} - -int -dmg_pool_reintegrate(const char *dmg_config_file, const uuid_t uuid, - const char *grp, d_rank_t rank, int tgt_idx) -{ - return dmg_pool_target("pool reintegrate", dmg_config_file, uuid, grp, rank, tgt_idx); -} - -int -dmg_pool_drain(const char *dmg_config_file, const uuid_t uuid, - const char *grp, d_rank_t rank, int tgt_idx) +dmg_pool_exclude(const char *dmg_config_file, const uuid_t uuid, const char *grp, d_rank_t rank, + int tgt_idx) { - return dmg_pool_target("pool drain", dmg_config_file, uuid, grp, rank, tgt_idx); -} + uintptr_t ctx = 0; + int rc; -int -dmg_pool_extend(const char *dmg_config_file, const uuid_t uuid, - const char *grp, d_rank_t *ranks, int rank_nr) -{ - char uuid_str[DAOS_UUID_STR_SIZE]; - d_rank_list_t rank_list = { 0 }; - char *rank_str = NULL; - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - rank_list.rl_ranks = ranks; - rank_list.rl_nr = rank_nr; - - rc = d_rank_list_to_str(&rank_list, &rank_str); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) - D_GOTO(out, rc); - - uuid_unparse_lower(uuid, uuid_str); - args = cmd_push_arg(args, &argcount, "%s ", uuid_str); - if (args == NULL) - D_GOTO(out_rankstr, rc = -DER_NOMEM); - - if (grp != NULL) { - args = cmd_push_arg(args, &argcount, "--sys=%s ", grp); - if (args == NULL) - D_GOTO(out_rankstr, rc = -DER_NOMEM); - } + return rc; - args = cmd_push_arg(args, &argcount, "--ranks=%s ", rank_str); - if (args == NULL) - D_GOTO(out_rankstr, rc = -DER_NOMEM); + rc = dmg_log_rc(daos_control_pool_exclude(ctx, (uuid_t *)uuid, (char *)grp, rank, tgt_idx), + "daos_control_pool_exclude"); - rc = daos_dmg_json_pipe("pool extend", dmg_config_file, - args, argcount, &dmg_out); - if (rc != 0) { - D_ERROR("dmg failed\n"); - goto out_json; - } - -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out_rankstr: - D_FREE(rank_str); -out: + dmg_ctx_fini(ctx); return rc; } int -dmg_pool_list(const char *dmg_config_file, const char *group, - daos_size_t *npools, daos_mgmt_pool_info_t *pools) +dmg_pool_reintegrate(const char *dmg_config_file, const uuid_t uuid, const char *grp, d_rank_t rank, + int tgt_idx) { - daos_size_t npools_in; - struct json_object *dmg_out = NULL; - struct json_object *pool_list = NULL; - struct json_object *pool = NULL; - int rc = 0; - int i; + uintptr_t ctx = 0; + int rc; - if (npools == NULL) - return -DER_INVAL; - npools_in = *npools; - - rc = daos_dmg_json_pipe("pool list", dmg_config_file, - NULL, 0, &dmg_out); - if (rc != 0) { - D_ERROR("dmg failed\n"); - goto out_json; - } - - if (!json_object_object_get_ex(dmg_out, "pools", &pool_list) || pool_list == NULL) - *npools = 0; - else - *npools = json_object_array_length(pool_list); - - if (pools == NULL) - goto out_json; - else if (npools_in < *npools) - D_GOTO(out_json, rc = -DER_TRUNC); - - for (i = 0; i < *npools; i++) { - pool = json_object_array_get_idx(pool_list, i); - if (pool == NULL) - D_GOTO(out_json, rc = -DER_INVAL); - - rc = parse_pool_info(pool, &pools[i]); - if (rc != 0) - goto out_json; - } + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); + rc = dmg_log_rc( + daos_control_pool_reintegrate(ctx, (uuid_t *)uuid, (char *)grp, rank, tgt_idx), + "daos_control_pool_reintegrate"); + dmg_ctx_fini(ctx); return rc; } int -dmg_pool_rebuild_stop(const char *dmg_config_file, const uuid_t uuid, const char *grp, bool force) +dmg_pool_drain(const char *dmg_config_file, const uuid_t uuid, const char *grp, d_rank_t rank, + int tgt_idx) { - char uuid_str[DAOS_UUID_STR_SIZE]; - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - uuid_unparse_lower(uuid, uuid_str); - args = cmd_push_arg(args, &argcount, "%s ", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - if (grp != NULL) { - args = cmd_push_arg(args, &argcount, "--sys=%s ", grp); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + uintptr_t ctx = 0; + int rc; - if (force) { - args = cmd_push_arg(args, &argcount, "--force"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - rc = daos_dmg_json_pipe("pool rebuild stop", dmg_config_file, args, argcount, &dmg_out); - if (rc != 0) { - D_ERROR("dmg pool rebuild stop failed\n"); - goto out_json; - } - -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: - return rc; -} - -int -dmg_pool_rebuild_start(const char *dmg_config_file, const uuid_t uuid, const char *grp) -{ - char uuid_str[DAOS_UUID_STR_SIZE]; - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - uuid_unparse_lower(uuid, uuid_str); - args = cmd_push_arg(args, &argcount, "%s ", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - if (grp != NULL) { - args = cmd_push_arg(args, &argcount, "--sys=%s ", grp); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; - rc = daos_dmg_json_pipe("pool rebuild start", dmg_config_file, args, argcount, &dmg_out); - if (rc != 0) { - D_ERROR("dmg pool rebuild start failed\n"); - goto out_json; - } + rc = dmg_log_rc(daos_control_pool_drain(ctx, (uuid_t *)uuid, (char *)grp, rank, tgt_idx), + "daos_control_pool_drain"); -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: + dmg_ctx_fini(ctx); return rc; } -static int -parse_device_info(struct json_object *smd_dev, device_list *devices, - char *host, int dev_length, int *disks) +int +dmg_pool_extend(const char *dmg_config_file, const uuid_t uuid, const char *grp, d_rank_t *ranks, + int rank_nr) { - struct json_object *tmp; - struct json_object *dev = NULL; - struct json_object *ctrlr = NULL; - struct json_object *target = NULL; - struct json_object *targets; - int tgts_len; - int i, j; - int rc; - char *tmp_var; - char *saved_ptr; - - for (i = 0; i < dev_length; i++) { - dev = json_object_array_get_idx(smd_dev, i); - - tmp_var = strtok_r(host, ":", &saved_ptr); - if (tmp_var == NULL) { - D_ERROR("Hostname is empty\n"); - return -DER_INVAL; - } + uintptr_t ctx = 0; + int rc; - snprintf(devices[*disks].host, sizeof(devices[*disks].host), - "%s", tmp_var + 1); + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; - if (!json_object_object_get_ex(dev, "uuid", &tmp)) { - D_ERROR("unable to extract uuid from JSON\n"); - return -DER_INVAL; - } + rc = dmg_log_rc(daos_control_pool_extend(ctx, (uuid_t *)uuid, (char *)grp, ranks, rank_nr), + "daos_control_pool_extend"); - rc = uuid_parse(json_object_get_string(tmp), devices[*disks].device_id); - if (rc != 0) { - D_ERROR("failed parsing uuid_str\n"); - return -DER_INVAL; - } + dmg_ctx_fini(ctx); + return rc; +} - if (!json_object_object_get_ex(dev, "tgt_ids", - &targets)) { - D_ERROR("unable to extract tgtids from JSON\n"); - return -DER_INVAL; - } +int +dmg_pool_list(const char *dmg_config_file, const char *group, daos_size_t *npools, + daos_mgmt_pool_info_t *pools) +{ + uintptr_t ctx = 0; + int rc; - if (targets != NULL) - tgts_len = json_object_array_length(targets); - else - tgts_len = 0; + if (npools == NULL) + return -DER_INVAL; - for (j = 0; j < tgts_len; j++) { - target = json_object_array_get_idx(targets, j); - devices[*disks].tgtidx[j] = atoi( - json_object_to_json_string(target)); - } - devices[*disks].n_tgtidx = tgts_len; + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; - if (!json_object_object_get_ex(dev, "rank", &tmp)) { - D_ERROR("unable to extract rank from JSON\n"); - return -DER_INVAL; - } - devices[*disks].rank = atoi(json_object_to_json_string(tmp)); + rc = dmg_log_rc(daos_control_pool_list(ctx, (char *)group, npools, pools), + "daos_control_pool_list"); - if (!json_object_object_get_ex(dev, "ctrlr", &ctrlr)) { - D_ERROR("unable to extract ctrlr obj from JSON\n"); - return -DER_INVAL; - } + dmg_ctx_fini(ctx); + return rc; +} - if (!json_object_object_get_ex(ctrlr, "dev_state", &tmp)) { - D_ERROR("unable to extract state from JSON\n"); - return -DER_INVAL; - } +int +dmg_pool_rebuild_stop(const char *dmg_config_file, const uuid_t uuid, const char *grp, bool force) +{ + uintptr_t ctx = 0; + int rc; - snprintf(devices[*disks].state, sizeof(devices[*disks].state), "%s", - json_object_to_json_string(tmp)); - *disks = *disks + 1; - } + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; + + rc = dmg_log_rc( + daos_control_pool_rebuild_stop(ctx, (uuid_t *)uuid, (char *)grp, force ? 1 : 0), + "daos_control_pool_rebuild_stop"); - return 0; + dmg_ctx_fini(ctx); + return rc; } int -dmg_storage_device_list(const char *dmg_config_file, int *ndisks, - device_list *devices) +dmg_pool_rebuild_start(const char *dmg_config_file, const uuid_t uuid, const char *grp) { - struct json_object *dmg_out = NULL; - struct json_object *storage_map = NULL; - struct json_object *hosts = NULL; - struct json_object *smd_info = NULL; - struct json_object *smd_dev = NULL; - char *host; - int dev_length = 0; - int rc = 0; - int *disk; - - if (ndisks != NULL) - *ndisks = 0; - - D_ALLOC_PTR(disk); - rc = daos_dmg_json_pipe("storage query list-devices", dmg_config_file, - NULL, 0, &dmg_out); - if (rc != 0) { - D_FREE(disk); - D_ERROR("dmg failed\n"); - goto out_json; - } + uintptr_t ctx = 0; + int rc; - if (!json_object_object_get_ex(dmg_out, "host_storage_map", - &storage_map)) { - D_ERROR("unable to extract host_storage_map from JSON\n"); - D_GOTO(out, rc = -DER_INVAL); - } + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; - json_object_object_foreach(storage_map, key, val) { - D_DEBUG(DB_TEST, "key:\"%s\",val=%s\n", key, - json_object_to_json_string(val)); + rc = dmg_log_rc(daos_control_pool_rebuild_start(ctx, (uuid_t *)uuid, (char *)grp), + "daos_control_pool_rebuild_start"); - if (!json_object_object_get_ex(val, "hosts", &hosts)) { - D_ERROR("unable to extract hosts from JSON\n"); - D_GOTO(out, rc = -DER_INVAL); - } + dmg_ctx_fini(ctx); + return rc; +} - D_ALLOC(host, strlen(json_object_to_json_string(hosts)) + 1); - strcpy(host, json_object_to_json_string(hosts)); - - json_object_object_foreach(val, key1, val1) { - D_DEBUG(DB_TEST, "key1:\"%s\",val1=%s\n", key1, - json_object_to_json_string(val1)); - - if (json_object_object_get_ex(val1, "smd_info", &smd_info)) { - if (smd_info == NULL) - continue; - - if (!json_object_object_get_ex( - smd_info, "devices", &smd_dev)) { - D_ERROR("unable to extract devices\n"); - D_FREE(host); - D_GOTO(out, rc = -DER_INVAL); - } - - if (smd_dev != NULL) - dev_length = json_object_array_length( - smd_dev); - - if (ndisks != NULL) - *ndisks = *ndisks + dev_length; - - if (devices != NULL) { - rc = parse_device_info(smd_dev, devices, - host, dev_length, - disk); - if (rc != 0) { - D_FREE(host); - goto out_json; - } - } - } - } - D_FREE(host); - } +int +dmg_storage_device_list(const char *dmg_config_file, int *ndisks, device_list *devices) +{ + uintptr_t ctx = 0; + int rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; -out: - D_FREE(disk); + rc = dmg_log_rc(daos_control_storage_device_list(ctx, NULL, ndisks, devices), + "daos_control_storage_device_list"); + + dmg_ctx_fini(ctx); return rc; } int -dmg_storage_set_nvme_fault(const char *dmg_config_file, - char *host, const uuid_t uuid, int force) +dmg_storage_set_nvme_fault(const char *dmg_config_file, char *host, const uuid_t uuid) { - char uuid_str[DAOS_UUID_STR_SIZE]; - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - uuid_unparse_lower(uuid, uuid_str); - args = cmd_push_arg(args, &argcount, " --uuid=%s ", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - if (force != 0) { - args = cmd_push_arg(args, &argcount, " --force "); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + uintptr_t ctx = 0; + int rc; - args = cmd_push_arg(args, &argcount, " --host=%s ", host); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; - rc = daos_dmg_json_pipe("storage set nvme-faulty ", dmg_config_file, - args, argcount, &dmg_out); - if (rc != 0) { - D_ERROR("dmg command failed\n"); - goto out_json; - } + rc = dmg_log_rc(daos_control_storage_set_nvme_fault(ctx, host, (uuid_t *)uuid), + "daos_control_storage_set_nvme_fault"); -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: + dmg_ctx_fini(ctx); return rc; } int -dmg_storage_query_device_health(const char *dmg_config_file, char *host, - char *stats, const uuid_t uuid) +dmg_storage_query_device_health(const char *dmg_config_file, char *host, char *stats, + size_t stats_len, const uuid_t uuid) { - struct json_object *dmg_out = NULL; - struct json_object *storage_map = NULL; - struct json_object *smd_info = NULL; - struct json_object *storage_info = NULL; - struct json_object *health_stats = NULL; - struct json_object *devices = NULL; - struct json_object *dev_info = NULL; - struct json_object *ctrlr_info = NULL; - struct json_object *tmp = NULL; - char uuid_str[DAOS_UUID_STR_SIZE]; - int argcount = 0; - char **args = NULL; - int rc = 0; - - uuid_unparse_lower(uuid, uuid_str); - args = cmd_push_arg(args, &argcount, " --uuid=%s ", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - args = cmd_push_arg(args, &argcount, " --host-list=%s ", host); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - rc = daos_dmg_json_pipe("storage query list-devices --health ", dmg_config_file, args, - argcount, &dmg_out); - if (rc != 0) { - D_ERROR("dmg command failed\n"); - goto out_json; - } - if (!json_object_object_get_ex(dmg_out, "host_storage_map", - &storage_map)) { - D_ERROR("unable to extract host_storage_map from JSON\n"); - D_GOTO(out_json, rc = -DER_INVAL); - } + char key[stats_len]; + uintptr_t ctx = 0; + int rc; - json_object_object_foreach(storage_map, key, val) { - D_DEBUG(DB_TEST, "key:\"%s\",val=%s\n", key, - json_object_to_json_string(val)); + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; - if (!json_object_object_get_ex(val, "storage", &storage_info)) { - D_ERROR("unable to extract storage info from JSON\n"); - D_GOTO(out_json, rc = -DER_INVAL); - } - if (!json_object_object_get_ex(storage_info, "smd_info", - &smd_info)) { - D_ERROR("unable to extract smd_info from JSON\n"); - D_GOTO(out_json, rc = -DER_INVAL); - } - if (!json_object_object_get_ex(smd_info, "devices", &devices)) { - D_ERROR("unable to extract devices list from JSON\n"); - D_GOTO(out_json, rc = -DER_INVAL); - } + /* + * The Go export requires that statsKey and statsOut are distinct + * buffers. Copy the key name into a local scratch buffer so the + * output can be written back into the caller's stats buffer without + * aliasing. + */ + strncpy(key, stats, stats_len - 1); + key[stats_len - 1] = '\0'; - dev_info = json_object_array_get_idx(devices, 0); - if (!json_object_object_get_ex(dev_info, "ctrlr", &ctrlr_info)) { - D_ERROR("unable to extract ctrlr details from JSON\n"); - D_GOTO(out_json, rc = -DER_INVAL); - } - if (json_object_object_get_ex(ctrlr_info, "health_stats", &health_stats)) { - if (health_stats != NULL) { - if (json_object_object_get_ex(health_stats, stats, &tmp)) - strcpy(stats, json_object_to_json_string(tmp)); - } - } - } + rc = dmg_log_rc(daos_control_storage_query_device_health(ctx, host, key, stats, + (int)stats_len, (uuid_t *)uuid), + "daos_control_storage_query_device_health"); -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: + dmg_ctx_fini(ctx); return rc; } -int verify_blobstore_state(int state, const char *state_str) +int +verify_blobstore_state(int state, const char *state_str) { if (strcasecmp(state_str, "FAULTY") == 0) { if (state == BIO_BS_STATE_FAULTY) @@ -1633,624 +548,261 @@ int verify_blobstore_state(int state, const char *state_str) return 1; } -int dmg_system_stop_rank(const char *dmg_config_file, d_rank_t rank, int force) +int +dmg_system_stop_rank(const char *dmg_config_file, d_rank_t rank, int force) { - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - if (rank != CRT_NO_RANK) { - args = cmd_push_arg(args, &argcount, " -r %d ", rank); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + uintptr_t ctx = 0; + int rc; - if (force != 0) { - args = cmd_push_arg(args, &argcount, " --force "); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - rc = daos_dmg_json_pipe("system stop", dmg_config_file, - args, argcount, &dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) - D_ERROR("dmg failed\n"); + return rc; - if (dmg_out != NULL) - json_object_put(dmg_out); + rc = dmg_log_rc(daos_control_system_stop_rank(ctx, rank, force), + "daos_control_system_stop_rank"); - cmd_free_args(args, argcount); -out: + dmg_ctx_fini(ctx); return rc; } -int dmg_system_start_rank(const char *dmg_config_file, d_rank_t rank) +int +dmg_system_stop(const char *dmg_config_file, int force) { - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - if (rank != CRT_NO_RANK) { - args = cmd_push_arg(args, &argcount, " -r %d ", rank); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + uintptr_t ctx = 0; + int rc; - rc = daos_dmg_json_pipe("system start", dmg_config_file, - args, argcount, &dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) - D_ERROR("dmg failed\n"); + return rc; - if (dmg_out != NULL) - json_object_put(dmg_out); + rc = dmg_log_rc(daos_control_system_stop(ctx, force), "daos_control_system_stop"); - cmd_free_args(args, argcount); -out: + dmg_ctx_fini(ctx); return rc; } -int dmg_system_reint_rank(const char *dmg_config_file, d_rank_t rank) +int +dmg_system_start_rank(const char *dmg_config_file, d_rank_t rank) { - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - if (rank == CRT_NO_RANK) - D_GOTO(out, rc = -DER_INVAL); + uintptr_t ctx = 0; + int rc; - args = cmd_push_arg(args, &argcount, " -r %d ", rank); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - rc = daos_dmg_json_pipe("system clear-exclude", dmg_config_file, - args, argcount, &dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) - D_ERROR("dmg system clear-exclude failed\n"); - - if (dmg_out != NULL) - json_object_put(dmg_out); + return rc; - cmd_free_args(args, argcount); + rc = + dmg_log_rc(daos_control_system_start_rank(ctx, rank), "daos_control_system_start_rank"); -out: + dmg_ctx_fini(ctx); return rc; } -int dmg_system_exclude_rank(const char *dmg_config_file, d_rank_t rank) +int +dmg_system_start(const char *dmg_config_file) { - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - if (rank == CRT_NO_RANK) - D_GOTO(out, rc = -DER_INVAL); + uintptr_t ctx = 0; + int rc; - args = cmd_push_arg(args, &argcount, " -r %d ", rank); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - rc = daos_dmg_json_pipe("system exclude", dmg_config_file, - args, argcount, &dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) - D_ERROR("dmg system exclude failed\n"); - - if (dmg_out != NULL) - json_object_put(dmg_out); + return rc; - cmd_free_args(args, argcount); + rc = dmg_log_rc(daos_control_system_start(ctx), "daos_control_system_start"); -out: + dmg_ctx_fini(ctx); return rc; } int -dmg_server_set_logmasks(const char *dmg_config_file, const char *masks, const char *streams, - const char *subsystems) +dmg_system_reint_rank(const char *dmg_config_file, d_rank_t rank) { - int argcount = 0; - char **args = NULL; - struct json_object *dmg_out = NULL; - int rc = 0; - - /* engine log_mask */ - if (masks != NULL) { - args = cmd_push_arg(args, &argcount, " --masks=%s", masks); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - /* DD_MASK environment variable (aka streams) */ - if (streams != NULL) { - args = cmd_push_arg(args, &argcount, " --streams=%s", streams); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - /* DD_SUBSYS environment variable */ - if (subsystems != NULL) { - args = cmd_push_arg(args, &argcount, " --subsystems=%s", subsystems); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + uintptr_t ctx = 0; + int rc; - /* If none of masks, streams, subsystems are specified, restore original engine config */ - rc = daos_dmg_json_pipe("server set-logmasks", dmg_config_file, args, argcount, &dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) - D_ERROR("dmg failed\n"); + return rc; - if (dmg_out != NULL) - json_object_put(dmg_out); + rc = + dmg_log_rc(daos_control_system_reint_rank(ctx, rank), "daos_control_system_reint_rank"); - cmd_free_args(args, argcount); -out: + dmg_ctx_fini(ctx); return rc; } -const char * -daos_target_state_enum_to_str(int state) -{ - switch (state) { - case DAOS_TS_UNKNOWN: return "UNKNOWN"; - case DAOS_TS_DOWN_OUT: return "DOWNOUT"; - case DAOS_TS_DOWN: return "DOWN"; - case DAOS_TS_UP: return "UP"; - case DAOS_TS_UP_IN: return "UPIN"; - case DAOS_TS_DRAIN: return "DRAIN"; - } - - return "Undefined State"; -} - int -dmg_fault_inject(const char *dmg_config_file, uuid_t uuid, bool mgmt, const char *fault) +dmg_system_exclude_rank(const char *dmg_config_file, d_rank_t rank) { - char uuid_str[DAOS_UUID_STR_SIZE]; - char **args = NULL; - struct json_object *dmg_out = NULL; - int argcount = 0; - int rc = 0; - - if (mgmt) - args = cmd_push_arg(args, &argcount, " mgmt-svc pool"); - else - args = cmd_push_arg(args, &argcount, " pool-svc"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - uuid_unparse_lower(uuid, uuid_str); - args = cmd_push_arg(args, &argcount, " %s", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - args = cmd_push_arg(args, &argcount, " %s", fault); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - rc = daos_dmg_json_pipe("faults", dmg_config_file, args, argcount, &dmg_out); - if (rc != 0) - D_ERROR("dmg %s fault injection for " DF_UUID " with %s got failure: %d\n", - mgmt ? "mgmt" : "pool", DP_UUID(uuid), fault, rc); + uintptr_t ctx = 0; + int rc; - if (dmg_out != NULL) - json_object_put(dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; - cmd_free_args(args, argcount); + rc = dmg_log_rc(daos_control_system_exclude_rank(ctx, rank), + "daos_control_system_exclude_rank"); -out: + dmg_ctx_fini(ctx); return rc; } int -dmg_check_switch(const char *dmg_config_file, bool enable) +dmg_server_set_logmasks(const char *dmg_config_file, const char *masks, const char *streams, + const char *subsystems) { - char **args = NULL; - struct json_object *dmg_out = NULL; - int argcount = 0; - int rc = 0; - - if (enable) - args = cmd_push_arg(args, &argcount, " enable"); - else - args = cmd_push_arg(args, &argcount, " disable"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - rc = daos_dmg_json_pipe("check", dmg_config_file, args, argcount, &dmg_out); - if (rc != 0) - D_ERROR("dmg check switch to %s failed: %d\n", enable ? "enable" : "disable", rc); + uintptr_t ctx = 0; + int rc; - if (dmg_out != NULL) - json_object_put(dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); + if (rc != 0) + return rc; - cmd_free_args(args, argcount); + rc = dmg_log_rc(daos_control_server_set_logmasks(ctx, NULL, (char *)masks, (char *)streams, + (char *)subsystems), + "daos_control_server_set_logmasks"); -out: + dmg_ctx_fini(ctx); return rc; } -int -dmg_check_start(const char *dmg_config_file, uint32_t flags, uint32_t pool_nr, uuid_t uuids[], - const char *policies) +const char * +daos_target_state_enum_to_str(int state) { - char uuid_str[DAOS_UUID_STR_SIZE]; - char **args = NULL; - struct json_object *dmg_out = NULL; - int argcount = 0; - int rc = 0; - int i; - - if (flags & TCSF_DRYRUN) { - args = cmd_push_arg(args, &argcount, " -n"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (flags & TCSF_RESET) { - args = cmd_push_arg(args, &argcount, " -r"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (flags & TCSF_FAILOUT) { - args = cmd_push_arg(args, &argcount, " --failout=on"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (flags & TCSF_AUTO) { - args = cmd_push_arg(args, &argcount, " --auto=on"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (flags & TCSF_ORPHAN) { - args = cmd_push_arg(args, &argcount, " -O"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (flags & TCSF_NO_FAILOUT) { - args = cmd_push_arg(args, &argcount, " --failout=off"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (flags & TCSF_NO_AUTO) { - args = cmd_push_arg(args, &argcount, " --auto=off"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (policies != NULL) { - args = cmd_push_arg(args, &argcount, " --policies=%s", policies); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - for (i = 0; i < pool_nr; i++) { - uuid_unparse_lower(uuids[i], uuid_str); - args = cmd_push_arg(args, &argcount, " %s", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); + switch (state) { + case DAOS_TS_UNKNOWN: + return "UNKNOWN"; + case DAOS_TS_DOWN_OUT: + return "DOWNOUT"; + case DAOS_TS_DOWN: + return "DOWN"; + case DAOS_TS_UP: + return "UP"; + case DAOS_TS_UP_IN: + return "UPIN"; + case DAOS_TS_DRAIN: + return "DRAIN"; } - rc = daos_dmg_json_pipe("check start", dmg_config_file, args, argcount, &dmg_out); - if (rc != 0) - D_ERROR("dmg check start with flags %x, policies %s failed: %d\n", flags, - policies != NULL ? policies : "(null)", rc); - - if (dmg_out != NULL) - json_object_put(dmg_out); - -out: - cmd_free_args(args, argcount); - - return rc; + return "Undefined State"; } int -dmg_check_stop(const char *dmg_config_file, uint32_t pool_nr, uuid_t uuids[]) +dmg_fault_inject(const char *dmg_config_file, uuid_t uuid, bool mgmt, const char *fault) { - char uuid_str[DAOS_UUID_STR_SIZE]; - char **args = NULL; - struct json_object *dmg_out = NULL; - int argcount = 0; - int rc = 0; - int i; - - for (i = 0; i < pool_nr; i++) { - uuid_unparse_lower(uuids[i], uuid_str); - args = cmd_push_arg(args, &argcount, " %s", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + uintptr_t ctx = 0; + int rc; - rc = daos_dmg_json_pipe("check stop", dmg_config_file, args, argcount, &dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) - D_ERROR("dmg check stop failed: %d\n", rc); - - if (dmg_out != NULL) - json_object_put(dmg_out); + return rc; - cmd_free_args(args, argcount); + rc = dmg_log_rc(daos_control_fault_inject(ctx, (uuid_t *)uuid, mgmt ? 1 : 0, (char *)fault), + "daos_control_fault_inject"); -out: + dmg_ctx_fini(ctx); return rc; } -static int -parse_check_query_pool(struct json_object *obj, uuid_t uuid, struct daos_check_info *dci) +int +dmg_check_switch(const char *dmg_config_file, bool enable) { - struct daos_check_pool_info *dcpi; - struct json_object *pool; - char uuid_str[DAOS_UUID_STR_SIZE]; - int rc; - - uuid_unparse_lower(uuid, uuid_str); + uintptr_t ctx = 0; + int rc; - /* The queried pool may not exist. */ - if (!json_object_object_get_ex(obj, uuid_str, &pool)) { - D_WARN("Do not find the pool %s in check query result, may not exist\n", uuid_str); - return 0; - } - - dcpi = &dci->dci_pools[dci->dci_pool_nr]; - - rc = parse_dmg_uuid(pool, "uuid", dcpi->dcpi_uuid); - if (rc != 0) - return rc; - - rc = parse_dmg_string(pool, "status", &dcpi->dcpi_status); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) return rc; - rc = parse_dmg_string(pool, "phase", &dcpi->dcpi_phase); - if (rc == 0) - dci->dci_pool_nr++; + rc = + dmg_log_rc(daos_control_check_switch(ctx, enable ? 1 : 0), "daos_control_check_switch"); + dmg_ctx_fini(ctx); return rc; } -static int -parse_check_query_report(struct json_object *obj, struct daos_check_report_info *dcri) +int +dmg_check_start(const char *dmg_config_file, uint32_t flags, uint32_t pool_nr, uuid_t uuids[], + const char *policies) { - struct json_object *tmp; - int rc; - int i; + uintptr_t ctx = 0; + int rc; - rc = parse_dmg_uuid(obj, "pool_uuid", dcri->dcri_uuid); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) return rc; - if (!json_object_object_get_ex(obj, "seq", &tmp)) { - D_ERROR("Unable to extract seq for pool " DF_UUID " from check query result\n", - DP_UUID(dcri->dcri_uuid)); - return -DER_INVAL; - } - - dcri->dcri_seq = json_object_get_int64(tmp); - - if (!json_object_object_get_ex(obj, "class", &tmp)) { - D_ERROR("Unable to extract class for pool " DF_UUID " from check query result\n", - DP_UUID(dcri->dcri_uuid)); - return -DER_INVAL; - } - - dcri->dcri_class = json_object_get_int(tmp); - - if (!json_object_object_get_ex(obj, "action", &tmp)) { - D_ERROR("Unable to extract action for pool " DF_UUID " from check query result\n", - DP_UUID(dcri->dcri_uuid)); - return -DER_INVAL; - } - - dcri->dcri_act = json_object_get_int(tmp); - - if (!json_object_object_get_ex(obj, "rank", &tmp)) { - D_ERROR("Unable to extract rank for pool " DF_UUID " from check query result\n", - DP_UUID(dcri->dcri_uuid)); - return -DER_INVAL; - } - - dcri->dcri_rank = json_object_get_int(tmp); - - if (!json_object_object_get_ex(obj, "result", &tmp)) - dcri->dcri_result = 0; - else - dcri->dcri_result = json_object_get_int(tmp); - - /* Not interaction. */ - if (!json_object_object_get_ex(obj, "act_choices", &tmp)) - return 0; - - dcri->dcri_option_nr = json_object_array_length(tmp); - D_ASSERTF(dcri->dcri_option_nr > 0, - "Invalid options count for pool " DF_UUID " in check query result: %d\n", - DP_UUID(dcri->dcri_uuid), dcri->dcri_option_nr); + rc = dmg_log_rc(daos_control_check_start(ctx, flags, pool_nr, uuids, (char *)policies), + "daos_control_check_start"); - for (i = 0; i < dcri->dcri_option_nr; i++) - dcri->dcri_options[i] = json_object_get_int(json_object_array_get_idx(tmp, i)); - - return 0; + dmg_ctx_fini(ctx); + return rc; } -static int -parse_check_query_info(struct json_object *query_output, uint32_t pool_nr, uuid_t uuids[], - struct daos_check_info *dci) +int +dmg_check_stop(const char *dmg_config_file, uint32_t pool_nr, uuid_t uuids[]) { - struct json_object *obj; - int i; - int rc; - - rc = parse_dmg_string(query_output, "status", &dci->dci_status); - if (rc != 0) - return rc; + uintptr_t ctx = 0; + int rc; - rc = parse_dmg_string(query_output, "scan_phase", &dci->dci_phase); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) return rc; - if (!json_object_object_get_ex(query_output, "leader", &obj)) { - D_ERROR("Unable to extract leader from check query result\n"); - return -DER_INVAL; - } - - dci->dci_leader = json_object_get_int(obj); - dci->dci_pool_nr = 0; - - if (pool_nr <= 0) - goto reports; - - if (!json_object_object_get_ex(query_output, "pools", &obj)) { - D_ERROR("Unable to extract pools from check query result\n"); - return -DER_INVAL; - } - - if (json_object_is_type(obj, json_type_null)) - goto reports; - - D_ALLOC_ARRAY(dci->dci_pools, pool_nr); - if (dci->dci_pools == NULL) { - D_ERROR("Failed to allocate pools (len %d) for check query result\n", pool_nr); - return -DER_NOMEM; - } - - for (i = 0; i < pool_nr; i++) { - rc = parse_check_query_pool(obj, uuids[i], dci); - if (rc != 0) - return rc; - } - -reports: - if (!json_object_object_get_ex(query_output, "reports", &obj)) { - D_ERROR("Unable to extract reports from check query result\n"); - return -DER_INVAL; - } - - if (json_object_is_type(obj, json_type_null)) { - dci->dci_report_nr = 0; - return 0; - } - - dci->dci_report_nr = json_object_array_length(obj); - D_ASSERTF(dci->dci_report_nr > 0, - "Invalid reports count pool in check query result: %d\n", dci->dci_report_nr); - - D_ALLOC_ARRAY(dci->dci_reports, dci->dci_report_nr); - if (dci->dci_reports == NULL) { - D_ERROR("Failed to allocate reports (len %d) for check query result\n", - dci->dci_report_nr); - return -DER_NOMEM; - } - - for (i = 0; i < dci->dci_report_nr; i++) { - rc = parse_check_query_report(json_object_array_get_idx(obj, i), - &dci->dci_reports[i]); - if (rc != 0) - return rc; - } + rc = dmg_log_rc(daos_control_check_stop(ctx, pool_nr, uuids), "daos_control_check_stop"); - return 0; + dmg_ctx_fini(ctx); + return rc; } int dmg_check_query(const char *dmg_config_file, uint32_t pool_nr, uuid_t uuids[], struct daos_check_info *dci) { - char uuid_str[DAOS_UUID_STR_SIZE]; - char **args = NULL; - struct json_object *dmg_out = NULL; - int argcount = 0; - int rc = 0; - int i; - - for (i = 0; i < pool_nr; i++) { - uuid_unparse_lower(uuids[i], uuid_str); - args = cmd_push_arg(args, &argcount, " %s", uuid_str); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + uintptr_t ctx = 0; + int rc; - rc = daos_dmg_json_pipe("check query", dmg_config_file, args, argcount, &dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) - D_ERROR("dmg check query failed: %d\n", rc); - else - rc = parse_check_query_info(dmg_out, pool_nr, uuids, dci); - - if (dmg_out != NULL) - json_object_put(dmg_out); + return rc; - cmd_free_args(args, argcount); + rc = dmg_log_rc(daos_control_check_query(ctx, pool_nr, uuids, dci), + "daos_control_check_query"); -out: + dmg_ctx_fini(ctx); return rc; } int dmg_check_repair(const char *dmg_config_file, uint64_t seq, uint32_t opt) { - char **args = NULL; - struct json_object *dmg_out = NULL; - int argcount = 0; - int rc = 0; + uintptr_t ctx = 0; + int rc; - args = cmd_push_arg(args, &argcount, " %Lu %u", seq, opt); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - rc = daos_dmg_json_pipe("check repair", dmg_config_file, args, argcount, &dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) - D_ERROR("dmg check repair with seq %lu, opt %u, failed: %d\n", (unsigned long)seq, - opt, rc); - - if (dmg_out != NULL) - json_object_put(dmg_out); + return rc; - cmd_free_args(args, argcount); + rc = dmg_log_rc(daos_control_check_repair(ctx, seq, opt), "daos_control_check_repair"); -out: + dmg_ctx_fini(ctx); return rc; } + int dmg_check_set_policy(const char *dmg_config_file, uint32_t flags, const char *policies) { - char **args = NULL; - struct json_object *dmg_out = NULL; - int argcount = 0; - int rc = 0; - - if (flags & TCPF_RESET) { - args = cmd_push_arg(args, &argcount, " -d"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (flags & TCPF_INTERACT) { - args = cmd_push_arg(args, &argcount, " -a"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (policies != NULL) { - args = cmd_push_arg(args, &argcount, " %s", policies); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + uintptr_t ctx = 0; + int rc; - rc = daos_dmg_json_pipe("check set-policy", dmg_config_file, args, argcount, &dmg_out); + rc = dmg_ctx_init(dmg_config_file, &ctx); if (rc != 0) - D_ERROR("dmg check set-policy with flags %x, policies %s failed: %d\n", flags, - policies != NULL ? policies : "(null)", rc); - - if (dmg_out != NULL) - json_object_put(dmg_out); + return rc; - cmd_free_args(args, argcount); + rc = dmg_log_rc(daos_control_check_set_policy(ctx, flags, (char *)policies), + "daos_control_check_set_policy"); -out: + dmg_ctx_fini(ctx); return rc; } diff --git a/src/control/lib/control/c/SConscript b/src/control/lib/control/c/SConscript new file mode 100644 index 00000000000..0df1c11180b --- /dev/null +++ b/src/control/lib/control/c/SConscript @@ -0,0 +1,93 @@ +"""Build DAOS Control C Bindings Shared Library""" +import os +from os.path import join + + +def scons(): + """Execute build""" + Import('env', 'base_env', 'prereqs') + + env.AppendUnique(LIBPATH=[Dir('.')]) + env.AppendUnique(CPPPATH=[Dir('.'), Dir('.').srcnode()]) + env.d_add_build_rpath() + base_env.AppendUnique(LIBPATH=[Dir('.')]) + base_env.AppendUnique(CPPPATH=[Dir('.'), Dir('.').srcnode()]) + base_env.d_add_build_rpath() + + if not prereqs.client_requested(): + return + + cenv = env.Clone() + + if cenv.get("COMPILER") == 'covc': + cenv.Replace(CC='gcc', CXX='g++') + + cenv.Tool('go_builder') + + cenv.d_add_rpaths("..", True, True) + cenv.require('protobufc') + cenv.AppendENVPath("CGO_CFLAGS", cenv.subst("$_CPPINCFLAGS"), sep=" ") + + cgolibs = cenv.subst("-L$BUILD_DIR/src/gurt " + "-L$BUILD_DIR/src/cart " + "-L$BUILD_DIR/src/common " + "$_RPATH") + cenv.AppendENVPath("CGO_LDFLAGS", cgolibs, sep=" ") + + gosrc = Dir('.').srcnode().abspath + + build_lib = cenv.subst(join('$BUILD_DIR/src/control/lib/control/c', 'libdaos_control.so')) + build_hdr = cenv.subst(join('$BUILD_DIR/src/control/lib/control/c', 'libdaos_control.h')) + # Write the header out for in-tree consumers. Not packaged for distribution. + build_hdr_daos = cenv.subst(join('$BUILD_DIR/src/control/lib/control/c', + 'daos', 'libdaos_control.h')) + + Import('daos_version', 'conf_dir') + ldflags_path = 'github.com/daos-stack/daos/src/control/build' + ldflags = (f'-X {ldflags_path}.DaosVersion={daos_version} ' + f'-X {ldflags_path}.ConfigDir={conf_dir} ' + f'-extldflags "-Wl,-soname,libdaos_control.so"') + + # Build tags must mirror what other go binaries use so conditional files + # get compiled consistently. + is_release = cenv.get("BUILD_TYPE") == "release" + build_tags = ["spdk"] + if is_release: + build_tags.append("release") + else: + build_tags.append("fault_injection") + if cenv.d_is_valgrind_build(): # pylint: disable=no-member + build_tags.append("valgrind") + tags_flag = f"-tags {','.join(build_tags)}" + + # Build as a C shared library so that C consumers can link against it. + hdr_daos_dir = os.path.dirname(build_hdr_daos) + cmd = (f"cd {gosrc} && {cenv.d_go_bin} build -mod vendor " + f"-ldflags '{ldflags}' " + f"{tags_flag} " + f"-buildmode=c-shared " + f"-o {build_lib} . && chmod 0755 {build_lib} && " + f"mkdir -p {hdr_daos_dir} && cp -f {build_hdr} {build_hdr_daos}") + + sources = [File(os.path.join(gosrc, f)) for f in os.listdir(gosrc) + if f.endswith('.go') or f.endswith('.h')] + sources.append(cenv.d_go_bin) + + targets = cenv.Command([build_lib, build_hdr, build_hdr_daos], sources, cmd) + lib_target = targets[0] + + # Ensure library dependencies are built first + Depends(targets, cenv.subst('$BUILD_DIR/src/common/libdaos_common.so')) + Depends(targets, cenv.subst('$BUILD_DIR/src/cart/libcart.so')) + Depends(targets, cenv.subst('$BUILD_DIR/src/gurt/libgurt.so')) + + # Install the shared library only. libdaos_control is consumed by + # libdaos_tests via the build-tree header on CPPPATH. + cenv.Install('$PREFIX/lib64', lib_target) + + daos_control_tgts = targets + Export('daos_control_tgts') + + +if __name__ == "SCons.Script": + scons() diff --git a/src/control/lib/control/c/bindings.go b/src/control/lib/control/c/bindings.go new file mode 100644 index 00000000000..a89180a6ab0 --- /dev/null +++ b/src/control/lib/control/c/bindings.go @@ -0,0 +1,70 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +// Package main provides C bindings for the DAOS control API. +// This library is built with -buildmode=c-shared to produce libdaos_control.so. +package main + +/* +#include +#include + +#include +*/ +import "C" +import ( + "fmt" + "os" + "runtime/cgo" +) + +// main is required for c-shared build mode but is not called. +func main() {} + +//export daos_control_init +func daos_control_init(args *C.struct_daos_control_init_args, handleOut *C.uintptr_t) (rc C.int) { + defer recoverExport(&rc) + + if handleOut == nil { + return C.int(errorToRC(errInvalidHandle)) + } + + var cfgPath, logFilePath, logLevelStr string + if args != nil { + cfgPath = goString(args.dcia_config_file) + logFilePath = goString(args.dcia_log_file) + logLevelStr = goString(args.dcia_log_level) + } + + ctx, err := newContext(cfgPath, logFilePath, logLevelStr) + if err != nil { + return C.int(errorToRC(err)) + } + + h := cgo.NewHandle(ctx) + *handleOut = C.uintptr_t(h) + return 0 +} + +//export daos_control_fini +func daos_control_fini(handle C.uintptr_t) { + defer recoverExportVoid() + + if handle == 0 { + return + } + + h := cgo.Handle(handle) + ctx, ok := h.Value().(*ctrlContext) + if !ok { + fmt.Fprintf(os.Stderr, + "daos_control_fini: handle %#x does not refer to a control context (leaked)\n", + uintptr(handle)) + return + } + ctx.close() + h.Delete() +} diff --git a/src/control/lib/control/c/bindings_test.go b/src/control/lib/control/c/bindings_test.go new file mode 100644 index 00000000000..fccd1c58e1e --- /dev/null +++ b/src/control/lib/control/c/bindings_test.go @@ -0,0 +1,90 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "os" + "path/filepath" + "runtime/cgo" + "testing" + + "github.com/daos-stack/daos/src/control/lib/daos" +) + +func TestControlC_InitFini(t *testing.T) { + for name, tc := range map[string]struct { + configFile string + logFile string + logLevel string + useTempLog bool + expRC int + }{ + "defaults (insecure localhost)": {}, + "with logging": { + useTempLog: true, + logLevel: "debug", + }, + "nonexistent config file": { + configFile: "/nonexistent/config/file.yml", + expRC: int(daos.Nonexistent), + }, + "non-writable log path": { + logFile: "/nonexistent/dir/test.log", + expRC: int(daos.Nonexistent), + }, + } { + t.Run(name, func(t *testing.T) { + logFile := tc.logFile + if tc.useTempLog { + logFile = filepath.Join(t.TempDir(), "test.log") + } + + handle, rc := callInit(tc.configFile, logFile, tc.logLevel) + if rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + if tc.expRC != 0 { + if handle != 0 { + callFini(handle) + t.Fatal("expected zero handle on init failure") + } + return + } + if handle == 0 { + t.Fatal("expected non-zero handle") + } + callFini(handle) + + if tc.useTempLog { + if _, err := os.Stat(logFile); err != nil { + t.Fatalf("expected log file to be created: %s", err) + } + } + }) + } +} + +func TestControlC_InitNilHandleOut(t *testing.T) { + if rc := callInitNilHandleOut(); rc != int(daos.InvalidInput) { + t.Fatalf("rc=%d, want %d (InvalidInput)", rc, int(daos.InvalidInput)) + } +} + +func TestControlC_FiniZeroHandle(t *testing.T) { + // Should not panic with zero handle + callFini(0) +} + +func TestControlC_BogusHandlePanicRecovery(t *testing.T) { + // Any non-zero integer that was never handed out by cgo.NewHandle will + // cause cgo.Handle.Value() to panic; recoverExport maps that to MiscError. + const bogus = cgo.Handle(0xdeadbeef) + + if rc := callCheckSwitch(bogus, true); rc != int(daos.MiscError) { + t.Fatalf("rc=%d, want %d (MiscError)", rc, int(daos.MiscError)) + } +} diff --git a/src/control/lib/control/c/bindings_test_helpers.go b/src/control/lib/control/c/bindings_test_helpers.go new file mode 100644 index 00000000000..34a458804e5 --- /dev/null +++ b/src/control/lib/control/c/bindings_test_helpers.go @@ -0,0 +1,45 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build test_stubs + +// cgo drivers for bindings_test.go (see test_helpers.go for why these can't +// live in the test file). + +package main + +/* +#include + +#include +*/ +import "C" +import "runtime/cgo" + +func callInit(configFile, logFile, logLevel string) (cgo.Handle, int) { + cfg, cfgFree := cString(configFile) + defer cfgFree() + lf, lfFree := cString(logFile) + defer lfFree() + ll, llFree := cString(logLevel) + defer llFree() + + args := C.struct_daos_control_init_args{ + dcia_config_file: cfg, + dcia_log_file: lf, + dcia_log_level: ll, + } + var handle C.uintptr_t + rc := int(daos_control_init(&args, &handle)) + return cgo.Handle(handle), rc +} + +func callInitNilHandleOut() int { + var args C.struct_daos_control_init_args + return int(daos_control_init(&args, nil)) +} + +func callFini(handle cgo.Handle) { daos_control_fini(C.uintptr_t(handle)) } diff --git a/src/control/lib/control/c/cgo_glue.go b/src/control/lib/control/c/cgo_glue.go new file mode 100644 index 00000000000..7efc8b76b3f --- /dev/null +++ b/src/control/lib/control/c/cgo_glue.go @@ -0,0 +1,635 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +// Package-internal cgo glue: panic recovery, the export wrapper, the +// management context, error → rc translation, and C↔Go conversion helpers +// shared by every export. + +package main + +/* +#include +#include +#include +#include +#include +#include + +#include "daos_control_util.h" +*/ +import "C" +import ( + "context" + "errors" + "fmt" + "io" + "os" + "os/user" + "regexp" + "runtime/cgo" + "runtime/debug" + "strconv" + "strings" + "unsafe" + + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/daos-stack/daos/src/control/build" + "github.com/daos-stack/daos/src/control/common" + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/lib/ranklist" + "github.com/daos-stack/daos/src/control/logging" + "github.com/daos-stack/daos/src/control/system" + "github.com/daos-stack/daos/src/control/system/checker" +) + +// init scrubs HTTP_PROXY/etc from the process environment at .so load time. +// gRPC-Go's default dialer honors these vars, but management RPCs are direct +// dials on the control network and must not be routed through any proxy. The +// standalone Go binaries do this from main(); a c-shared library has no +// main(), so it goes in package init before any export can run. +func init() { + common.ScrubProxyVariables() +} + +// recoverExport converts any panic into an error return code. Every cgo- +// exported function returning C.int should defer this with a pointer to its +// named return, so that a Go panic does not propagate across the cgo boundary +// (where it would SIGABRT the process). The panic and a stack trace are +// written to stderr for post-mortem diagnosis. +func recoverExport(rc *C.int) { + if r := recover(); r != nil { + fmt.Fprintf(os.Stderr, "panic in libdaos_control export: %v\n%s\n", r, debug.Stack()) + *rc = C.int(daos.MiscError) + } +} + +// recoverExportVoid is recoverExport for exports that do not return a code. +func recoverExportVoid() { + if r := recover(); r != nil { + fmt.Fprintf(os.Stderr, "panic in libdaos_control export: %v\n%s\n", r, debug.Stack()) + } +} + +// withContext wraps the standard //export dance: panic recovery, handle +// lookup, and Go-error → rc translation. The closure returns nil for +// success or an error for failure (a daos.Status, or anything errorToRC +// understands). getContext failures short-circuit before the closure runs. +func withContext(handle C.uintptr_t, fn func(*ctrlContext) error) (rc C.int) { + defer recoverExport(&rc) + ctx, errRC := getContext(handle) + if errRC != 0 { + return errRC + } + if err := fn(ctx); err != nil { + rc := errorToRC(err) + // When errorToRC exhausts every specific check and falls back to + // DER_MISC, leave a breadcrumb so the next reader of the trace log + // can see what kind of error escaped classification. + if rc == int(daos.MiscError) && ctx.log != nil { + ctx.log.Debugf("errorToRC: no mapping for %T (%s); using DER_MISC", + err, summarizeRPCErr(err)) + } + return C.int(rc) + } + return 0 +} + +// ctrlContext holds the client connection state for a management context. +type ctrlContext struct { + client control.UnaryInvoker + log *logging.LeveledLogger + logFile *os.File +} + +// newContext creates a new management context from a config file path. +// If configFile is empty, uses default config (localhost, insecure mode). +// logFilePath and logLevelStr configure logging; empty strings use defaults. +func newContext(configFile, logFilePath, logLevelStr string) (*ctrlContext, error) { + var cfg *control.Config + var err error + + if configFile == "" { + cfg = control.DefaultConfig() + cfg.TransportConfig.AllowInsecure = true + } else { + cfg, err = control.LoadConfig(configFile) + if err != nil { + return nil, err + } + } + + var logDest io.Writer = io.Discard + var logFile *os.File + + if logFilePath != "" { + logFile, err = os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return nil, err + } + logDest = logFile + } + + // Notice and Error also go to stderr so that when libdaos_control is + // loaded inside a test binary (e.g. daos_test) the failure reason is + // captured by the job manager's stdout/stderr collection even if the + // log file is unavailable or not collected. Debug/Info remain + // log-file-only to avoid spamming the test transcript. + log := logging.NewCombinedLogger("daos_control", logDest). + WithLogLevel(logging.LogLevelNotice). + WithNoticeLogger(logging.NewNoticeLogger("daos_control", os.Stderr)). + WithErrorLogger(logging.NewErrorLogger("daos_control", os.Stderr)) + + if logLevelStr != "" { + var level logging.LogLevel + if err := level.SetString(logLevelStr); err == nil { + log.SetLevel(level) + } else { + log.Noticef("daos_control: ignoring invalid log_level %q: %s; keeping %s", + logLevelStr, err, logging.LogLevelNotice) + } + } + + client := control.NewClient( + control.WithConfig(cfg), + control.WithClientLogger(log), + control.WithClientComponent(build.ComponentAdmin), + ) + + return &ctrlContext{ + client: client, + log: log, + logFile: logFile, + }, nil +} + +// ctx returns a background context for operations. +func (c *ctrlContext) ctx() context.Context { + ctx, _ := logging.ToContext(context.Background(), c.log) + return ctx +} + +// close releases resources associated with the context. +func (c *ctrlContext) close() { + if c.logFile != nil { + c.logFile.Close() + } +} + +// newTestContext creates a context with a mock invoker for testing. +func newTestContext(client control.UnaryInvoker, log *logging.LeveledLogger) *ctrlContext { + if log == nil { + log = logging.NewCombinedLogger("test", io.Discard) + } + return &ctrlContext{ + client: client, + log: log, + } +} + +// errInvalidHandle is returned when a zero/invalid handle is provided. +var errInvalidHandle = errors.New("invalid control API handle") + +// requireRank rejects the CRT_NO_RANK sentinel (== ranklist.NilRank) where a +// specific rank is required. +func requireRank(rank C.d_rank_t) error { + if ranklist.Rank(rank) == ranklist.NilRank { + return fmt.Errorf("a specific rank is required, got CRT_NO_RANK: %w", + daos.InvalidInput) + } + return nil +} + +// getContext validates the handle and retrieves the context. Returns nil and +// a non-zero rc if the handle is invalid. cgo.Handle.Value() panics on a +// never-allocated or freed handle; that panic is caught by recoverExport at +// the cgo boundary, not here. +func getContext(handle C.uintptr_t) (*ctrlContext, C.int) { + if handle == 0 { + return nil, C.int(errorToRC(errInvalidHandle)) + } + + c, ok := cgo.Handle(handle).Value().(*ctrlContext) + if !ok { + return nil, C.int(errorToRC(errInvalidHandle)) + } + return c, 0 +} + +// errorToRC converts a Go error to a DAOS return code. +func errorToRC(err error) int { + if err == nil { + return 0 + } + + var ds daos.Status + if errors.As(err, &ds) { + return int(ds) + } + + if errors.Is(err, errInvalidHandle) { + return int(daos.InvalidInput) + } + if errors.Is(err, control.ErrNoConfigFile) { + return int(daos.BadPath) + } + + if errors.Is(err, os.ErrNotExist) { + return int(daos.Nonexistent) + } + if errors.Is(err, os.ErrPermission) { + return int(daos.NoPermission) + } + + if errors.Is(err, context.DeadlineExceeded) { + return int(daos.TimedOut) + } + if errors.Is(err, context.Canceled) { + return int(daos.Canceled) + } + + // Many control-plane paths stringify a daos.Status into the error message + // (see daos.Status.Error()). As a last resort before collapsing to + // MiscError, recover the embedded status code so callers can distinguish + // e.g. DER_BUSY from DER_NONEXIST. + if st, ok := statusFromMsg(err.Error()); ok { + return int(st) + } + + // MS conditions arrive wire-stringified (their types don't survive + // gRPC); match the canonical sentinels' renderings, not local prose. + if msg := err.Error(); strings.Contains(msg, (&system.ErrPoolNotFound{}).Error()) { + return int(daos.Nonexistent) + } else if strings.Contains(msg, checker.FaultCheckerEnabled.Error()) { + return int(daos.Busy) + } + + // Surface gRPC transport classes so a dial failure (Unavailable) or a + // server-side classified error doesn't collapse to a generic DER_MISC. + if gs, ok := status.FromError(err); ok { + switch gs.Code() { + case codes.Unavailable: + return int(daos.Unreachable) + case codes.DeadlineExceeded: + return int(daos.TimedOut) + case codes.Canceled: + return int(daos.Canceled) + case codes.NotFound: + return int(daos.Nonexistent) + case codes.AlreadyExists: + return int(daos.Exists) + case codes.PermissionDenied, codes.Unauthenticated: + return int(daos.NoPermission) + case codes.ResourceExhausted: + return int(daos.NoSpace) + case codes.InvalidArgument: + return int(daos.InvalidInput) + case codes.Unimplemented: + return int(daos.NotImpl) + case codes.Aborted: + return int(daos.TryAgain) + } + } + + return int(daos.MiscError) +} + +// summarizeRPCErr returns a one-line err string for logging. When the message +// embeds an HTTP response (gRPC transport errors via an HTTP proxy, for +// example, where the response body has been %q-escaped by the inner wrapper), +// the body is compacted to its status line plus Server header so the actual +// failure isn't buried under escaped HTML. +func summarizeRPCErr(err error) string { + if err == nil { + return "" + } + msg := err.Error() + hidx := strings.Index(msg, "HTTP/") + if hidx < 0 { + return msg + } + rest := msg[hidx:] + // Status line terminator: either the literal 4-char "\r\n" sequence + // (when an inner wrapper has %q-escaped the response) or an actual CRLF. + nl := strings.Index(rest, `\r\n`) + if nl < 0 { + nl = strings.Index(rest, "\r\n") + } + if nl <= 0 { + return msg + } + summary := msg[:hidx] + rest[:nl] + if k := strings.Index(rest, "Server: "); k > 0 { + srv := rest[k+len("Server: "):] + l := strings.Index(srv, `\r\n`) + if l < 0 { + l = strings.Index(srv, "\r\n") + } + if l > 0 { + summary += " (Server: " + srv[:l] + ")" + } + } + return summary +} + +// derStatusPattern matches a daos.Status.Error() token, which formats as +// "DER_X(-code)". Anchoring to the DER_ identifier avoids false matches on +// unrelated parenthesized numbers in wrapper text (e.g. "retry(-1)"). +var derStatusPattern = regexp.MustCompile(`DER_[A-Z0-9_]+\((-?\d+)\)`) + +// statusFromMsg extracts a daos.Status from a message produced by +// daos.Status.Error(), which formats as "DER_X(-code): description". Returns +// (0, false) if no DER_XX(-code) token is present. +func statusFromMsg(msg string) (daos.Status, bool) { + for _, m := range derStatusPattern.FindAllStringSubmatch(msg, -1) { + code, err := strconv.Atoi(m[1]) + if err == nil && code <= 0 && code > -10000 { + return daos.Status(code), true + } + } + return 0, false +} + +// firstErroredStatus walks items and returns a DAOS status for the first +// one that errored. Returns nil if none errored. +func firstErroredStatus[T any](items []T, getErr func(T) error) error { + for _, it := range items { + err := getErr(it) + if err == nil { + continue + } + var ds daos.Status + if errors.As(err, &ds) { + return ds + } + if st, ok := statusFromMsg(err.Error()); ok { + return st + } + return daos.MiscError + } + return nil +} + +// firstRankStatus adapts firstErroredStatus for []*control.PoolRankResult. +func firstRankStatus(results []*control.PoolRankResult) error { + return firstErroredStatus(results, func(r *control.PoolRankResult) error { + if r == nil || !r.Errored { + return nil + } + return errors.New(r.Msg) + }) +} + +// firstMemberStatus adapts firstErroredStatus for system.MemberResults. +func firstMemberStatus(results system.MemberResults) error { + return firstErroredStatus(results, func(r *system.MemberResult) error { + if r == nil || !r.Errored { + return nil + } + return errors.New(r.Msg) + }) +} + +// logAllHostErrors emits each host error in the map to the context log at +// Error level, so an rc that collapses them doesn't lose the diagnostic. +func logAllHostErrors(ctx *ctrlContext, hem control.HostErrorsMap) { + if ctx == nil || ctx.log == nil { + return + } + for _, key := range hem.Keys() { + hes, ok := hem[key] + if !ok || hes == nil || hes.HostError == nil { + continue + } + hosts := "" + if hes.HostSet != nil { + hosts = hes.HostSet.RangedString() + } + ctx.log.Errorf("host(s) %s: %s", hosts, hes.HostError) + } +} + +// firstHostStatus adapts firstErroredStatus for control.HostErrorsMap. +func firstHostStatus(hem control.HostErrorsMap) error { + keys := hem.Keys() + return firstErroredStatus(keys, func(k string) error { + hes, ok := hem[k] + if !ok || hes == nil || hes.HostError == nil { + return nil + } + return hes.HostError + }) +} + +// uuidFromC converts a C uuid_t to a Go uuid.UUID. +func uuidFromC(cUUID *C.uuid_t) uuid.UUID { + if cUUID == nil { + return uuid.Nil + } + var goUUID uuid.UUID + for i := 0; i < 16; i++ { + goUUID[i] = byte(cUUID[i]) + } + return goUUID +} + +// poolIDFromC converts a C uuid_t to a pool ID string. +func poolIDFromC(cUUID *C.uuid_t) string { + return uuidFromC(cUUID).String() +} + +// copyUUIDToC copies a Go uuid.UUID to a C uuid_t. +func copyUUIDToC(goUUID uuid.UUID, cUUID *C.uuid_t) { + if cUUID == nil { + return + } + for i := 0; i < 16; i++ { + cUUID[i] = C.uchar(goUUID[i]) + } +} + +// rankListFromC converts a C d_rank_list_t to a slice of ranklist.Rank. +func rankListFromC(cRankList *C.d_rank_list_t) []ranklist.Rank { + if cRankList == nil || cRankList.rl_nr == 0 || cRankList.rl_ranks == nil { + return nil + } + + ranks := make([]ranklist.Rank, cRankList.rl_nr) + cRanks := unsafe.Slice(cRankList.rl_ranks, cRankList.rl_nr) + for i, r := range cRanks { + ranks[i] = ranklist.Rank(r) + } + return ranks +} + +// copyRankListToC copies up to maxLen ranks from a []ranklist.Rank into a +// caller-pre-allocated C d_rank_list_t and sets rl_nr to the number of ranks +// actually written. +func copyRankListToC(ranks []ranklist.Rank, cRankList *C.d_rank_list_t, maxLen int) { + if cRankList == nil { + return + } + if cRankList.rl_ranks == nil || maxLen <= 0 { + cRankList.rl_nr = 0 + return + } + + toCopy := len(ranks) + if toCopy > maxLen { + toCopy = maxLen + } + + cRankList.rl_nr = C.uint32_t(toCopy) + if toCopy == 0 { + return + } + cRanks := unsafe.Slice(cRankList.rl_ranks, toCopy) + for i := 0; i < toCopy; i++ { + cRanks[i] = C.d_rank_t(ranks[i]) + } +} + +// goString safely converts a C string to a Go string, returning empty string for nil. +func goString(cStr *C.char) string { + if cStr == nil { + return "" + } + return C.GoString(cStr) +} + +// uidToUsername converts a numeric UID to a username. +func uidToUsername(uid uint32) (string, error) { + u, err := user.LookupId(strconv.FormatUint(uint64(uid), 10)) + if err != nil { + return "", fmt.Errorf("uid %d lookup: %s: %w", uid, err, daos.InvalidInput) + } + return u.Username, nil +} + +// gidToGroupname converts a numeric GID to a group name. +func gidToGroupname(gid uint32) (string, error) { + g, err := user.LookupGroupId(strconv.FormatUint(uint64(gid), 10)) + if err != nil { + return "", fmt.Errorf("gid %d lookup: %s: %w", gid, err, daos.InvalidInput) + } + return g.Name, nil +} + +// propsFromC converts a daos_prop_t to Go PoolProperty values for a +// pool-create request. Entries handled outside the property registry +// (ACL, owner, owner_group, svc_list) are silently skipped; all other +// unrecognized property types return daos.NotSupported. +func propsFromC(cProps *C.daos_prop_t) ([]*daos.PoolProperty, error) { + if cProps == nil || cProps.dpp_nr == 0 { + return nil, nil + } + + entries := unsafe.Slice(cProps.dpp_entries, cProps.dpp_nr) + registry := daos.PoolProperties() + numToName := make(map[uint32]string, len(registry)) + for name, handler := range registry { + numToName[handler.GetProperty(name).Number] = name + } + + var props []*daos.PoolProperty + for i := range entries { + entry := &entries[i] + propNum := uint32(entry.dpe_type) + + // These entries are handled by poolCreateExtrasFromC, not the + // registry. svc_list is read-only and unsettable at create time. + switch propNum { + case C.DAOS_PROP_PO_ACL, + C.DAOS_PROP_PO_OWNER, + C.DAOS_PROP_PO_OWNER_GROUP, + C.DAOS_PROP_PO_SVC_LIST: + continue + } + + name, ok := numToName[propNum] + if !ok { + return nil, daos.NotSupported + } + p := registry[name].GetProperty(name) + + switch propNum { + case C.DAOS_PROP_PO_LABEL: + if s := C.get_dpe_str(entry); s != nil { + p.Value.SetString(C.GoString(s)) + } + default: + p.Value.SetNumber(uint64(C.get_dpe_val(entry))) + } + + props = append(props, p) + } + + return props, nil +} + +// poolCreateExtrasFromC extracts ACL entries and owner/owner-group overrides +// from a pool-create prop list and applies them to req. ACL entries come from +// DAOS_PROP_PO_ACL (dpe_val_ptr → struct daos_acl *); owner/owner_group come +// from DAOS_PROP_PO_OWNER/DAOS_PROP_PO_OWNER_GROUP (dpe_str values such as +// "alice@" or "admins@"). +// +// The old dmg_pool_create helper (065f68a3c0~1:src/common/tests_dmg_helpers.c) +// passed ACL entries via a tempfile to --acl-file, but did NOT read +// DAOS_PROP_PO_OWNER or DAOS_PROP_PO_OWNER_GROUP from the prop at all — owner +// was derived solely from uid/gid. The new path honors both for full parity. +func poolCreateExtrasFromC(cProps *C.daos_prop_t, req *control.PoolCreateReq) error { + if cProps == nil || cProps.dpp_nr == 0 { + return nil + } + + entries := unsafe.Slice(cProps.dpp_entries, cProps.dpp_nr) + for i := range entries { + entry := &entries[i] + propNum := uint32(entry.dpe_type) + switch propNum { + case C.DAOS_PROP_PO_ACL: + acl := (*C.struct_daos_acl)(C.get_dpe_val_ptr(entry)) + if acl == nil { + continue + } + var aceStrs **C.char + var aceNr C.size_t + if rc := C.daos_acl_to_strs(acl, &aceStrs, &aceNr); rc != 0 { + return fmt.Errorf("daos_acl_to_strs: %w", daos.Status(rc)) + } + nr := int(aceNr) + goStrs := make([]string, nr) + if nr > 0 && aceStrs != nil { + cSlice := unsafe.Slice(aceStrs, nr) + for j := 0; j < nr; j++ { + goStrs[j] = C.GoString(cSlice[j]) + C.free(unsafe.Pointer(cSlice[j])) + } + } + if aceStrs != nil { + C.free(unsafe.Pointer(aceStrs)) + } + if req.ACL == nil { + req.ACL = &control.AccessControlList{} + } + req.ACL.Entries = goStrs + + case C.DAOS_PROP_PO_OWNER: + if s := C.get_dpe_str(entry); s != nil { + req.User = C.GoString(s) + } + + case C.DAOS_PROP_PO_OWNER_GROUP: + if s := C.get_dpe_str(entry); s != nil { + req.UserGroup = C.GoString(s) + } + } + } + return nil +} diff --git a/src/control/lib/control/c/check.go b/src/control/lib/control/c/check.go new file mode 100644 index 00000000000..999ec4bf560 --- /dev/null +++ b/src/control/lib/control/c/check.go @@ -0,0 +1,284 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +/* +#include +#include +#include +#include + +#include +*/ +import "C" +import ( + "fmt" + "sort" + "strings" + "unsafe" + + "github.com/google/uuid" + + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" +) + +// parsePolicies parses a comma-separated "CLASS:ACTION" policy string. +// Errors include the offending element's index and wrap daos.InvalidInput +// so errorToRC reduces them to a single rc at the cgo boundary. +func parsePolicies(policiesStr string) ([]*control.SystemCheckPolicy, error) { + var policies []*control.SystemCheckPolicy + + for i, policyStr := range strings.Split(policiesStr, ",") { + policyStr = strings.TrimSpace(policyStr) + if policyStr == "" { + continue + } + parts := strings.SplitN(policyStr, ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("policy[%d] %q: missing CLASS:ACTION separator: %w", + i, policyStr, daos.InvalidInput) + } + policy, err := control.NewSystemCheckPolicy(parts[0], parts[1]) + if err != nil { + return nil, fmt.Errorf("policy[%d] %q: %v: %w", + i, policyStr, err, daos.InvalidInput) + } + policies = append(policies, policy) + } + + return policies, nil +} + +// uuidStringsFromC translates an array of C uuid_t values (length poolNr) +// into canonical Go UUID strings, skipping zero UUIDs. +func uuidStringsFromC(uuids *C.uuid_t, poolNr C.uint32_t) []string { + if uuids == nil || poolNr == 0 { + return nil + } + slice := unsafe.Slice(uuids, poolNr) + out := make([]string, 0, poolNr) + for i := range slice { + if u := uuidFromC(&slice[i]); u != uuid.Nil { + out = append(out, u.String()) + } + } + return out +} + +//export daos_control_check_switch +func daos_control_check_switch(handle C.uintptr_t, enable C.int) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + if enable != 0 { + return control.SystemCheckEnable(ctx.ctx(), ctx.client, &control.SystemCheckEnableReq{}) + } + return control.SystemCheckDisable(ctx.ctx(), ctx.client, &control.SystemCheckDisableReq{}) + }) +} + +//export daos_control_check_start +func daos_control_check_start(handle C.uintptr_t, flags, poolNr C.uint32_t, uuids *C.uuid_t, policies *C.char) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.SystemCheckStartReq{} + req.CheckStartReq.Flags = uint32(flags) + req.CheckStartReq.Uuids = uuidStringsFromC(uuids, poolNr) + + if policiesStr := goString(policies); policiesStr != "" { + parsed, err := parsePolicies(policiesStr) + if err != nil { + ctx.log.Errorf("parsePolicies: %v", err) + return err + } + req.Policies = parsed + } + return control.SystemCheckStart(ctx.ctx(), ctx.client, req) + }) +} + +//export daos_control_check_stop +func daos_control_check_stop(handle C.uintptr_t, poolNr C.uint32_t, uuids *C.uuid_t) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.SystemCheckStopReq{} + req.CheckStopReq.Uuids = uuidStringsFromC(uuids, poolNr) + return control.SystemCheckStop(ctx.ctx(), ctx.client, req) + }) +} + +//export daos_control_check_query +func daos_control_check_query(handle C.uintptr_t, poolNr C.uint32_t, uuids *C.uuid_t, dci *C.struct_daos_check_info) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.SystemCheckQueryReq{} + req.CheckQueryReq.Uuids = uuidStringsFromC(uuids, poolNr) + + resp, err := control.SystemCheckQuery(ctx.ctx(), ctx.client, req) + if err != nil { + return err + } + + if dci == nil { + return nil + } + + // Zero-init so conditionally-set pointer fields stay NULL. + C.memset(unsafe.Pointer(dci), 0, C.size_t(unsafe.Sizeof(*dci))) + + dci.dci_status = C.CString(resp.Status.String()) + dci.dci_phase = C.CString(resp.ScanPhase.String()) + dci.dci_leader = C.int(resp.Leader) + + // Empty queryUUIDs means "report on every pool the server returned"; + // a non-empty list filters to those UUIDs. Sort the no-filter case + // by UUID so output order is deterministic across map iterations. + var includeUUIDs []string + if len(req.CheckQueryReq.Uuids) > 0 { + for _, qUUID := range req.CheckQueryReq.Uuids { + if _, ok := resp.Pools[qUUID]; ok { + includeUUIDs = append(includeUUIDs, qUUID) + } + } + } else if len(resp.Pools) > 0 { + includeUUIDs = make([]string, 0, len(resp.Pools)) + for u := range resp.Pools { + includeUUIDs = append(includeUUIDs, u) + } + sort.Strings(includeUUIDs) + } + + if len(includeUUIDs) > 0 { + dci.dci_pools = (*C.struct_daos_check_pool_info)(C.calloc(C.size_t(len(includeUUIDs)), + C.size_t(unsafe.Sizeof(C.struct_daos_check_pool_info{})))) + if dci.dci_pools == nil { + daos_control_check_info_free(dci) + return daos.NoMemory + } + poolSlice := unsafe.Slice(dci.dci_pools, len(includeUUIDs)) + + for i, u := range includeUUIDs { + pool := resp.Pools[u] + if parsedUUID, err := uuid.Parse(pool.UUID); err == nil { + copyUUIDToC(parsedUUID, &poolSlice[i].dcpi_uuid) + } + poolSlice[i].dcpi_status = C.CString(pool.Status) + poolSlice[i].dcpi_phase = C.CString(pool.Phase) + } + dci.dci_pool_nr = C.int(len(includeUUIDs)) + } + + if len(resp.Reports) > 0 { + dci.dci_reports = (*C.struct_daos_check_report_info)(C.calloc(C.size_t(len(resp.Reports)), + C.size_t(unsafe.Sizeof(C.struct_daos_check_report_info{})))) + if dci.dci_reports == nil { + daos_control_check_info_free(dci) + return daos.NoMemory + } + reportSlice := unsafe.Slice(dci.dci_reports, len(resp.Reports)) + + for i, rpt := range resp.Reports { + if parsedUUID, err := uuid.Parse(rpt.PoolUuid); err == nil { + copyUUIDToC(parsedUUID, &reportSlice[i].dcri_uuid) + } + reportSlice[i].dcri_seq = C.uint64_t(rpt.Seq) + reportSlice[i].dcri_class = C.uint32_t(rpt.Class) + reportSlice[i].dcri_act = C.uint32_t(rpt.Action) + reportSlice[i].dcri_result = C.int(rpt.Result) + reportSlice[i].dcri_rank = C.int(rpt.Rank) + + nChoices := len(rpt.ActChoices) + if nChoices > C.DAOS_CHECK_MAX_ACT_OPTIONS { + nChoices = C.DAOS_CHECK_MAX_ACT_OPTIONS + } + reportSlice[i].dcri_option_nr = C.int(nChoices) + for j := 0; j < nChoices; j++ { + reportSlice[i].dcri_options[j] = C.int(rpt.ActChoices[j]) + } + } + dci.dci_report_nr = C.int(len(resp.Reports)) + } + + return nil + }) +} + +//export daos_control_check_info_free +func daos_control_check_info_free(dci *C.struct_daos_check_info) { + defer recoverExportVoid() + + if dci == nil { + return + } + + if dci.dci_status != nil { + C.free(unsafe.Pointer(dci.dci_status)) + dci.dci_status = nil + } + if dci.dci_phase != nil { + C.free(unsafe.Pointer(dci.dci_phase)) + dci.dci_phase = nil + } + + if dci.dci_pools != nil { + poolSlice := unsafe.Slice(dci.dci_pools, dci.dci_pool_nr) + for i := 0; i < int(dci.dci_pool_nr); i++ { + if poolSlice[i].dcpi_status != nil { + C.free(unsafe.Pointer(poolSlice[i].dcpi_status)) + } + if poolSlice[i].dcpi_phase != nil { + C.free(unsafe.Pointer(poolSlice[i].dcpi_phase)) + } + } + C.free(unsafe.Pointer(dci.dci_pools)) + dci.dci_pools = nil + dci.dci_pool_nr = 0 + } + + if dci.dci_reports != nil { + C.free(unsafe.Pointer(dci.dci_reports)) + dci.dci_reports = nil + dci.dci_report_nr = 0 + } +} + +//export daos_control_check_repair +func daos_control_check_repair(handle C.uintptr_t, seq C.uint64_t, action C.uint32_t) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.SystemCheckRepairReq{} + req.CheckActReq.Seq = uint64(seq) + if err := req.SetAction(int32(action)); err != nil { + return daos.InvalidInput + } + return control.SystemCheckRepair(ctx.ctx(), ctx.client, req) + }) +} + +//export daos_control_check_set_policy +func daos_control_check_set_policy(handle C.uintptr_t, flags C.uint32_t, policies *C.char) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.SystemCheckSetPolicyReq{} + + // TCPF_* flag values from chk.proto (SetSystemCheckPolicyFlags). + const ( + tcpfReset = 1 << 0 + tcpfInteract = 1 << 1 + ) + if uint32(flags)&tcpfReset != 0 { + req.ResetToDefaults = true + } + if uint32(flags)&tcpfInteract != 0 { + req.AllInteractive = true + } + + if policiesStr := goString(policies); policiesStr != "" { + parsed, err := parsePolicies(policiesStr) + if err != nil { + ctx.log.Errorf("parsePolicies: %v", err) + return err + } + req.Policies = parsed + } + return control.SystemCheckSetPolicy(ctx.ctx(), ctx.client, req) + }) +} diff --git a/src/control/lib/control/c/check_test.go b/src/control/lib/control/c/check_test.go new file mode 100644 index 00000000000..b53f017bc4e --- /dev/null +++ b/src/control/lib/control/c/check_test.go @@ -0,0 +1,614 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "errors" + "strings" + "testing" + + "github.com/google/uuid" + + chk "github.com/daos-stack/daos/src/control/common/proto/chk" + mgmtpb "github.com/daos-stack/daos/src/control/common/proto/mgmt" + "github.com/daos-stack/daos/src/control/common/test" + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/logging" +) + +func TestControlC_ParsePolicies(t *testing.T) { + for name, tc := range map[string]struct { + input string + expErrSub string // substring expected in err.Error() + }{ + "missing separator": { + input: "POOL_BAD_LABEL", + expErrSub: `policy[0] "POOL_BAD_LABEL"`, + }, + "bad class/action": { + input: "BOGUS:NONSENSE", + expErrSub: `policy[0] "BOGUS:NONSENSE"`, + }, + "second element bad": { + input: "CIC_POOL_NONEXIST_ON_MS:CIA_IGNORE,bogus-entry", + expErrSub: `policy[1] "bogus-entry"`, + }, + } { + t.Run(name, func(t *testing.T) { + _, err := parsePolicies(tc.input) + if err == nil { + t.Fatalf("expected error for %q", tc.input) + } + if !strings.Contains(err.Error(), tc.expErrSub) { + t.Errorf("error %q missing %q", err.Error(), tc.expErrSub) + } + if !errors.Is(err, daos.InvalidInput) { + t.Errorf("errors.Is(daos.InvalidInput) false for %v", err) + } + }) + } +} + +func TestControlC_CheckSwitch(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + enable bool + expRC int + }{ + "enable success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.DaosResp{}), + }, + enable: true, + expRC: 0, + }, + "disable success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.DaosResp{}), + }, + enable: false, + expRC: 0, + }, + "enable failure": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.NoPermission, + }, + enable: true, + expRC: int(daos.NoPermission), + }, + "disable failure": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.NoPermission, + }, + enable: false, + expRC: int(daos.NoPermission), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + rc := callCheckSwitch(handle, tc.enable) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestControlC_CheckStart(t *testing.T) { + testUUID := uuid.MustParse("12345678-1234-1234-1234-123456789abc") + + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + flags uint32 + poolUUIDs []uuid.UUID + policies string + expRC int + }{ + "success - no pools": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.CheckStartResp{}), + }, + expRC: 0, + }, + "success - with pools": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.CheckStartResp{}), + }, + poolUUIDs: []uuid.UUID{testUUID}, + expRC: 0, + }, + "success - with policies": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.CheckStartResp{}), + }, + policies: "CIC_POOL_BAD_LABEL:CIA_INTERACT", + expRC: 0, + }, + "failure - invalid policy format": { + mic: &control.MockInvokerConfig{}, + policies: "invalid", + expRC: int(daos.InvalidInput), + }, + "failure": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.Busy, + }, + expRC: int(daos.Busy), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + rc := callCheckStart(handle, tc.flags, tc.poolUUIDs, tc.policies) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestControlC_CheckStop(t *testing.T) { + testUUID := uuid.MustParse("12345678-1234-1234-1234-123456789abc") + + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + poolUUIDs []uuid.UUID + expRC int + }{ + "success - no pools": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.CheckStopResp{}), + }, + expRC: 0, + }, + "success - with pools": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.CheckStopResp{}), + }, + poolUUIDs: []uuid.UUID{testUUID}, + expRC: 0, + }, + "failure": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.Busy, + }, + expRC: int(daos.Busy), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + rc := callCheckStop(handle, tc.poolUUIDs) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestControlC_CheckQuery(t *testing.T) { + testUUID := uuid.MustParse("12345678-1234-1234-1234-123456789abc") + + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + poolUUIDs []uuid.UUID + expRC int + }{ + "success - no pools": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.CheckQueryResp{}), + }, + expRC: 0, + }, + "success - with pools": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.CheckQueryResp{}), + }, + poolUUIDs: []uuid.UUID{testUUID}, + expRC: 0, + }, + "failure": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.Busy, + }, + expRC: int(daos.Busy), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + rc := callCheckQuery(handle, tc.poolUUIDs) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestControlC_CheckQueryWithInfoAndFree(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mic := &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.CheckQueryResp{}), + } + + mi := control.NewMockInvoker(log, mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + info, rc := callCheckQueryWithInfo(handle, nil) + if rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + // Free-on-zeroed path: the helper already ran the free twice internally. + // Post-free state must be zeroed. + if !info.PostFreeZeroed { + t.Fatal("post-free state not zeroed on empty struct") + } +} + +func TestControlC_CheckInfoFreeNil(t *testing.T) { + callCheckInfoFreeNil() +} + +// TestCheckQueryWithInfoPopulated exercises daos_control_check_query with a +// non-empty response: pools are only populated for UUIDs the caller asked +// about, reports always populate, and daos_control_check_info_free zeroes +// the struct's counts/pointers. +func TestControlC_CheckQueryWithInfoPopulated(t *testing.T) { + uuidA := uuid.MustParse("10000000-0000-0000-0000-000000000001") + uuidB := uuid.MustParse("20000000-0000-0000-0000-000000000002") + uuidC := uuid.MustParse("30000000-0000-0000-0000-000000000003") // sent by server but not requested + + // Server-side phase/status enum values are small ints; exact name + // strings are enum .String() — we just verify the non-empty set the + // control layer stringifies. + resp := &mgmtpb.CheckQueryResp{ + InsStatus: chk.CheckInstStatus_CIS_RUNNING, + InsPhase: chk.CheckScanPhase_CSP_POOL_MBS, + Pools: []*mgmtpb.CheckQueryPool{ + { + Uuid: uuidA.String(), + Status: chk.CheckPoolStatus_CPS_CHECKING, + Phase: chk.CheckScanPhase_CSP_POOL_MBS, + // Time must be non-nil: getPoolCheckInfo in the control + // library dereferences pbPool.Time without a guard. + Time: &mgmtpb.CheckQueryTime{}, + }, + { + Uuid: uuidB.String(), + Status: chk.CheckPoolStatus_CPS_CHECKED, + Phase: chk.CheckScanPhase_CSP_DONE, + Time: &mgmtpb.CheckQueryTime{}, + }, + { + Uuid: uuidC.String(), + Status: chk.CheckPoolStatus_CPS_CHECKING, + Time: &mgmtpb.CheckQueryTime{}, + }, + }, + Reports: []*chk.CheckReport{ + { + Seq: 42, + Class: chk.CheckInconsistClass_CIC_POOL_NONEXIST_ON_MS, + Action: chk.CheckInconsistAction_CIA_INTERACT, + Result: 0, + PoolUuid: uuidA.String(), + ActChoices: []chk.CheckInconsistAction{ + chk.CheckInconsistAction_CIA_IGNORE, + chk.CheckInconsistAction_CIA_DISCARD, + }, + }, + { + Seq: 43, + Class: chk.CheckInconsistClass_CIC_POOL_LESS_SVC_WITH_QUORUM, + Action: chk.CheckInconsistAction_CIA_DISCARD, + Result: 0, + PoolUuid: uuidB.String(), + }, + }, + } + + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, resp), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + info, rc := callCheckQueryWithInfo(handle, []uuid.UUID{uuidA, uuidB}) + if rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + + if info.Status == "" { + t.Error("Status not populated") + } + if info.Phase == "" { + t.Error("Phase not populated") + } + + if len(info.Pools) != 2 { + t.Fatalf("got %d pools, want 2", len(info.Pools)) + } + gotUUIDs := map[uuid.UUID]bool{info.Pools[0].UUID: true, info.Pools[1].UUID: true} + for _, want := range []uuid.UUID{uuidA, uuidB} { + if !gotUUIDs[want] { + t.Errorf("pools missing UUID %s", want) + } + } + if gotUUIDs[uuidC] { + t.Errorf("pools contains un-requested UUID %s", uuidC) + } + for i, p := range info.Pools { + if p.Status == "" { + t.Errorf("pools[%d].Status empty", i) + } + if p.Phase == "" { + t.Errorf("pools[%d].Phase empty", i) + } + } + + if len(info.Reports) != 2 { + t.Fatalf("got %d reports, want 2", len(info.Reports)) + } + // Reports sort by class, not input order — index by UUID for robustness. + reportsByUUID := make(map[uuid.UUID]testCheckReportInfo, len(info.Reports)) + for _, r := range info.Reports { + reportsByUUID[r.UUID] = r + } + if r, ok := reportsByUUID[uuidA]; !ok { + t.Error("report for uuidA missing") + } else { + if r.Seq != 42 { + t.Errorf("report[uuidA].Seq=%d, want 42", r.Seq) + } + if len(r.Options) != 2 { + t.Errorf("report[uuidA].Options has %d entries, want 2", len(r.Options)) + } + } + if r, ok := reportsByUUID[uuidB]; !ok { + t.Error("report for uuidB missing") + } else if r.Seq != 43 { + t.Errorf("report[uuidB].Seq=%d, want 43", r.Seq) + } + + // The helper ran free twice internally; post-free state must be zeroed. + if !info.PostFreeZeroed { + t.Error("post-free state not zeroed") + } +} + +// TestCheckQueryWithInfoEmptyUUIDsReturnsAllPools pins that a caller passing +// zero UUIDs to daos_control_check_query receives every pool the server +// returned, not an empty pool list. The previous implementation silently +// dropped pools when the UUID filter was empty. +func TestControlC_CheckQueryWithInfoEmptyUUIDsReturnsAllPools(t *testing.T) { + uuidA := uuid.MustParse("10000000-0000-0000-0000-000000000001") + uuidB := uuid.MustParse("20000000-0000-0000-0000-000000000002") + + resp := &mgmtpb.CheckQueryResp{ + InsStatus: chk.CheckInstStatus_CIS_RUNNING, + InsPhase: chk.CheckScanPhase_CSP_POOL_MBS, + Pools: []*mgmtpb.CheckQueryPool{ + { + Uuid: uuidA.String(), + Status: chk.CheckPoolStatus_CPS_CHECKING, + Phase: chk.CheckScanPhase_CSP_POOL_MBS, + Time: &mgmtpb.CheckQueryTime{}, + }, + { + Uuid: uuidB.String(), + Status: chk.CheckPoolStatus_CPS_CHECKED, + Phase: chk.CheckScanPhase_CSP_DONE, + Time: &mgmtpb.CheckQueryTime{}, + }, + }, + } + + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, resp), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + info, rc := callCheckQueryWithInfo(handle, nil) + if rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + + if len(info.Pools) != 2 { + t.Fatalf("got %d pools with empty UUID filter, want 2", len(info.Pools)) + } + gotUUIDs := map[uuid.UUID]bool{} + for _, p := range info.Pools { + gotUUIDs[p.UUID] = true + if p.Status == "" || p.Phase == "" { + t.Errorf("pool %s: missing Status/Phase", p.UUID) + } + } + for _, want := range []uuid.UUID{uuidA, uuidB} { + if !gotUUIDs[want] { + t.Errorf("missing pool %s", want) + } + } +} + +func TestControlC_CheckRepair(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + seq uint64 + action uint32 + expRC int + }{ + "success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.CheckActResp{}), + }, + seq: 1, + action: 1, + expRC: 0, + }, + "failure": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.Nonexistent, + }, + seq: 1, + action: 1, + expRC: int(daos.Nonexistent), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + rc := callCheckRepair(handle, tc.seq, tc.action) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestControlC_CheckSetPolicy(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + flags uint32 + policies string + expRC int + }{ + "success - reset flag": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.DaosResp{}), + }, + flags: 1, // TCPF_RESET + expRC: 0, + }, + "success - with policies": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.DaosResp{}), + }, + policies: "CIC_POOL_BAD_LABEL:CIA_INTERACT", + expRC: 0, + }, + "failure - invalid policy format": { + mic: &control.MockInvokerConfig{}, + policies: "invalid", + expRC: int(daos.InvalidInput), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + rc := callCheckSetPolicy(handle, tc.flags, tc.policies) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +// TestCheckSetPolicyInteractiveFlag pins that the TCPF_INTERACT flag (1<<1) +// is forwarded on the request as AllInteractive=true. The sibling test +// covers TCPF_RESET but leaves this branch unexercised. +func TestControlC_CheckSetPolicyInteractiveFlag(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.DaosResp{}), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + const tcpfInteract = 1 << 1 + if rc := callCheckSetPolicy(handle, tcpfInteract, ""); rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + + if len(mi.SentReqs) != 1 { + t.Fatalf("SentReqs=%d, want 1", len(mi.SentReqs)) + } + req, ok := mi.SentReqs[0].(*control.SystemCheckSetPolicyReq) + if !ok { + t.Fatalf("SentReqs[0] type=%T, want *control.SystemCheckSetPolicyReq", mi.SentReqs[0]) + } + if !req.AllInteractive { + t.Fatal("AllInteractive=false, want true (TCPF_INTERACT should flip it)") + } + if req.ResetToDefaults { + t.Fatal("ResetToDefaults=true, want false (TCPF_RESET was not set)") + } +} + +func TestControlC_CheckOperationsInvalidHandle(t *testing.T) { + testUUID := uuid.MustParse("12345678-1234-1234-1234-123456789abc") + + tests := []struct { + name string + fn func() int + }{ + {"switch", func() int { return callCheckSwitch(0, true) }}, + {"start", func() int { return callCheckStart(0, 0, []uuid.UUID{testUUID}, "") }}, + {"stop", func() int { return callCheckStop(0, []uuid.UUID{testUUID}) }}, + {"query", func() int { return callCheckQuery(0, []uuid.UUID{testUUID}) }}, + {"repair", func() int { return callCheckRepair(0, 1, 1) }}, + {"set_policy", func() int { return callCheckSetPolicy(0, 0, "") }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rc := tt.fn() + if rc == 0 { + t.Fatalf("expected error for invalid handle on %s, got success", tt.name) + } + }) + } +} diff --git a/src/control/lib/control/c/check_test_helpers.go b/src/control/lib/control/c/check_test_helpers.go new file mode 100644 index 00000000000..9f13475d1f3 --- /dev/null +++ b/src/control/lib/control/c/check_test_helpers.go @@ -0,0 +1,170 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build test_stubs + +// cgo drivers for check_test.go (see test_helpers.go for why these can't +// live in the test file). + +package main + +/* +#include +#include +#include + +#include +*/ +import "C" +import ( + "runtime/cgo" + "unsafe" + + "github.com/google/uuid" +) + +func uuidArrayToC(uuids []uuid.UUID) []C.uuid_t { + if len(uuids) == 0 { + return nil + } + out := make([]C.uuid_t, len(uuids)) + for i, u := range uuids { + copyUUIDToC(u, &out[i]) + } + return out +} + +func callCheckSwitch(handle cgo.Handle, enable bool) int { + var e C.int + if enable { + e = 1 + } + return int(daos_control_check_switch(C.uintptr_t(handle), e)) +} + +func callCheckStart(handle cgo.Handle, flags uint32, poolUUIDs []uuid.UUID, policies string) int { + arr := uuidArrayToC(poolUUIDs) + var uuids *C.uuid_t + if len(arr) > 0 { + uuids = &arr[0] + } + cPol, f := cString(policies) + defer f() + return int(daos_control_check_start(C.uintptr_t(handle), C.uint32_t(flags), C.uint32_t(len(poolUUIDs)), uuids, cPol)) +} + +func callCheckStop(handle cgo.Handle, poolUUIDs []uuid.UUID) int { + arr := uuidArrayToC(poolUUIDs) + var uuids *C.uuid_t + if len(arr) > 0 { + uuids = &arr[0] + } + return int(daos_control_check_stop(C.uintptr_t(handle), C.uint32_t(len(poolUUIDs)), uuids)) +} + +func callCheckQuery(handle cgo.Handle, poolUUIDs []uuid.UUID) int { + arr := uuidArrayToC(poolUUIDs) + var uuids *C.uuid_t + if len(arr) > 0 { + uuids = &arr[0] + } + return int(daos_control_check_query(C.uintptr_t(handle), C.uint32_t(len(poolUUIDs)), uuids, nil)) +} + +func callCheckRepair(handle cgo.Handle, seq uint64, action uint32) int { + return int(daos_control_check_repair(C.uintptr_t(handle), C.uint64_t(seq), C.uint32_t(action))) +} + +func callCheckSetPolicy(handle cgo.Handle, flags uint32, policies string) int { + cPol, f := cString(policies) + defer f() + return int(daos_control_check_set_policy(C.uintptr_t(handle), C.uint32_t(flags), cPol)) +} + +type testCheckPoolInfo struct { + UUID uuid.UUID + Status string + Phase string +} + +type testCheckReportInfo struct { + UUID uuid.UUID + Seq uint64 + Class uint32 + Action uint32 + Result int + Options []int +} + +type testCheckInfo struct { + Status string + Phase string + Pools []testCheckPoolInfo + Reports []testCheckReportInfo + PostFreeZeroed bool +} + +// callCheckQueryWithInfo populates a daos_check_info, snapshots it into Go +// values, frees the C allocation, and asserts (via PostFreeZeroed) that the +// free zeroed out the struct's pointers and counters. +func callCheckQueryWithInfo(handle cgo.Handle, poolUUIDs []uuid.UUID) (testCheckInfo, int) { + arr := uuidArrayToC(poolUUIDs) + var uuids *C.uuid_t + if len(arr) > 0 { + uuids = &arr[0] + } + + var dci C.struct_daos_check_info + rc := int(daos_control_check_query(C.uintptr_t(handle), C.uint32_t(len(poolUUIDs)), uuids, &dci)) + + info := testCheckInfo{ + Status: goCString(dci.dci_status), + Phase: goCString(dci.dci_phase), + } + + if dci.dci_pools != nil && dci.dci_pool_nr > 0 { + pools := unsafe.Slice(dci.dci_pools, int(dci.dci_pool_nr)) + info.Pools = make([]testCheckPoolInfo, len(pools)) + for i := range pools { + info.Pools[i] = testCheckPoolInfo{ + UUID: uuidFromC(&pools[i].dcpi_uuid), + Status: goCString(pools[i].dcpi_status), + Phase: goCString(pools[i].dcpi_phase), + } + } + } + if dci.dci_reports != nil && dci.dci_report_nr > 0 { + reports := unsafe.Slice(dci.dci_reports, int(dci.dci_report_nr)) + info.Reports = make([]testCheckReportInfo, len(reports)) + for i := range reports { + r := testCheckReportInfo{ + UUID: uuidFromC(&reports[i].dcri_uuid), + Seq: uint64(reports[i].dcri_seq), + Class: uint32(reports[i].dcri_class), + Action: uint32(reports[i].dcri_act), + Result: int(reports[i].dcri_result), + } + nOpts := int(reports[i].dcri_option_nr) + r.Options = make([]int, nOpts) + for j := 0; j < nOpts; j++ { + r.Options[j] = int(reports[i].dcri_options[j]) + } + info.Reports[i] = r + } + } + + // Free once, assert post-free zeroed, free again (must be a no-op). + daos_control_check_info_free(&dci) + info.PostFreeZeroed = dci.dci_pool_nr == 0 && dci.dci_report_nr == 0 && + dci.dci_pools == nil && dci.dci_reports == nil && + dci.dci_status == nil && dci.dci_phase == nil + daos_control_check_info_free(&dci) + + return info, rc +} + +// callCheckInfoFreeNil exercises the nil-safe path of the free helper. +func callCheckInfoFreeNil() { daos_control_check_info_free(nil) } diff --git a/src/control/lib/control/c/convert_test.go b/src/control/lib/control/c/convert_test.go new file mode 100644 index 00000000000..f4b04fcecbc --- /dev/null +++ b/src/control/lib/control/c/convert_test.go @@ -0,0 +1,105 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "testing" + + "github.com/daos-stack/daos/src/control/lib/daos" +) + +func TestControlC_CopyStringToCharArray(t *testing.T) { + for name, tc := range map[string]struct { + input string + bufSize int + want string + }{ + "simple string": {"hello", 10, "hello"}, + "exact fit": {"hello", 6, "hello"}, + "truncated": {"hello world", 6, "hello"}, + "empty string": {"", 10, ""}, + "single char buffer": {"hello", 1, ""}, + } { + t.Run(name, func(t *testing.T) { + if got := testCopyStringToCharArray(tc.input, tc.bufSize); got != tc.want { + t.Fatalf("got %q, want %q", got, tc.want) + } + }) + } +} + +func TestControlC_CopyStringToCharArrayNilDest(t *testing.T) { + testCopyStringToNil() +} + +func TestControlC_PropsFromC(t *testing.T) { + t.Run("nil and empty", func(t *testing.T) { + if got, err := testPropsFromC(nil, true); err != nil || got != nil { + t.Fatalf("nil: got (%v, %v), want (nil, nil)", got, err) + } + if got, err := testPropsFromC(nil, false); err != nil || got != nil { + t.Fatalf("empty: got (%v, %v), want (nil, nil)", got, err) + } + }) + + t.Run("string, numeric, and enum-numeric mix", func(t *testing.T) { + got, err := testPropsFromC([]testPropEntry{ + testPropStr(testPropPoLabel, "mypool"), + testPropNum(testPropPoRedunFac, 2), + testPropNum(testPropPoScrubMode, 2), + }, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(got) != 3 { + t.Fatalf("got %d props, want 3", len(got)) + } + + if got[0].Name != "label" || got[0].Value.String() != "mypool" { + t.Errorf("props[0]=%s, want label:mypool", got[0]) + } + if got[1].Name != "rd_fac" { + t.Errorf("props[1].Name=%q, want rd_fac", got[1].Name) + } + if n, err := got[1].Value.GetNumber(); err != nil || n != 2 { + t.Errorf("rd_fac=(%d, %v), want 2", n, err) + } + if got[2].Name != "scrub" { + t.Errorf("props[2].Name=%q, want scrub", got[2].Name) + } + if n, err := got[2].Value.GetNumber(); err != nil || n != 2 { + t.Errorf("scrub=(%d, %v), want 2", n, err) + } + }) + + t.Run("ACL, owner, owner_group, svc_list skipped by propsFromC", func(t *testing.T) { + // These entry types are handled by poolCreateExtrasFromC, not the + // registry. propsFromC must skip them silently (not return an error). + // ACL and svc_list use val-pointer storage; owner/owner_group use str. + for _, e := range []testPropEntry{ + testPropNum(testPropPoACL, 0), + testPropStr(testPropPoOwner, "alice@"), + testPropStr(testPropPoOwnerGroup, "admins@"), + testPropNum(testPropPoSvcList, 0), + } { + got, err := testPropsFromC([]testPropEntry{e}, false) + if err != nil { + t.Errorf("propType=%d: unexpected error %v", e.Type, err) + } + if len(got) != 0 { + t.Errorf("propType=%d: got %d props, want 0 (skipped)", e.Type, len(got)) + } + } + }) + + t.Run("unknown property number rejected", func(t *testing.T) { + _, err := testPropsFromC([]testPropEntry{testPropNum(0xbad, 0)}, false) + if err != daos.NotSupported { + t.Errorf("err=%v, want daos.NotSupported", err) + } + }) +} diff --git a/src/control/lib/control/c/convert_test_helpers.go b/src/control/lib/control/c/convert_test_helpers.go new file mode 100644 index 00000000000..f62aaf8fb0b --- /dev/null +++ b/src/control/lib/control/c/convert_test_helpers.go @@ -0,0 +1,101 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build test_stubs + +// cgo drivers for convert_test.go (see test_helpers.go for why these can't +// live in the test file). + +package main + +/* +#include +#include +#include +#include +#include +*/ +import "C" +import ( + "unsafe" + + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/lib/ranklist" +) + +// testPropsFromC drives propsFromC end-to-end: builds a C prop from entries, +// converts it back through the library, and frees the C allocation. +// Passing nilInput=true runs propsFromC(nil). +func testPropsFromC(entries []testPropEntry, nilInput bool) ([]*daos.PoolProperty, error) { + var cProp *C.daos_prop_t + if !nilInput { + cProp = buildCProp(entries) + defer C.daos_prop_free(cProp) + } + return propsFromC(cProp) +} + +// testCopyStringToCharArray exercises copyStringToCharArray with a Go input +// and the given buffer size, returning the resulting Go string. +func testCopyStringToCharArray(s string, bufSize int) string { + if bufSize <= 0 { + copyStringToCharArray(s, nil, bufSize) + return "" + } + buf := make([]C.char, bufSize) + copyStringToCharArray(s, &buf[0], bufSize) + return goCString(&buf[0]) +} + +// testCopyStringToNil exercises the nil-dest fast path. +func testCopyStringToNil() { + copyStringToCharArray("hello", nil, 10) +} + +// testRankListRoundTrip builds a C rank list from the Go input, converts it +// back via rankListFromC, and returns the resulting slice. +func testRankListRoundTrip(ranks []uint32) []uint32 { + crl := C.d_rank_list_alloc(C.uint32_t(len(ranks))) + defer C.d_rank_list_free(crl) + if len(ranks) > 0 { + cRanks := unsafe.Slice(crl.rl_ranks, len(ranks)) + for i, r := range ranks { + cRanks[i] = C.d_rank_t(r) + } + } + got := rankListFromC(crl) + out := make([]uint32, len(got)) + for i, r := range got { + out[i] = uint32(r) + } + return out +} + +// testCopyRankListTo exercises copyRankListToC with the given input and +// destination capacity; returns the ranks written and rl_nr after the call. +func testCopyRankListTo(input []uint32, outCap int) (got []uint32, rlNr uint32) { + if outCap <= 0 { + return nil, 0 + } + dst := C.d_rank_list_alloc(C.uint32_t(outCap)) + defer C.d_rank_list_free(dst) + + goRanks := make([]ranklist.Rank, len(input)) + for i, r := range input { + goRanks[i] = ranklist.Rank(r) + } + copyRankListToC(goRanks, dst, outCap) + + rlNr = uint32(dst.rl_nr) + got = make([]uint32, rlNr) + if rlNr > 0 { + cRanks := unsafe.Slice(dst.rl_ranks, rlNr) + for i, r := range cRanks { + got[i] = uint32(r) + } + } + return got, rlNr +} diff --git a/src/control/lib/control/c/daos_control_util.h b/src/control/lib/control/c/daos_control_util.h new file mode 100644 index 00000000000..f34828c3101 --- /dev/null +++ b/src/control/lib/control/c/daos_control_util.h @@ -0,0 +1,42 @@ +/* + * (C) Copyright 2026 Hewlett Packard Enterprise Development LP + * + * SPDX-License-Identifier: BSD-2-Clause-Patent + */ +#ifndef __DAOS_CONTROL_C_UTIL_H__ +#define __DAOS_CONTROL_C_UTIL_H__ + +#include + +/* + * cgo is unable to work directly with unions, so we have + * to provide these glue helpers. + */ +static inline char * +get_dpe_str(struct daos_prop_entry *dpe) +{ + if (dpe == NULL) + return NULL; + + return dpe->dpe_str; +} + +static inline uint64_t +get_dpe_val(struct daos_prop_entry *dpe) +{ + if (dpe == NULL) + return 0; + + return dpe->dpe_val; +} + +static inline void * +get_dpe_val_ptr(struct daos_prop_entry *dpe) +{ + if (dpe == NULL) + return NULL; + + return dpe->dpe_val_ptr; +} + +#endif /* __DAOS_CONTROL_C_UTIL_H__ */ diff --git a/src/control/lib/control/c/errors_test.go b/src/control/lib/control/c/errors_test.go new file mode 100644 index 00000000000..72392e129ee --- /dev/null +++ b/src/control/lib/control/c/errors_test.go @@ -0,0 +1,232 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "testing" + + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/system" + "github.com/daos-stack/daos/src/control/system/checker" +) + +func TestControlC_StatusFromMsg(t *testing.T) { + for name, tc := range map[string]struct { + msg string + expSt daos.Status + expHit bool + }{ + "empty": {msg: "", expHit: false}, + "no parens": {msg: "something bad happened", expHit: false}, + "plain daos.Status": {msg: daos.NoSpace.Error(), expSt: daos.NoSpace, expHit: true}, + "wrapped prefix": {msg: "wrapper: " + daos.Busy.Error(), expSt: daos.Busy, expHit: true}, + "wrapped prefix+suffix": {msg: "oops: " + daos.Nonexistent.Error() + ": tail", expSt: daos.Nonexistent, expHit: true}, + "skip unrelated parens": { + msg: "failed rank(7): " + daos.TimedOut.Error(), + expSt: daos.TimedOut, + expHit: true, + }, + "negative parens without DER_ prefix rejected": { + msg: "failed after retry(-1): something else", + expHit: false, + }, + "DER_-prefix anchored through wrapper": { + msg: "failed after retry(-1): " + daos.InvalidInput.Error(), + expSt: daos.InvalidInput, + expHit: true, + }, + "positive numbers rejected": {msg: "something (42)", expHit: false}, + "non-numeric rejected": {msg: "something (oops)", expHit: false}, + } { + t.Run(name, func(t *testing.T) { + st, ok := statusFromMsg(tc.msg) + if ok != tc.expHit { + t.Fatalf("hit=%v, want %v", ok, tc.expHit) + } + if ok && st != tc.expSt { + t.Fatalf("status=%d (%s), want %d (%s)", st, st, tc.expSt, tc.expSt) + } + }) + } +} + +func TestControlC_UidGidLookupErrorsAreInvalidInput(t *testing.T) { + const nonexistent = uint32(0xFFFFFFFE) + + _, err := uidToUsername(nonexistent) + if err == nil { + t.Fatalf("uidToUsername(%d) unexpectedly succeeded — host has this UID", nonexistent) + } + if got := errorToRC(err); got != int(daos.InvalidInput) { + t.Fatalf("uidToUsername rc=%d, want InvalidInput(%d)", got, int(daos.InvalidInput)) + } + + _, err = gidToGroupname(nonexistent) + if err == nil { + t.Fatalf("gidToGroupname(%d) unexpectedly succeeded — host has this GID", nonexistent) + } + if got := errorToRC(err); got != int(daos.InvalidInput) { + t.Fatalf("gidToGroupname rc=%d, want InvalidInput(%d)", got, int(daos.InvalidInput)) + } +} + +func TestControlC_ErrorToRC(t *testing.T) { + for name, tc := range map[string]struct { + err error + expRC int + }{ + "nil error": { + err: nil, + expRC: 0, + }, + "pool service not found, no embedded code": { + err: fmt.Errorf("pool destroy failed: %s", system.ErrPoolUUIDNotFound(uuid.New())), + expRC: int(daos.Nonexistent), + }, + "checker enabled blocks normal ops": { + err: fmt.Errorf("pool create failed: %s", checker.FaultCheckerEnabled), + expRC: int(daos.Busy), + }, + "daos.Status - NoSpace": { + err: daos.NoSpace, + expRC: int(daos.NoSpace), + }, + "wrapped daos.Status (string-embedded)": { + err: errors.New("wrapper: " + daos.NoSpace.Error()), + expRC: int(daos.NoSpace), + }, + "errInvalidHandle": { + err: errInvalidHandle, + expRC: int(daos.InvalidInput), + }, + "control.ErrNoConfigFile": { + err: control.ErrNoConfigFile, + expRC: int(daos.BadPath), + }, + "os.ErrNotExist": { + err: os.ErrNotExist, + expRC: int(daos.Nonexistent), + }, + "context.DeadlineExceeded": { + err: context.DeadlineExceeded, + expRC: int(daos.TimedOut), + }, + "unknown error": { + err: errors.New("some unknown error"), + expRC: int(daos.MiscError), + }, + "gRPC Unavailable": { + err: status.Error(codes.Unavailable, "transport unavailable"), + expRC: int(daos.Unreachable), + }, + "gRPC DeadlineExceeded": { + err: status.Error(codes.DeadlineExceeded, "deadline"), + expRC: int(daos.TimedOut), + }, + "gRPC PermissionDenied": { + err: status.Error(codes.PermissionDenied, "no"), + expRC: int(daos.NoPermission), + }, + "gRPC Unauthenticated": { + err: status.Error(codes.Unauthenticated, "no"), + expRC: int(daos.NoPermission), + }, + "gRPC NotFound": { + err: status.Error(codes.NotFound, "missing"), + expRC: int(daos.Nonexistent), + }, + "gRPC AlreadyExists": { + err: status.Error(codes.AlreadyExists, "exists"), + expRC: int(daos.Exists), + }, + "gRPC ResourceExhausted": { + err: status.Error(codes.ResourceExhausted, "full"), + expRC: int(daos.NoSpace), + }, + "gRPC InvalidArgument": { + err: status.Error(codes.InvalidArgument, "bad"), + expRC: int(daos.InvalidInput), + }, + "gRPC Unimplemented": { + err: status.Error(codes.Unimplemented, "nope"), + expRC: int(daos.NotImpl), + }, + "gRPC Aborted": { + err: status.Error(codes.Aborted, "again"), + expRC: int(daos.TryAgain), + }, + "gRPC Unknown falls through to MiscError": { + err: status.Error(codes.Unknown, "unclassified"), + expRC: int(daos.MiscError), + }, + "embedded DER_ wins over gRPC code": { + // Server-side DAOS errors come back through gRPC; the explicit + // DER_ token in the message must take precedence over the + // transport-class fallback. + err: status.Error(codes.Unavailable, "wrapper: "+daos.Busy.Error()), + expRC: int(daos.Busy), + }, + } { + t.Run(name, func(t *testing.T) { + got := errorToRC(tc.err) + if got != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, got) + } + }) + } +} + +func TestControlC_SummarizeRPCErr(t *testing.T) { + // gRPC formats embedded HTTP responses with %q, so \r\n appears as the + // literal 4-char sequence in the error message. + zscaler := `rpc error: code = Unavailable desc = connection error: ` + + `desc = "transport: error while dialing: failed to do connect handshake, ` + + `response: \"HTTP/1.0 502 Bad Gateway\r\nConnection: close\r\n` + + `Content-Type: text/html\r\nServer: Zscaler/6.2\r\n\r\n...body...\""` + + for name, tc := range map[string]struct { + err error + wantSub []string // substrings the output must contain + wantNo []string // substrings the output must NOT contain + }{ + "nil err": {err: nil, wantSub: []string{}}, + "plain string": {err: errors.New("plain"), wantSub: []string{"plain"}}, + "zscaler 502 sanitized": { + err: errors.New(zscaler), + wantSub: []string{"502 Bad Gateway", "Server: Zscaler/6.2"}, + wantNo: []string{"", "Content-Type"}, + }, + "no http response untouched": { + err: errors.New("rpc error: code = NotFound desc = pool not found"), + wantSub: []string{"pool not found"}, + }, + } { + t.Run(name, func(t *testing.T) { + got := summarizeRPCErr(tc.err) + for _, s := range tc.wantSub { + if !strings.Contains(got, s) { + t.Errorf("output %q missing substring %q", got, s) + } + } + for _, s := range tc.wantNo { + if strings.Contains(got, s) { + t.Errorf("output %q unexpectedly contains %q", got, s) + } + } + }) + } +} diff --git a/src/control/lib/control/c/fi.go b/src/control/lib/control/c/fi.go new file mode 100644 index 00000000000..c56a1148d15 --- /dev/null +++ b/src/control/lib/control/c/fi.go @@ -0,0 +1,55 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build fault_injection + +package main + +/* +#include +#include +#include +*/ +import "C" +import ( + "context" + + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" + + chkpb "github.com/daos-stack/daos/src/control/common/proto/chk" + mgmtpb "github.com/daos-stack/daos/src/control/common/proto/mgmt" + "github.com/daos-stack/daos/src/control/lib/control" +) + +func parseFaultClass(name string) (chkpb.CheckInconsistClass, error) { + var cls control.SystemCheckFindingClass + if err := cls.FromString(name); err != nil { + return chkpb.CheckInconsistClass_CIC_NONE, err + } + return chkpb.CheckInconsistClass(cls), nil +} + +//export daos_control_fault_inject +func daos_control_fault_inject(handle C.uintptr_t, poolUUID *C.uuid_t, mgmtSvc C.int, fault *C.char) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + cls, err := parseFaultClass(goString(fault)) + if err != nil { + return err + } + goUUID := uuidFromC(poolUUID).String() + payload := &chkpb.Fault{Class: cls, Strings: []string{goUUID}} + + rpcFn := func(rpcCtx context.Context, conn *grpc.ClientConn) (proto.Message, error) { + if mgmtSvc != 0 { + return mgmtpb.NewMgmtSvcClient(conn).FaultInjectMgmtPoolFault(rpcCtx, payload) + } + return mgmtpb.NewMgmtSvcClient(conn).FaultInjectPoolFault(rpcCtx, payload) + } + _, err = control.InvokeFaultRPC(ctx.ctx(), ctx.client, rpcFn) + return err + }) +} diff --git a/src/control/lib/control/c/fi_test.go b/src/control/lib/control/c/fi_test.go new file mode 100644 index 00000000000..f728065b457 --- /dev/null +++ b/src/control/lib/control/c/fi_test.go @@ -0,0 +1,63 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build fault_injection + +package main + +import ( + "testing" + + chkpb "github.com/daos-stack/daos/src/control/common/proto/chk" + "github.com/daos-stack/daos/src/control/lib/daos" +) + +func TestControlC_ParseFaultClass(t *testing.T) { + for name, tc := range map[string]struct { + input string + expectErr bool + expectNot chkpb.CheckInconsistClass + }{ + "empty rejected": { + input: "", + expectErr: true, + }, + "garbage rejected": { + input: "not_a_real_class", + expectErr: true, + }, + "canonical name accepted": { + input: "POOL_LESS_SVC_WITHOUT_QUORUM", + expectNot: chkpb.CheckInconsistClass_CIC_NONE, + }, + } { + t.Run(name, func(t *testing.T) { + got, err := parseFaultClass(tc.input) + if tc.expectErr { + if err == nil { + t.Fatalf("expected error, got class=%v", got) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got == tc.expectNot { + t.Fatalf("class=%v, want something other than %v", got, tc.expectNot) + } + }) + } +} + +func TestControlC_FaultInjectInvalidHandle(t *testing.T) { + rc := callFaultInject(0, "pool-less-svc-without-quorum", false) + if rc == 0 { + t.Fatalf("expected non-zero rc for invalid handle") + } + if rc == int(daos.Success) { + t.Fatalf("expected error rc, got Success") + } +} diff --git a/src/control/lib/control/c/fi_test_helpers.go b/src/control/lib/control/c/fi_test_helpers.go new file mode 100644 index 00000000000..eb11060b2a1 --- /dev/null +++ b/src/control/lib/control/c/fi_test_helpers.go @@ -0,0 +1,31 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build fault_injection && test_stubs + +package main + +/* +#include +#include +#include +*/ +import "C" +import ( + "runtime/cgo" + "unsafe" +) + +func callFaultInject(handle cgo.Handle, fault string, mgmtSvc bool) int { + cFault := C.CString(fault) + defer C.free(unsafe.Pointer(cFault)) + var poolUUID C.uuid_t + var ms C.int + if mgmtSvc { + ms = 1 + } + return int(daos_control_fault_inject(C.uintptr_t(handle), &poolUUID, ms, cFault)) +} diff --git a/src/control/lib/control/c/no_fi.go b/src/control/lib/control/c/no_fi.go new file mode 100644 index 00000000000..289a89c7429 --- /dev/null +++ b/src/control/lib/control/c/no_fi.go @@ -0,0 +1,30 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build !fault_injection + +package main + +/* +#include +#include +#include +*/ +import "C" + +import ( + "github.com/daos-stack/daos/src/control/lib/daos" +) + +//export daos_control_fault_inject +func daos_control_fault_inject( + handle C.uintptr_t, + poolUUID *C.uuid_t, + mgmtSvc C.int, + fault *C.char, +) C.int { + return C.int(daos.NotSupported) +} diff --git a/src/control/lib/control/c/pool.go b/src/control/lib/control/c/pool.go new file mode 100644 index 00000000000..70c752e6dac --- /dev/null +++ b/src/control/lib/control/c/pool.go @@ -0,0 +1,442 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +/* +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "daos_control_util.h" +*/ +import "C" +import ( + "context" + "fmt" + "unsafe" + + "github.com/google/uuid" + + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/lib/ranklist" +) + +// freePartialPoolList releases C memory in poolsSlice[0..n-1], matching +// daos_control_pool_list_free for the partial-population rollback path. +func freePartialPoolList(poolsSlice []C.daos_mgmt_pool_info_t, n int) { + for i := 0; i < n; i++ { + if poolsSlice[i].mgpi_label != nil { + C.free(unsafe.Pointer(poolsSlice[i].mgpi_label)) + poolsSlice[i].mgpi_label = nil + } + if poolsSlice[i].mgpi_svc != nil { + C.d_rank_list_free(poolsSlice[i].mgpi_svc) + poolsSlice[i].mgpi_svc = nil + } + } +} + +// validatePoolCreateArgs rejects malformed input before the RPC: nil args, +// nil pool_uuid_out (no output target), a non-NULL *svc_out (would leak), +// and the both-sizes-zero case. Errors wrap daos.InvalidInput so errorToRC +// collapses them to a single rc. +func validatePoolCreateArgs(args *C.struct_daos_control_pool_create_args, svcOut **C.d_rank_list_t, poolUUIDOut *C.uuid_t) error { + if args == nil { + return fmt.Errorf("args is nil: %w", daos.InvalidInput) + } + if poolUUIDOut == nil { + return fmt.Errorf("pool_uuid_out is nil: %w", daos.InvalidInput) + } + if svcOut != nil && *svcOut != nil { + return fmt.Errorf("*svc_out must be NULL on input: %w", daos.InvalidInput) + } + if args.dcpa_scm_size == 0 && args.dcpa_nvme_size == 0 { + return fmt.Errorf("scm_size and nvme_size are both zero: %w", daos.InvalidInput) + } + return nil +} + +// newPoolRanksReq creates a PoolRanksReq for single-rank operations +// that optionally include a target index. +func newPoolRanksReq(poolUUID *C.uuid_t, rank C.d_rank_t, tgtIdx C.int) *control.PoolRanksReq { + req := &control.PoolRanksReq{ + ID: poolIDFromC(poolUUID), + Ranks: []ranklist.Rank{ranklist.Rank(rank)}, + } + if tgtIdx >= 0 { + req.TargetIdx = []uint32{uint32(tgtIdx)} + } + return req +} + +// daos_control_pool_create creates a pool described by args. On success the +// new pool's UUID is written to poolUUIDOut, and if svcOut is non-NULL, +// *svcOut receives the service replica list (allocated with d_rank_list_alloc; +// caller releases with d_rank_list_free). *svcOut must be NULL on input and +// remains NULL if the server returns no replicas. +// +//export daos_control_pool_create +func daos_control_pool_create(handle C.uintptr_t, args *C.struct_daos_control_pool_create_args, svcOut **C.d_rank_list_t, poolUUIDOut *C.uuid_t) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + if err := validatePoolCreateArgs(args, svcOut, poolUUIDOut); err != nil { + ctx.log.Errorf("PoolCreate: %s", err) + return err + } + + goProps, err := propsFromC(args.dcpa_prop) + if err != nil { + return err + } + + userName, err := uidToUsername(uint32(args.dcpa_uid)) + if err != nil { + ctx.log.Errorf("PoolCreate: %s", err) + return err + } + groupName, err := gidToGroupname(uint32(args.dcpa_gid)) + if err != nil { + ctx.log.Errorf("PoolCreate: %s", err) + return err + } + + req := &control.PoolCreateReq{ + User: userName, + UserGroup: groupName, + NumSvcReps: uint32(args.dcpa_nsvc), + } + req.SetSystem(goString(args.dcpa_grp)) + + if goTgts := rankListFromC(args.dcpa_tgts); len(goTgts) > 0 { + req.Ranks = goTgts + } + + req.TierBytes = []uint64{uint64(args.dcpa_scm_size), uint64(args.dcpa_nvme_size)} + req.Properties = goProps + + // ACL and owner/owner-group entries are handled outside the + // property registry; apply them after the uid/gid defaults so + // that prop entries win when present. + if err := poolCreateExtrasFromC(args.dcpa_prop, req); err != nil { + ctx.log.Errorf("PoolCreate: %s", err) + return err + } + + resp, err := control.PoolCreate(ctx.ctx(), ctx.client, req) + if err != nil { + ctx.log.Errorf("PoolCreate failed: %s", summarizeRPCErr(err)) + return err + } + + respUUID, err := uuid.Parse(resp.UUID) + if err != nil { + return err + } + copyUUIDToC(respUUID, poolUUIDOut) + + if svcOut != nil && len(resp.SvcReps) > 0 { + svc := C.d_rank_list_alloc(C.uint32_t(len(resp.SvcReps))) + if svc == nil { + return daos.NoMemory + } + svcRanks := make([]ranklist.Rank, len(resp.SvcReps)) + for i, r := range resp.SvcReps { + svcRanks[i] = ranklist.Rank(r) + } + copyRankListToC(svcRanks, svc, len(resp.SvcReps)) + *svcOut = svc + } + + return nil + }) +} + +//export daos_control_pool_destroy +func daos_control_pool_destroy(handle C.uintptr_t, poolUUID *C.uuid_t, grp *C.char, force C.int) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.PoolDestroyReq{ + ID: poolIDFromC(poolUUID), + Recursive: true, + Force: force != 0, + } + req.SetSystem(goString(grp)) + return control.PoolDestroy(ctx.ctx(), ctx.client, req) + }) +} + +//export daos_control_pool_evict +func daos_control_pool_evict(handle C.uintptr_t, poolUUID *C.uuid_t, grp *C.char) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.PoolEvictReq{ID: poolIDFromC(poolUUID)} + req.SetSystem(goString(grp)) + return control.PoolEvict(ctx.ctx(), ctx.client, req) + }) +} + +// poolRanksOp is the shared pool_exclude/reintegrate/drain body: fires a +// PoolRanks-style RPC and returns the first errored result (or nil). +func poolRanksOp( + ctx *ctrlContext, + poolUUID *C.uuid_t, grp *C.char, rank C.d_rank_t, tgtIdx C.int, + rpc func(context.Context, control.UnaryInvoker, *control.PoolRanksReq) (*control.PoolRanksResp, error), +) error { + if err := requireRank(rank); err != nil { + return err + } + + req := newPoolRanksReq(poolUUID, rank, tgtIdx) + req.SetSystem(goString(grp)) + resp, err := rpc(ctx.ctx(), ctx.client, req) + if err != nil { + return err + } + return firstRankStatus(resp.Results) +} + +//export daos_control_pool_exclude +func daos_control_pool_exclude(handle C.uintptr_t, poolUUID *C.uuid_t, grp *C.char, rank C.d_rank_t, tgtIdx C.int) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + return poolRanksOp(ctx, poolUUID, grp, rank, tgtIdx, control.PoolExclude) + }) +} + +//export daos_control_pool_reintegrate +func daos_control_pool_reintegrate(handle C.uintptr_t, poolUUID *C.uuid_t, grp *C.char, rank C.d_rank_t, tgtIdx C.int) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + return poolRanksOp(ctx, poolUUID, grp, rank, tgtIdx, control.PoolReintegrate) + }) +} + +//export daos_control_pool_drain +func daos_control_pool_drain(handle C.uintptr_t, poolUUID *C.uuid_t, grp *C.char, rank C.d_rank_t, tgtIdx C.int) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + return poolRanksOp(ctx, poolUUID, grp, rank, tgtIdx, control.PoolDrain) + }) +} + +//export daos_control_pool_extend +func daos_control_pool_extend(handle C.uintptr_t, poolUUID *C.uuid_t, grp *C.char, ranks *C.d_rank_t, ranksNr C.int) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + var goRanks []ranklist.Rank + if ranks != nil && ranksNr > 0 { + cRanks := unsafe.Slice(ranks, ranksNr) + goRanks = make([]ranklist.Rank, ranksNr) + for i, r := range cRanks { + goRanks[i] = ranklist.Rank(r) + } + } + + req := &control.PoolExtendReq{ + ID: poolIDFromC(poolUUID), + Ranks: goRanks, + } + req.SetSystem(goString(grp)) + return control.PoolExtend(ctx.ctx(), ctx.client, req) + }) +} + +//export daos_control_pool_list +func daos_control_pool_list(handle C.uintptr_t, grp *C.char, npools *C.daos_size_t, pools *C.daos_mgmt_pool_info_t) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + if npools == nil { + return daos.InvalidInput + } + + req := &control.ListPoolsReq{NoQuery: true} + req.SetSystem(goString(grp)) + + resp, err := control.ListPools(ctx.ctx(), ctx.client, req) + if err != nil { + *npools = 0 + return err + } + + numPools := C.daos_size_t(len(resp.Pools)) + + // Count-only mode: caller passes nil pools to query the required size. + if pools == nil { + *npools = numPools + return nil + } + + // Return the required count either way so the caller can grow its + // buffer and retry. + npoolsIn := *npools + *npools = numPools + if npoolsIn < numPools { + return daos.BufTooSmall + } + + // Zero-init so conditionally-set pointer fields (mgpi_label, mgpi_svc) are NULL. + C.memset(unsafe.Pointer(pools), 0, + C.size_t(numPools)*C.size_t(unsafe.Sizeof(*pools))) + poolsSlice := unsafe.Slice(pools, numPools) + + for i, p := range resp.Pools { + copyUUIDToC(p.UUID, &poolsSlice[i].mgpi_uuid) + + if p.Label != "" { + poolsSlice[i].mgpi_label = C.CString(p.Label) + } + + poolsSlice[i].mgpi_ldr = C.d_rank_t(p.ServiceLeader) + + // On alloc failure, free prior entries and reset *npools = 0 so the + // caller need not call pool_list_free. + if len(p.ServiceReplicas) > 0 { + poolsSlice[i].mgpi_svc = C.d_rank_list_alloc(C.uint32_t(len(p.ServiceReplicas))) + if poolsSlice[i].mgpi_svc == nil { + freePartialPoolList(poolsSlice, i+1) + *npools = 0 + return daos.NoMemory + } + copyRankListToC(p.ServiceReplicas, poolsSlice[i].mgpi_svc, len(p.ServiceReplicas)) + } + } + + *npools = numPools + return nil + }) +} + +//export daos_control_pool_list_free +func daos_control_pool_list_free( + pools *C.daos_mgmt_pool_info_t, + npools C.daos_size_t, +) { + defer recoverExportVoid() + + if pools == nil || npools == 0 { + return + } + + poolsSlice := unsafe.Slice(pools, npools) + for i := range poolsSlice { + if poolsSlice[i].mgpi_label != nil { + C.free(unsafe.Pointer(poolsSlice[i].mgpi_label)) + poolsSlice[i].mgpi_label = nil + } + if poolsSlice[i].mgpi_svc != nil { + C.d_rank_list_free(poolsSlice[i].mgpi_svc) + poolsSlice[i].mgpi_svc = nil + } + } +} + +// resolvePoolID returns label if non-empty, otherwise the UUID string. +// label takes precedence to mirror dmg's handling of either identifier. +func resolvePoolID(label *C.char, poolUUID *C.uuid_t) string { + if id := goString(label); id != "" { + return id + } + return poolIDFromC(poolUUID) +} + +//export daos_control_pool_set_prop +func daos_control_pool_set_prop(handle C.uintptr_t, label *C.char, poolUUID *C.uuid_t, propName, propValue *C.char) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + handler, ok := daos.PoolProperties()[goString(propName)] + if !ok { + return daos.InvalidInput + } + prop := handler.GetProperty(goString(propName)) + if err := prop.SetValue(goString(propValue)); err != nil { + return err + } + req := &control.PoolSetPropReq{ + ID: resolvePoolID(label, poolUUID), + Properties: []*daos.PoolProperty{prop}, + } + return control.PoolSetProp(ctx.ctx(), ctx.client, req) + }) +} + +//export daos_control_pool_get_prop +func daos_control_pool_get_prop(handle C.uintptr_t, label *C.char, poolUUID *C.uuid_t, propName *C.char, propValue **C.char) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + goName := goString(propName) + handler, ok := daos.PoolProperties()[goName] + if !ok { + return daos.InvalidInput + } + req := &control.PoolGetPropReq{ + ID: resolvePoolID(label, poolUUID), + Properties: []*daos.PoolProperty{handler.GetProperty(goName)}, + } + props, err := control.PoolGetProp(ctx.ctx(), ctx.client, req) + if err != nil { + return err + } + if len(props) == 0 || !props[0].Value.IsSet() { + return daos.Nonexistent + } + // C.CString allocates; caller must free. StringValue() renders enum + // values by name (e.g. "timed" instead of "2" for scrub). + if propValue != nil { + *propValue = C.CString(props[0].StringValue()) + } + return nil + }) +} + +//export daos_control_pool_update_ace +func daos_control_pool_update_ace(handle C.uintptr_t, poolUUID *C.uuid_t, grp, ace *C.char) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.PoolUpdateACLReq{ + ID: poolIDFromC(poolUUID), + ACL: &control.AccessControlList{Entries: []string{goString(ace)}}, + } + req.SetSystem(goString(grp)) + _, err := control.PoolUpdateACL(ctx.ctx(), ctx.client, req) + return err + }) +} + +//export daos_control_pool_delete_ace +func daos_control_pool_delete_ace(handle C.uintptr_t, poolUUID *C.uuid_t, grp, principal *C.char) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.PoolDeleteACLReq{ + ID: poolIDFromC(poolUUID), + Principal: goString(principal), + } + req.SetSystem(goString(grp)) + _, err := control.PoolDeleteACL(ctx.ctx(), ctx.client, req) + return err + }) +} + +//export daos_control_pool_rebuild_stop +func daos_control_pool_rebuild_stop(handle C.uintptr_t, poolUUID *C.uuid_t, grp *C.char, force C.int) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.PoolRebuildManageReq{ + ID: poolIDFromC(poolUUID), + OpCode: control.PoolRebuildOpCodeStop, + Force: force != 0, + } + req.SetSystem(goString(grp)) + return control.PoolRebuildManage(ctx.ctx(), ctx.client, req) + }) +} + +//export daos_control_pool_rebuild_start +func daos_control_pool_rebuild_start(handle C.uintptr_t, poolUUID *C.uuid_t, grp *C.char) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.PoolRebuildManageReq{ + ID: poolIDFromC(poolUUID), + OpCode: control.PoolRebuildOpCodeStart, + } + req.SetSystem(goString(grp)) + return control.PoolRebuildManage(ctx.ctx(), ctx.client, req) + }) +} diff --git a/src/control/lib/control/c/pool_test.go b/src/control/lib/control/c/pool_test.go new file mode 100644 index 00000000000..141967b78cd --- /dev/null +++ b/src/control/lib/control/c/pool_test.go @@ -0,0 +1,827 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "os" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + + mgmtpb "github.com/daos-stack/daos/src/control/common/proto/mgmt" + "github.com/daos-stack/daos/src/control/common/test" + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/logging" +) + +const testPoolUUID = "12345678-1234-1234-1234-123456789abc" + +func TestControlC_PoolCreate(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + expRC int + expSvcRanks []uint32 + }{ + "success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolCreateResp{ + SvcReps: []uint32{0, 1, 2}, + }), + }, + expSvcRanks: []uint32{0, 1, 2}, + }, + "failure - no space": {mic: &control.MockInvokerConfig{UnaryError: daos.NoSpace}, expRC: int(daos.NoSpace)}, + "failure - permission denied": {mic: &control.MockInvokerConfig{UnaryError: daos.NoPermission}, expRC: int(daos.NoPermission)}, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + res := callPoolCreate(handle, uint32(os.Getuid()), uint32(os.Getgid()), 1<<30, 0, 3) + if res.rc != tc.expRC { + t.Fatalf("rc=%d, want %d", res.rc, tc.expRC) + } + if tc.expRC != 0 { + return + } + if diff := cmp.Diff(tc.expSvcRanks, res.svcRanks); diff != "" { + t.Fatalf("svc ranks mismatch (-want +got):\n%s", diff) + } + }) + } +} + +// TestPoolCreateServerPicksSvcCount passes nsvc=0 and verifies the library +// surfaces whatever replica set the server chose. +func TestControlC_PoolCreateServerPicksSvcCount(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolCreateResp{ + SvcReps: []uint32{0, 1, 2}, + }), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + res := callPoolCreate(handle, uint32(os.Getuid()), uint32(os.Getgid()), 1<<30, 0, 0) + if res.rc != 0 { + t.Fatalf("rc=%d, want 0", res.rc) + } + if diff := cmp.Diff([]uint32{0, 1, 2}, res.svcRanks); diff != "" { + t.Fatalf("svc ranks mismatch (-want +got):\n%s", diff) + } + + req := mi.SentReqs[0].(*control.PoolCreateReq) + if req.NumSvcReps != 0 { + t.Fatalf("NumSvcReps=%d, want 0 (nsvc=0 means server picks)", req.NumSvcReps) + } +} + +func TestControlC_PoolCreateWithProps(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolCreateResp{ + SvcReps: []uint32{0}, + }), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + entries := []testPropEntry{ + testPropStr(testPropPoLabel, "my-pool"), + testPropNum(testPropPoRedunFac, 1), + } + res := callPoolCreateWithProp(handle, uint32(os.Getuid()), uint32(os.Getgid()), 1<<30, 0, 1, entries) + if res.rc != 0 { + t.Fatalf("rc=%d, want 0", res.rc) + } + + req := mi.SentReqs[0].(*control.PoolCreateReq) + if len(req.Properties) != 2 { + t.Fatalf("Properties=%d, want 2", len(req.Properties)) + } + byName := map[string]*daos.PoolProperty{} + for _, p := range req.Properties { + byName[p.Name] = p + } + if byName["label"].Value.String() != "my-pool" { + t.Errorf("label=%q, want my-pool", byName["label"].Value.String()) + } + if n, _ := byName["rd_fac"].Value.GetNumber(); n != 1 { + t.Errorf("rd_fac=%d, want 1", n) + } +} + +func TestControlC_PoolCreateInvalidHandle(t *testing.T) { + if rc := callPoolCreateInvalidHandle(); rc == 0 { + t.Fatal("expected error for invalid handle, got success") + } +} + +func TestControlC_RankListConversion(t *testing.T) { + for name, tc := range map[string]struct{ ranks []uint32 }{ + "empty": {nil}, + "single rank": {[]uint32{5}}, + "multiple ranks": {[]uint32{0, 1, 2, 3, 4}}, + } { + t.Run(name, func(t *testing.T) { + got := testRankListRoundTrip(tc.ranks) + if len(tc.ranks) == 0 { + if len(got) != 0 { + t.Fatalf("got %v, want empty", got) + } + return + } + if diff := cmp.Diff(tc.ranks, got); diff != "" { + t.Fatalf("round-trip mismatch (-want +got):\n%s", diff) + } + + out, nr := testCopyRankListTo(tc.ranks, len(tc.ranks)) + if nr != uint32(len(tc.ranks)) { + t.Fatalf("rl_nr=%d, want %d", nr, len(tc.ranks)) + } + if diff := cmp.Diff(tc.ranks, out); diff != "" { + t.Fatalf("copy-back mismatch (-want +got):\n%s", diff) + } + }) + } +} + +// TestCopyRankListToCTruncation pins that copyRankListToC respects the +// caller's output capacity and does not overrun when input > cap. +func TestControlC_CopyRankListToCTruncation(t *testing.T) { + input := []uint32{10, 20, 30, 40, 50} + got, nr := testCopyRankListTo(input, 2) + if nr != 2 { + t.Fatalf("rl_nr=%d, want 2", nr) + } + if diff := cmp.Diff([]uint32{10, 20}, got); diff != "" { + t.Fatalf("mismatch (-want +got):\n%s", diff) + } +} + +func TestControlC_PoolDestroy(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + force bool + expRC int + }{ + "success": {mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolDestroyResp{})}}, + "success with force": {mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolDestroyResp{})}, force: true}, + "failure - pool not found": {mic: &control.MockInvokerConfig{UnaryError: daos.Nonexistent}, expRC: int(daos.Nonexistent)}, + "failure - busy": {mic: &control.MockInvokerConfig{UnaryError: daos.Busy}, expRC: int(daos.Busy)}, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callPoolDestroy(handle, uuid.MustParse(testPoolUUID), tc.force); rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + }) + } +} + +func TestControlC_PoolEvict(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + expRC int + }{ + "success": {mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolEvictResp{})}}, + "failure - pool not found": {mic: &control.MockInvokerConfig{UnaryError: daos.Nonexistent}, expRC: int(daos.Nonexistent)}, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callPoolEvict(handle, uuid.MustParse(testPoolUUID)); rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + }) + } +} + +func TestControlC_PoolExclude(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + rank uint32 + tgtIdx int + expRC int + }{ + "success - rank only": { + mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolExcludeResp{})}, + tgtIdx: -1, + }, + "success - rank and target": { + mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolExcludeResp{})}, + rank: 1, + tgtIdx: 2, + }, + "failure - response error": { + mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolExcludeResp{ + Status: int32(daos.Nonexistent), + })}, + tgtIdx: -1, + expRC: int(daos.Nonexistent), + }, + "CRT_NO_RANK rejected": { + mic: &control.MockInvokerConfig{}, + rank: 0xffffffff, + tgtIdx: -1, + expRC: int(daos.InvalidInput), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callPoolExclude(handle, uuid.MustParse(testPoolUUID), tc.rank, tc.tgtIdx); rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + }) + } +} + +func TestControlC_PoolDrain(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + expRC int + }{ + "success": {mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolDrainResp{})}}, + "failure - response error": { + mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolDrainResp{ + Status: int32(daos.Busy), + })}, + expRC: int(daos.Busy), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callPoolDrain(handle, uuid.MustParse(testPoolUUID), 0, -1); rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + }) + } +} + +func TestControlC_PoolReintegrate(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + expRC int + }{ + "success": {mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolReintResp{})}}, + "failure - response error": { + mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolReintResp{ + Status: int32(daos.Busy), + })}, + expRC: int(daos.Busy), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callPoolReintegrate(handle, uuid.MustParse(testPoolUUID), 0, -1); rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + }) + } +} + +func TestControlC_PoolOperationsInvalidHandle(t *testing.T) { + u := uuid.MustParse(testPoolUUID) + tests := []struct { + name string + fn func() int + }{ + {"destroy", func() int { return callPoolDestroy(0, u, false) }}, + {"evict", func() int { return callPoolEvict(0, u) }}, + {"exclude", func() int { return callPoolExclude(0, u, 0, -1) }}, + {"drain", func() int { return callPoolDrain(0, u, 0, -1) }}, + {"reintegrate", func() int { return callPoolReintegrate(0, u, 0, -1) }}, + {"extend", func() int { return callPoolExtend(0, u, []uint32{1, 2}) }}, + {"set_prop", func() int { return callPoolSetProp(0, "", u, "label", "test") }}, + {"get_prop", func() int { _, rc := callPoolGetProp(0, "", u, "label"); return rc }}, + {"update_ace", func() int { return callPoolUpdateACE(0, u, "A::user@:rw") }}, + {"delete_ace", func() int { return callPoolDeleteACE(0, u, "user@") }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if rc := tt.fn(); rc == 0 { + t.Fatalf("expected error for invalid handle on %s", tt.name) + } + }) + } +} + +func TestControlC_PoolExtend(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + ranks []uint32 + expRC int + }{ + "success": { + mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolExtendResp{})}, + ranks: []uint32{1, 2}, + }, + "failure": { + mic: &control.MockInvokerConfig{UnaryError: daos.NoSpace}, + ranks: []uint32{1}, + expRC: int(daos.NoSpace), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callPoolExtend(handle, uuid.MustParse(testPoolUUID), tc.ranks); rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + }) + } +} + +func TestControlC_PoolSetProp(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + propName, propVal string + expRC int + }{ + "success": { + mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolSetPropResp{})}, + propName: "label", + propVal: "testpool", + }, + "invalid property name": { + mic: &control.MockInvokerConfig{}, + propName: "invalid_prop_name", + propVal: "value", + expRC: int(daos.InvalidInput), + }, + "failure": { + mic: &control.MockInvokerConfig{UnaryError: daos.NoPermission}, + propName: "label", + propVal: "testpool", + expRC: int(daos.NoPermission), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callPoolSetProp(handle, "", uuid.MustParse(testPoolUUID), tc.propName, tc.propVal); rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + }) + } +} + +func TestControlC_PoolGetProp(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + label string + propName string + expVal string + expRC int + }{ + "success with UUID": { + mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolGetPropResp{ + Properties: []*mgmtpb.PoolProperty{ + {Number: 1, Value: &mgmtpb.PoolProperty_Strval{Strval: "mypool"}}, + }, + })}, + propName: "label", + expVal: "mypool", + }, + "success with label": { + mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolGetPropResp{ + Properties: []*mgmtpb.PoolProperty{ + {Number: 1, Value: &mgmtpb.PoolProperty_Strval{Strval: "mypool"}}, + }, + })}, + label: "mypool", + propName: "label", + expVal: "mypool", + }, + "invalid property name": { + mic: &control.MockInvokerConfig{}, + propName: "invalid_prop_name", + expRC: int(daos.InvalidInput), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + val, rc := callPoolGetProp(handle, tc.label, uuid.MustParse(testPoolUUID), tc.propName) + if rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + if rc == 0 && val != tc.expVal { + t.Fatalf("val=%q, want %q", val, tc.expVal) + } + }) + } +} + +func TestControlC_PoolUpdateACE(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + expRC int + }{ + "success": {mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.ACLResp{})}}, + "failure": {mic: &control.MockInvokerConfig{UnaryError: daos.NoPermission}, expRC: int(daos.NoPermission)}, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callPoolUpdateACE(handle, uuid.MustParse(testPoolUUID), "A::user@:rw"); rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + }) + } +} + +func TestControlC_PoolDeleteACE(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + expRC int + }{ + "success": {mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.ACLResp{})}}, + "failure": {mic: &control.MockInvokerConfig{UnaryError: daos.NoPermission}, expRC: int(daos.NoPermission)}, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callPoolDeleteACE(handle, uuid.MustParse(testPoolUUID), "user@"); rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + }) + } +} + +func TestControlC_PoolRebuildStop(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + force bool + expRC int + }{ + "success": {mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.DaosResp{})}}, + "success with force": {mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.DaosResp{})}, force: true}, + "failure": {mic: &control.MockInvokerConfig{UnaryError: daos.Busy}, expRC: int(daos.Busy)}, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callPoolRebuildStop(handle, uuid.MustParse(testPoolUUID), tc.force); rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + }) + } +} + +func TestControlC_PoolRebuildStart(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + expRC int + }{ + "success": {mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.DaosResp{})}}, + "failure": {mic: &control.MockInvokerConfig{UnaryError: daos.Busy}, expRC: int(daos.Busy)}, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callPoolRebuildStart(handle, uuid.MustParse(testPoolUUID)); rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + }) + } +} + +func TestControlC_PoolList(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + expNpools uint64 + expRC int + }{ + "success - count only": { + mic: &control.MockInvokerConfig{UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.ListPoolsResp{ + Pools: []*mgmtpb.ListPoolsResp_Pool{ + {Uuid: "12345678-1234-1234-1234-123456789abc", Label: "pool1"}, + {Uuid: "22345678-1234-1234-1234-123456789abc", Label: "pool2"}, + }, + })}, + expNpools: 2, + }, + "failure": {mic: &control.MockInvokerConfig{UnaryError: daos.NoPermission}, expRC: int(daos.NoPermission)}, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + n, rc := callPoolListCount(handle, 0) + if rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + if rc == 0 && n != tc.expNpools { + t.Fatalf("count=%d, want %d", n, tc.expNpools) + } + }) + } +} + +func TestControlC_PoolListWithData(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mic := &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.ListPoolsResp{ + Pools: []*mgmtpb.ListPoolsResp_Pool{ + {Uuid: "12345678-1234-1234-1234-123456789abc", Label: "pool1", SvcReps: []uint32{0, 1, 2}}, + {Uuid: "22345678-1234-1234-1234-123456789abc", Label: "pool2", SvcReps: []uint32{0}}, + }, + }), + } + mi := control.NewMockInvoker(log, mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + entries, n, rc := callPoolList(handle, 2) + if rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + if n != 2 { + t.Fatalf("n=%d, want 2", n) + } + want := []testPoolInfo{ + {UUID: uuid.MustParse("12345678-1234-1234-1234-123456789abc"), Label: "pool1"}, + {UUID: uuid.MustParse("22345678-1234-1234-1234-123456789abc"), Label: "pool2"}, + } + if diff := cmp.Diff(want, entries); diff != "" { + t.Fatalf("entries mismatch (-want +got):\n%s", diff) + } +} + +func TestControlC_PoolListBufferTooSmall(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.ListPoolsResp{ + Pools: []*mgmtpb.ListPoolsResp_Pool{ + {Uuid: "12345678-1234-1234-1234-123456789abc", Label: "pool1"}, + {Uuid: "22345678-1234-1234-1234-123456789abc", Label: "pool2"}, + {Uuid: "32345678-1234-1234-1234-123456789abc", Label: "pool3"}, + }, + }), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + _, _, rc := callPoolList(handle, 1) + if rc != int(daos.BufTooSmall) { + t.Fatalf("rc=%d, want BufTooSmall(%d)", rc, int(daos.BufTooSmall)) + } +} + +func TestControlC_PoolListInvalidHandle(t *testing.T) { + if _, rc := callPoolListCount(0, 0); rc == 0 { + t.Fatal("expected error for invalid handle, got success") + } +} + +func TestControlC_PoolListErrorResetsNpools(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{UnaryError: daos.NoPermission}) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + n, rc := callPoolListCount(handle, 99) + if rc != int(daos.NoPermission) { + t.Fatalf("rc=%d, want NoPermission(%d)", rc, int(daos.NoPermission)) + } + if n != 0 { + t.Fatalf("n=%d, want 0 on error", n) + } +} + +func TestControlC_PoolListFreeIdempotent(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.ListPoolsResp{ + Pools: []*mgmtpb.ListPoolsResp_Pool{ + {Uuid: "12345678-1234-1234-1234-123456789abc", Label: "pool1", SvcReps: []uint32{0, 1, 2}}, + }, + }), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callPoolListDoubleFree(handle); rc != 0 { + t.Fatalf("double-free path rc=%d, want 0", rc) + } +} + +func TestControlC_ValidatePoolCreateArgs(t *testing.T) { + for name, tc := range map[string]struct { + spec *validatePoolCreateArgsSpec + expRC int + }{ + "valid - scm only": {&validatePoolCreateArgsSpec{scmSize: 1 << 30}, 0}, + "valid - nvme only": {&validatePoolCreateArgsSpec{nvmeSize: 1 << 30}, 0}, + "valid - both sizes set": {&validatePoolCreateArgsSpec{scmSize: 1 << 30, nvmeSize: 1 << 30}, 0}, + "invalid - nil args": {&validatePoolCreateArgsSpec{nilArgs: true}, int(daos.InvalidInput)}, + "invalid - missing pool_uuid": { + &validatePoolCreateArgsSpec{omitPoolUUID: true, scmSize: 1 << 30}, + int(daos.InvalidInput), + }, + "invalid - both sizes zero": {&validatePoolCreateArgsSpec{}, int(daos.InvalidInput)}, + "invalid - non-nil svc on input": { + &validatePoolCreateArgsSpec{scmSize: 1 << 30, nonNilSvc: true}, + int(daos.InvalidInput), + }, + } { + t.Run(name, func(t *testing.T) { + if rc := callValidatePoolCreateArgs(tc.spec); rc != tc.expRC { + t.Fatalf("rc=%d, want %d", rc, tc.expRC) + } + }) + } +} + +func TestControlC_PoolRebuildInvalidHandle(t *testing.T) { + u := uuid.MustParse(testPoolUUID) + if rc := callPoolRebuildStop(0, u, false); rc == 0 { + t.Fatal("rebuild stop: expected error for invalid handle") + } + if rc := callPoolRebuildStart(0, u); rc == 0 { + t.Fatal("rebuild start: expected error for invalid handle") + } +} + +// TestControlC_PoolCreateWithACL verifies that a DAOS_PROP_PO_ACL entry in the +// create prop is converted to req.ACL entries, not dropped with NotSupported. +func TestControlC_PoolCreateWithACL(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolCreateResp{ + SvcReps: []uint32{0}, + }), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + // DAOS_ACL_PERM_READ | DAOS_ACL_PERM_WRITE (0x03) as allow perms + const testPerms = uint64(0x03) + res := callPoolCreateWithACL(handle, uint32(os.Getuid()), uint32(os.Getgid()), 1<<30, testPerms) + if res.rc != 0 { + t.Fatalf("rc=%d, want 0", res.rc) + } + + if len(mi.SentReqs) == 0 { + t.Fatal("no request sent to invoker") + } + req, ok := mi.SentReqs[0].(*control.PoolCreateReq) + if !ok { + t.Fatalf("sent req type %T, want *control.PoolCreateReq", mi.SentReqs[0]) + } + if req.ACL == nil { + t.Fatal("req.ACL is nil, want non-nil ACL from prop") + } + // The make_test_acl helper creates one OWNER@ Allow ACE; we should get + // exactly one ACE string back. + if len(req.ACL.Entries) != 1 { + t.Fatalf("req.ACL.Entries=%d, want 1", len(req.ACL.Entries)) + } + // ACE strings are formatted as "type:flags:identity:perms" — the OWNER@ + // Allow entry should start with "A::" (Allow, no flags, OWNER@). + if !strings.HasPrefix(req.ACL.Entries[0], "A::OWNER@:") { + t.Errorf("ACE[0]=%q, want prefix A::OWNER@:", req.ACL.Entries[0]) + } +} + +// TestControlC_PoolCreateWithOwnerProp verifies that DAOS_PROP_PO_OWNER and +// DAOS_PROP_PO_OWNER_GROUP entries override the uid/gid-derived defaults in +// PoolCreateReq. +func TestControlC_PoolCreateWithOwnerProp(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolCreateResp{ + SvcReps: []uint32{0}, + }), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + const wantOwner = "alice@" + const wantGroup = "admins@" + res := callPoolCreateWithOwnerProp(handle, uint32(os.Getuid()), uint32(os.Getgid()), 1<<30, wantOwner, wantGroup) + if res.rc != 0 { + t.Fatalf("rc=%d, want 0", res.rc) + } + + if len(mi.SentReqs) == 0 { + t.Fatal("no request sent to invoker") + } + req, ok := mi.SentReqs[0].(*control.PoolCreateReq) + if !ok { + t.Fatalf("sent req type %T, want *control.PoolCreateReq", mi.SentReqs[0]) + } + // formatNameGroup appends "@" if missing; since wantOwner already ends + // with "@", the field should be exactly wantOwner. + if req.User != wantOwner { + t.Errorf("req.User=%q, want %q", req.User, wantOwner) + } + if req.UserGroup != wantGroup { + t.Errorf("req.UserGroup=%q, want %q", req.UserGroup, wantGroup) + } +} + +// TestControlC_PoolCreateWithSvcListPropSkipped verifies that a +// DAOS_PROP_PO_SVC_LIST entry does not cause a NotSupported error and is +// silently skipped (svc_list is read-only and unsettable at create time). +func TestControlC_PoolCreateWithSvcListPropSkipped(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolCreateResp{ + SvcReps: []uint32{0}, + }), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + res := callPoolCreateWithSvcListProp(handle, uint32(os.Getuid()), uint32(os.Getgid()), 1<<30) + if res.rc != 0 { + t.Fatalf("rc=%d, want 0 (svc_list skipped)", res.rc) + } +} diff --git a/src/control/lib/control/c/pool_test_helpers.go b/src/control/lib/control/c/pool_test_helpers.go new file mode 100644 index 00000000000..68f3fb5eec0 --- /dev/null +++ b/src/control/lib/control/c/pool_test_helpers.go @@ -0,0 +1,387 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build test_stubs + +// cgo drivers for pool_test.go (see test_helpers.go for why these can't +// live in the test file). + +package main + +/* +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// Build a minimal ACL with one Allow entry for OWNER@ with the given perms. +// Returns a newly allocated struct daos_acl * (caller must daos_acl_free). +static struct daos_acl * +make_pool_test_acl(uint64_t allow_perms) +{ + struct daos_ace *ace = daos_ace_create(DAOS_ACL_OWNER, NULL); + if (ace == NULL) + return NULL; + ace->dae_access_types = DAOS_ACL_ACCESS_ALLOW; + ace->dae_allow_perms = allow_perms; + + struct daos_ace *aces[] = { ace }; + struct daos_acl *acl = daos_acl_create(aces, 1); + daos_ace_free(ace); + return acl; +} + +static void +set_pool_prop_entry_acl(daos_prop_t *prop, uint32_t idx, struct daos_acl *acl) +{ + prop->dpp_entries[idx].dpe_type = DAOS_PROP_PO_ACL; + prop->dpp_entries[idx].dpe_val_ptr = acl; +} + +static void +set_pool_prop_entry_str(daos_prop_t *prop, uint32_t idx, uint32_t type, const char *str) +{ + prop->dpp_entries[idx].dpe_type = type; + D_STRNDUP(prop->dpp_entries[idx].dpe_str, str, DAOS_PROP_LABEL_MAX_LEN); +} + +static void +set_pool_prop_entry_val(daos_prop_t *prop, uint32_t idx, uint32_t type, uint64_t val) +{ + prop->dpp_entries[idx].dpe_type = type; + prop->dpp_entries[idx].dpe_val = val; +} +*/ +import "C" +import ( + "runtime/cgo" + "unsafe" + + "github.com/google/uuid" + + "github.com/daos-stack/daos/src/control/lib/daos" +) + +type poolCreateResult struct { + rc int + poolUUID uuid.UUID + svcRanks []uint32 +} + +func doPoolCreate(handle cgo.Handle, args C.struct_daos_control_pool_create_args) *poolCreateResult { + var cUUIDOut C.uuid_t + var svcOut *C.d_rank_list_t + + rc := int(daos_control_pool_create(C.uintptr_t(handle), &args, &svcOut, &cUUIDOut)) + res := &poolCreateResult{rc: rc} + if rc == 0 { + res.poolUUID = uuidFromC(&cUUIDOut) + } + if svcOut != nil { + n := int(svcOut.rl_nr) + if n > 0 { + cRanks := unsafe.Slice(svcOut.rl_ranks, n) + res.svcRanks = make([]uint32, n) + for i, r := range cRanks { + res.svcRanks[i] = uint32(r) + } + } + C.d_rank_list_free(svcOut) + } + return res +} + +func callPoolCreate(handle cgo.Handle, uid, gid uint32, scmSize, nvmeSize uint64, nsvc uint32) *poolCreateResult { + return doPoolCreate(handle, C.struct_daos_control_pool_create_args{ + dcpa_uid: C.uid_t(uid), + dcpa_gid: C.gid_t(gid), + dcpa_scm_size: C.daos_size_t(scmSize), + dcpa_nvme_size: C.daos_size_t(nvmeSize), + dcpa_nsvc: C.uint32_t(nsvc), + }) +} + +func callPoolCreateWithProp(handle cgo.Handle, uid, gid uint32, scmSize, nvmeSize uint64, nsvc uint32, entries []testPropEntry) *poolCreateResult { + args := C.struct_daos_control_pool_create_args{ + dcpa_uid: C.uid_t(uid), + dcpa_gid: C.gid_t(gid), + dcpa_scm_size: C.daos_size_t(scmSize), + dcpa_nvme_size: C.daos_size_t(nvmeSize), + dcpa_nsvc: C.uint32_t(nsvc), + } + if entries != nil { + args.dcpa_prop = buildCProp(entries) + defer C.daos_prop_free(args.dcpa_prop) + } + return doPoolCreate(handle, args) +} + +func callPoolCreateInvalidHandle() int { + var poolUUID C.uuid_t + args := C.struct_daos_control_pool_create_args{ + dcpa_uid: C.uid_t(1000), + dcpa_gid: C.gid_t(1000), + dcpa_scm_size: C.daos_size_t(1 << 30), + } + return int(daos_control_pool_create(C.uintptr_t(0), &args, nil, &poolUUID)) +} + +type validatePoolCreateArgsSpec struct { + nilArgs bool + omitPoolUUID bool + nonNilSvc bool + scmSize uint64 + nvmeSize uint64 +} + +func callValidatePoolCreateArgs(spec *validatePoolCreateArgsSpec) int { + var poolUUID C.uuid_t + if spec == nil || spec.nilArgs { + return errorToRC(validatePoolCreateArgs(nil, nil, &poolUUID)) + } + args := C.struct_daos_control_pool_create_args{ + dcpa_scm_size: C.daos_size_t(spec.scmSize), + dcpa_nvme_size: C.daos_size_t(spec.nvmeSize), + } + poolUUIDOut := &poolUUID + if spec.omitPoolUUID { + poolUUIDOut = nil + } + var svcOut *C.d_rank_list_t + if spec.nonNilSvc { + svcOut = C.d_rank_list_alloc(1) + defer C.d_rank_list_free(svcOut) + } + return errorToRC(validatePoolCreateArgs(&args, &svcOut, poolUUIDOut)) +} + +func callPoolDestroy(handle cgo.Handle, poolUUID uuid.UUID, force bool) int { + cu := cUUID(poolUUID) + var fi C.int + if force { + fi = 1 + } + return int(daos_control_pool_destroy(C.uintptr_t(handle), &cu, nil, fi)) +} + +func callPoolEvict(handle cgo.Handle, poolUUID uuid.UUID) int { + cu := cUUID(poolUUID) + return int(daos_control_pool_evict(C.uintptr_t(handle), &cu, nil)) +} + +func callPoolExclude(handle cgo.Handle, poolUUID uuid.UUID, rank uint32, tgtIdx int) int { + cu := cUUID(poolUUID) + return int(daos_control_pool_exclude(C.uintptr_t(handle), &cu, nil, C.d_rank_t(rank), C.int(tgtIdx))) +} + +func callPoolDrain(handle cgo.Handle, poolUUID uuid.UUID, rank uint32, tgtIdx int) int { + cu := cUUID(poolUUID) + return int(daos_control_pool_drain(C.uintptr_t(handle), &cu, nil, C.d_rank_t(rank), C.int(tgtIdx))) +} + +func callPoolReintegrate(handle cgo.Handle, poolUUID uuid.UUID, rank uint32, tgtIdx int) int { + cu := cUUID(poolUUID) + return int(daos_control_pool_reintegrate(C.uintptr_t(handle), &cu, nil, C.d_rank_t(rank), C.int(tgtIdx))) +} + +func callPoolExtend(handle cgo.Handle, poolUUID uuid.UUID, ranks []uint32) int { + cu := cUUID(poolUUID) + if len(ranks) == 0 { + return int(daos_control_pool_extend(C.uintptr_t(handle), &cu, nil, nil, 0)) + } + cRanks := make([]C.d_rank_t, len(ranks)) + for i, r := range ranks { + cRanks[i] = C.d_rank_t(r) + } + return int(daos_control_pool_extend(C.uintptr_t(handle), &cu, nil, &cRanks[0], C.int(len(ranks)))) +} + +func callPoolSetProp(handle cgo.Handle, label string, poolUUID uuid.UUID, name, value string) int { + cu := cUUID(poolUUID) + cLabel, lFree := cString(label) + defer lFree() + cName, nFree := cString(name) + defer nFree() + cValue, vFree := cString(value) + defer vFree() + return int(daos_control_pool_set_prop(C.uintptr_t(handle), cLabel, &cu, cName, cValue)) +} + +func callPoolGetProp(handle cgo.Handle, label string, poolUUID uuid.UUID, name string) (string, int) { + cu := cUUID(poolUUID) + cLabel, lFree := cString(label) + defer lFree() + cName, nFree := cString(name) + defer nFree() + var cValue *C.char + rc := int(daos_control_pool_get_prop(C.uintptr_t(handle), cLabel, &cu, cName, &cValue)) + var value string + if cValue != nil { + value = C.GoString(cValue) + C.free(unsafe.Pointer(cValue)) + } + return value, rc +} + +func callPoolUpdateACE(handle cgo.Handle, poolUUID uuid.UUID, ace string) int { + cu := cUUID(poolUUID) + lustreACE, f := cString(ace) + defer f() + return int(daos_control_pool_update_ace(C.uintptr_t(handle), &cu, nil, lustreACE)) +} + +func callPoolDeleteACE(handle cgo.Handle, poolUUID uuid.UUID, principal string) int { + cu := cUUID(poolUUID) + cP, f := cString(principal) + defer f() + return int(daos_control_pool_delete_ace(C.uintptr_t(handle), &cu, nil, cP)) +} + +func callPoolRebuildStop(handle cgo.Handle, poolUUID uuid.UUID, force bool) int { + cu := cUUID(poolUUID) + var fi C.int + if force { + fi = 1 + } + return int(daos_control_pool_rebuild_stop(C.uintptr_t(handle), &cu, nil, fi)) +} + +func callPoolRebuildStart(handle cgo.Handle, poolUUID uuid.UUID) int { + cu := cUUID(poolUUID) + return int(daos_control_pool_rebuild_start(C.uintptr_t(handle), &cu, nil)) +} + +// testPoolInfo is a Go-native snapshot of one entry in the pool_list output. +type testPoolInfo struct { + UUID uuid.UUID + Label string +} + +// callPoolListCount runs pool_list in count-only mode (pools=NULL). inputCap +// is the caller-side *npools value before the call. +func callPoolListCount(handle cgo.Handle, inputCap uint64) (outCount uint64, rc int) { + cCount := C.daos_size_t(inputCap) + rc = int(daos_control_pool_list(C.uintptr_t(handle), nil, &cCount, nil)) + return uint64(cCount), rc +} + +// callPoolList runs pool_list with a caller-allocated buffer of size cap. On +// success returns the populated entries. On BufTooSmall, entries is nil and +// outCount reports the required capacity. +func callPoolList(handle cgo.Handle, cap uint64) (entries []testPoolInfo, outCount uint64, rc int) { + var cPools *C.daos_mgmt_pool_info_t + var buf []C.daos_mgmt_pool_info_t + if cap > 0 { + buf = make([]C.daos_mgmt_pool_info_t, cap) + cPools = &buf[0] + } + cCount := C.daos_size_t(cap) + + rc = int(daos_control_pool_list(C.uintptr_t(handle), nil, &cCount, cPools)) + outCount = uint64(cCount) + + if rc != 0 || cap == 0 { + return nil, outCount, rc + } + + n := outCount + if n > cap { + n = cap + } + entries = make([]testPoolInfo, n) + for i := uint64(0); i < n; i++ { + entries[i] = testPoolInfo{ + UUID: uuidFromC(&buf[i].mgpi_uuid), + Label: goCString(buf[i].mgpi_label), + } + } + daos_control_pool_list_free(&buf[0], C.daos_size_t(outCount)) + return entries, outCount, rc +} + +// callPoolListDoubleFree exercises the pool_list_free double-free guard. +// Populates a buffer via pool_list, then calls the free helper twice. +func callPoolListDoubleFree(handle cgo.Handle) int { + buf := make([]C.daos_mgmt_pool_info_t, 1) + cCount := C.daos_size_t(1) + rc := int(daos_control_pool_list(C.uintptr_t(handle), nil, &cCount, &buf[0])) + if rc != 0 { + return rc + } + daos_control_pool_list_free(&buf[0], cCount) + daos_control_pool_list_free(&buf[0], cCount) + return 0 +} + +// callPoolCreateWithACL builds a pool-create prop containing a single +// DAOS_PROP_PO_ACL entry with one OWNER@ Allow ACE set to allowPerms, then +// calls pool create. Returns the poolCreateResult. +func callPoolCreateWithACL(handle cgo.Handle, uid, gid uint32, scmSize uint64, allowPerms uint64) *poolCreateResult { + acl := C.make_pool_test_acl(C.uint64_t(allowPerms)) + if acl == nil { + return &poolCreateResult{rc: int(daos.NoMemory)} + } + + prop := C.daos_prop_alloc(1) + // Transfer ACL ownership to the prop; daos_prop_free will call + // daos_acl_free on dpe_val_ptr, so we must not free acl separately. + C.set_pool_prop_entry_acl(prop, 0, acl) + defer C.daos_prop_free(prop) + + return doPoolCreate(handle, C.struct_daos_control_pool_create_args{ + dcpa_uid: C.uid_t(uid), + dcpa_gid: C.gid_t(gid), + dcpa_scm_size: C.daos_size_t(scmSize), + dcpa_prop: prop, + dcpa_nsvc: 1, + }) +} + +// callPoolCreateWithOwnerProp builds a pool-create prop containing +// DAOS_PROP_PO_OWNER and DAOS_PROP_PO_OWNER_GROUP string entries and calls +// pool create. +func callPoolCreateWithOwnerProp(handle cgo.Handle, uid, gid uint32, scmSize uint64, owner, ownerGroup string) *poolCreateResult { + prop := C.daos_prop_alloc(2) + defer C.daos_prop_free(prop) + + cOwner := C.CString(owner) + defer C.free(unsafe.Pointer(cOwner)) + C.set_pool_prop_entry_str(prop, 0, C.DAOS_PROP_PO_OWNER, cOwner) + + cGroup := C.CString(ownerGroup) + defer C.free(unsafe.Pointer(cGroup)) + C.set_pool_prop_entry_str(prop, 1, C.DAOS_PROP_PO_OWNER_GROUP, cGroup) + + return doPoolCreate(handle, C.struct_daos_control_pool_create_args{ + dcpa_uid: C.uid_t(uid), + dcpa_gid: C.gid_t(gid), + dcpa_scm_size: C.daos_size_t(scmSize), + dcpa_prop: prop, + dcpa_nsvc: 1, + }) +} + +// callPoolCreateWithSvcListProp builds a prop containing DAOS_PROP_PO_SVC_LIST +// (a read-only type) and calls pool create. propsFromC must skip it silently. +func callPoolCreateWithSvcListProp(handle cgo.Handle, uid, gid uint32, scmSize uint64) *poolCreateResult { + prop := C.daos_prop_alloc(1) + defer C.daos_prop_free(prop) + C.set_pool_prop_entry_val(prop, 0, C.DAOS_PROP_PO_SVC_LIST, 0) + + return doPoolCreate(handle, C.struct_daos_control_pool_create_args{ + dcpa_uid: C.uid_t(uid), + dcpa_gid: C.gid_t(gid), + dcpa_scm_size: C.daos_size_t(scmSize), + dcpa_prop: prop, + dcpa_nsvc: 1, + }) +} diff --git a/src/control/lib/control/c/server.go b/src/control/lib/control/c/server.go new file mode 100644 index 00000000000..f5db5f53196 --- /dev/null +++ b/src/control/lib/control/c/server.go @@ -0,0 +1,50 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +/* +#include +#include +*/ +import "C" +import ( + "github.com/daos-stack/daos/src/control/lib/control" +) + +//export daos_control_server_set_logmasks +func daos_control_server_set_logmasks(handle C.uintptr_t, host, masks, streams, subsystems *C.char) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.SetEngineLogMasksReq{} + if hostStr := goString(host); hostStr != "" { + req.SetHostList([]string{hostStr}) + } + if masks != nil { + m := goString(masks) + req.Masks = &m + } + if streams != nil { + s := goString(streams) + req.Streams = &s + } + if subsystems != nil { + ss := goString(subsystems) + req.Subsystems = &ss + } + + resp, err := control.SetEngineLogMasks(ctx.ctx(), ctx.client, req) + if err != nil { + return err + } + + hem := resp.HostErrorsResp.GetHostErrors() + if hem.ErrorCount() > 0 { + logAllHostErrors(ctx, hem) + return firstHostStatus(hem) + } + return nil + }) +} diff --git a/src/control/lib/control/c/server_test.go b/src/control/lib/control/c/server_test.go new file mode 100644 index 00000000000..607f69823fa --- /dev/null +++ b/src/control/lib/control/c/server_test.go @@ -0,0 +1,95 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + ctlpb "github.com/daos-stack/daos/src/control/common/proto/ctl" + "github.com/daos-stack/daos/src/control/common/test" + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/logging" +) + +func TestControlC_ServerSetLogmasks(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + masks string + streams string + subsystems string + expRC int + }{ + "failure - connection error": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.Unreachable, + }, + masks: "DEBUG", + expRC: int(daos.Unreachable), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + rc := callServerSetLogmasks(handle, "", tc.masks, tc.streams, tc.subsystems) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestControlC_ServerSetLogmasksInvalidHandle(t *testing.T) { + rc := callServerSetLogmasks(0, "", "DEBUG", "", "") + if rc == 0 { + t.Fatal("expected error for invalid handle, got success") + } +} + +func TestControlC_ServerSetLogmasksHostFilter(t *testing.T) { + for name, tc := range map[string]struct { + host string + wantList []string + }{ + "no host forwards empty list": {host: "", wantList: nil}, + "host forwarded as singleton": {host: "host-7", wantList: []string{"host-7"}}, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host-7", nil, &ctlpb.SetLogMasksResp{}), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callServerSetLogmasks(handle, tc.host, "DEBUG", "", ""); rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + + if len(mi.SentReqs) != 1 { + t.Fatalf("SentReqs=%d, want 1", len(mi.SentReqs)) + } + req, ok := mi.SentReqs[0].(*control.SetEngineLogMasksReq) + if !ok { + t.Fatalf("SentReqs[0] type=%T, want *control.SetEngineLogMasksReq", mi.SentReqs[0]) + } + if diff := cmp.Diff(tc.wantList, req.HostList); diff != "" { + t.Fatalf("HostList mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/src/control/lib/control/c/server_test_helpers.go b/src/control/lib/control/c/server_test_helpers.go new file mode 100644 index 00000000000..13fe5b49e03 --- /dev/null +++ b/src/control/lib/control/c/server_test_helpers.go @@ -0,0 +1,30 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build test_stubs + +// cgo drivers for server_test.go (see test_helpers.go for why these can't +// live in the test file). + +package main + +/* +#include +*/ +import "C" +import "runtime/cgo" + +func callServerSetLogmasks(handle cgo.Handle, host, masks, streams, subsystems string) int { + cHost, hF := cString(host) + defer hF() + cMasks, mF := cString(masks) + defer mF() + cStreams, strF := cString(streams) + defer strF() + cSubs, sF := cString(subsystems) + defer sF() + return int(daos_control_server_set_logmasks(C.uintptr_t(handle), cHost, cMasks, cStreams, cSubs)) +} diff --git a/src/control/lib/control/c/storage.go b/src/control/lib/control/c/storage.go new file mode 100644 index 00000000000..b6921bd5a73 --- /dev/null +++ b/src/control/lib/control/c/storage.go @@ -0,0 +1,243 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +/* +#include +#include +#include +#include +#include + +#include +*/ +import "C" +import ( + "encoding/json" + "net" + "unsafe" + + "github.com/google/uuid" + + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" +) + +//export daos_control_storage_device_list +func daos_control_storage_device_list(handle C.uintptr_t, host *C.char, ndisks *C.int, devices *C.struct_device_list) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + if ndisks == nil { + return daos.InvalidInput + } + + req := &control.SmdQueryReq{OmitPools: true} + if hostStr := goString(host); hostStr != "" { + req.SetHostList([]string{hostStr}) + } + + resp, err := control.SmdQuery(ctx.ctx(), ctx.client, req) + if err != nil { + *ndisks = 0 + return err + } + + totalDevices := 0 + for _, hss := range resp.HostStorage { + if hss.HostStorage != nil && hss.HostStorage.SmdInfo != nil { + totalDevices += len(hss.HostStorage.SmdInfo.Devices) + } + } + + // Query-only mode. + if devices == nil { + *ndisks = C.int(totalDevices) + return nil + } + + capacity := int(*ndisks) + if capacity < totalDevices { + *ndisks = C.int(totalDevices) + return daos.BufTooSmall + } + + if totalDevices > 0 { + C.memset(unsafe.Pointer(devices), 0, + C.size_t(totalDevices)*C.size_t(unsafe.Sizeof(*devices))) + } + deviceSlice := unsafe.Slice(devices, capacity) + idx := 0 + for _, hss := range resp.HostStorage { + if hss.HostStorage == nil || hss.HostStorage.SmdInfo == nil { + continue + } + + // Strip port from "host:port" if present. SplitHostPort handles + // bracketed IPv6 correctly and returns an error for inputs without + // a port — in that case we keep the original string. + hostAddr := hss.HostSet.RangedString() + if h, _, err := net.SplitHostPort(hostAddr); err == nil { + hostAddr = h + } + + for _, dev := range hss.HostStorage.SmdInfo.Devices { + if dev == nil { + continue + } + if idx >= totalDevices { + break + } + + if devUUID, err := uuid.Parse(dev.UUID); err == nil { + copyUUIDToC(devUUID, &deviceSlice[idx].dl_device_id) + } + + // storage.SmdDevice.Ctrlr is a value type; its zero value + // yields NvmeStateUnknown.String() == "UNKNOWN", so a + // missing proto Ctrlr surfaces as "UNKNOWN" rather than a + // nil deref. + state := dev.Ctrlr.NvmeState.String() + copyStringToCharArray(state, &deviceSlice[idx].dl_state[0], C.DAOS_DEV_STATE_MAX_LEN) + + deviceSlice[idx].dl_rank = C.int(dev.Rank) + + copyStringToCharArray(hostAddr, &deviceSlice[idx].dl_host[0], C.DAOS_HOSTNAME_MAX_LEN) + + nTgts := len(dev.TargetIDs) + if nTgts > C.DAOS_MAX_TARGETS_PER_DEVICE { + nTgts = C.DAOS_MAX_TARGETS_PER_DEVICE + } + for i := 0; i < nTgts; i++ { + deviceSlice[idx].dl_tgtidx[i] = C.int(dev.TargetIDs[i]) + } + deviceSlice[idx].dl_n_tgtidx = C.int(nTgts) + + idx++ + } + } + + *ndisks = C.int(idx) + return nil + }) +} + +//export daos_control_storage_set_nvme_fault +func daos_control_storage_set_nvme_fault(handle C.uintptr_t, host *C.char, devUUID *C.uuid_t) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.SmdManageReq{ + IDs: uuidFromC(devUUID).String(), + Operation: control.SetFaultyOp, + } + if hostStr := goString(host); hostStr != "" { + req.SetHostList([]string{hostStr}) + } + + resp, err := control.SmdManage(ctx.ctx(), ctx.client, req) + if err != nil { + return err + } + + // Log the full host-error set before collapsing to a single rc. + hem := resp.HostErrorsResp.GetHostErrors() + if hem.ErrorCount() > 0 { + logAllHostErrors(ctx, hem) + return firstHostStatus(hem) + } + return nil + }) +} + +//export daos_control_storage_query_device_health +func daos_control_storage_query_device_health(handle C.uintptr_t, host, statsKey, statsOut *C.char, statsOutLen C.int, devUUID *C.uuid_t) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + key := goString(statsKey) + + req := &control.SmdQueryReq{ + OmitPools: true, + IncludeBioHealth: true, + UUID: uuidFromC(devUUID).String(), + } + if hostStr := goString(host); hostStr != "" { + req.SetHostList([]string{hostStr}) + } + + resp, err := control.SmdQuery(ctx.ctx(), ctx.client, req) + if err != nil { + return err + } + + // Round-trip HealthStats through JSON to look up the key by name, matching + // the legacy dmg-helper behavior and avoiding a per-field switch here. + for _, hss := range resp.HostStorage { + if hss.HostStorage == nil || hss.HostStorage.SmdInfo == nil { + continue + } + for _, dev := range hss.HostStorage.SmdInfo.Devices { + if dev == nil || dev.Ctrlr.HealthStats == nil { + continue + } + value, err := extractHealthStat(dev.Ctrlr.HealthStats, key) + if err != nil { + ctx.log.Errorf("health stat lookup for %q failed: %v", key, err) + return err + } + if value == "" { + return daos.Nonexistent + } + if statsOut != nil && statsOutLen > 0 { + copyStringToCharArray(value, statsOut, int(statsOutLen)) + } + return nil + } + } + return daos.Nonexistent + }) +} + +// extractHealthStat marshals the health stats struct to JSON and returns the +// value for the named key. Strings are returned with their JSON quoting intact +// to match the old dmg-helper behavior (which used json_object_to_json_string). +func extractHealthStat(stats interface{}, key string) (string, error) { + if key == "" { + return "", nil + } + data, err := json.Marshal(stats) + if err != nil { + return "", err + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return "", err + } + raw, ok := fields[key] + if !ok { + return "", nil + } + return string(raw), nil +} + +// copyStringToCharArray copies s into dest (length maxLen), null-terminated. +func copyStringToCharArray(s string, dest *C.char, maxLen int) { + if dest == nil || maxLen <= 0 { + return + } + + destSlice := unsafe.Slice(dest, maxLen) + + // Reserve one byte for the null terminator. + copyLen := len(s) + if copyLen > maxLen-1 { + copyLen = maxLen - 1 + // Walk back past any UTF-8 continuation bytes so we don't truncate mid-rune. + for copyLen > 0 && (s[copyLen]&0xC0) == 0x80 { + copyLen-- + } + } + for i := 0; i < copyLen; i++ { + destSlice[i] = C.char(s[i]) + } + destSlice[copyLen] = 0 +} diff --git a/src/control/lib/control/c/storage_ops_test.go b/src/control/lib/control/c/storage_ops_test.go new file mode 100644 index 00000000000..6a6c27b6503 --- /dev/null +++ b/src/control/lib/control/c/storage_ops_test.go @@ -0,0 +1,588 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + + ctlpb "github.com/daos-stack/daos/src/control/common/proto/ctl" + "github.com/daos-stack/daos/src/control/common/test" + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/logging" +) + +func TestControlC_StorageDeviceList(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + expNdisks int + expRC int + }{ + "success - empty": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &ctlpb.SmdQueryResp{}), + }, + expNdisks: 0, + expRC: 0, + }, + "failure": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.Unreachable, + }, + expRC: int(daos.Unreachable), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + ndisks, rc := callStorageDeviceList(handle, "") + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + + if rc == 0 && ndisks != tc.expNdisks { + t.Fatalf("expected %d disks, got %d", tc.expNdisks, ndisks) + } + }) + } +} + +func TestControlC_StorageSetNVMeFault(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + host string + expRC int + }{ + "failure - connection error": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.Unreachable, + }, + host: "host1", + expRC: int(daos.Unreachable), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + devUUID := uuid.MustParse("12345678-1234-1234-1234-123456789abc") + + rc := callStorageSetNVMeFault(handle, tc.host, devUUID) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestControlC_StorageQueryDeviceHealth(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + host string + statsKey string + expRC int + }{ + "failure - connection error": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.Unreachable, + }, + host: "host1", + statsKey: "temperature", + expRC: int(daos.Unreachable), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + devUUID := uuid.MustParse("12345678-1234-1234-1234-123456789abc") + + _, rc := callStorageQueryDeviceHealth(handle, tc.host, tc.statsKey, devUUID) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestControlC_ExtractHealthStat(t *testing.T) { + stats := struct { + Temperature uint32 `json:"temperature"` + BioReadErrs uint32 `json:"bio_read_errs"` + BioWriteErrs uint32 `json:"bio_write_errs"` + DevUUID string `json:"dev_uuid"` + TempWarn bool `json:"temp_warn"` + }{ + Temperature: 300, + BioReadErrs: 5, + BioWriteErrs: 2, + DevUUID: "abcd", + TempWarn: true, + } + + for name, tc := range map[string]struct { + key string + expVal string + }{ + "numeric field": {key: "temperature", expVal: "300"}, + "snake_case read": {key: "bio_read_errs", expVal: "5"}, + "snake_case write": {key: "bio_write_errs", expVal: "2"}, + "string field quoted": {key: "dev_uuid", expVal: `"abcd"`}, + "bool field": {key: "temp_warn", expVal: "true"}, + "unknown key": {key: "nonexistent", expVal: ""}, + "empty key": {key: "", expVal: ""}, + } { + t.Run(name, func(t *testing.T) { + got, err := extractHealthStat(stats, tc.key) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tc.expVal { + t.Fatalf("expected %q, got %q", tc.expVal, got) + } + }) + } +} + +// populatedSmdQueryResp builds a single-host SmdQueryResp covering the fields +// daos_control_storage_device_list reads. +func populatedSmdQueryResp(host string, devs []*ctlpb.SmdDevice) *control.UnaryResponse { + ranks := make([]*ctlpb.SmdQueryResp_RankResp, 0, len(devs)) + for _, d := range devs { + ranks = append(ranks, &ctlpb.SmdQueryResp_RankResp{ + Rank: d.Rank, + Devices: []*ctlpb.SmdDevice{d}, + }) + } + return control.MockMSResponse(host, nil, &ctlpb.SmdQueryResp{Ranks: ranks}) +} + +func TestControlC_StorageDeviceListPopulated(t *testing.T) { + uuid0 := uuid.MustParse("00000000-0000-0000-0000-000000000001") + uuid1 := uuid.MustParse("00000000-0000-0000-0000-000000000002") + + longTargets := make([]int32, 116) + for i := range longTargets { + longTargets[i] = int32(i) + } + + devs := []*ctlpb.SmdDevice{ + { + Uuid: uuid0.String(), + Rank: 0, + TgtIds: []int32{0, 1, 2, 3}, + Ctrlr: &ctlpb.NvmeController{ + DevState: ctlpb.NvmeDevState_EVICTED, + }, + }, + { + Uuid: uuid1.String(), + Rank: 1, + TgtIds: longTargets, + Ctrlr: &ctlpb.NvmeController{ + DevState: ctlpb.NvmeDevState_NORMAL, + }, + }, + } + + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: populatedSmdQueryResp("host-0", devs), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + got, ndisks, rc := callStorageDeviceListPopulated(handle, "", 4) + if rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + if ndisks != 2 { + t.Fatalf("ndisks=%d, want 2", ndisks) + } + if len(got) != 2 { + t.Fatalf("len(got)=%d, want 2", len(got)) + } + + wantStates := map[uuid.UUID]string{ + uuid0: "EVICTED", + uuid1: "NORMAL", + } + wantRanks := map[uuid.UUID]uint32{ + uuid0: 0, + uuid1: 1, + } + + for _, d := range got { + if d.Host != "host-0" { + t.Errorf("device %s: host=%q, want %q", d.UUID, d.Host, "host-0") + } + if want := wantStates[d.UUID]; d.State != want { + t.Errorf("device %s: state=%q, want %q", d.UUID, d.State, want) + } + if want := wantRanks[d.UUID]; d.Rank != want { + t.Errorf("device %s: rank=%d, want %d", d.UUID, d.Rank, want) + } + + switch d.UUID { + case uuid0: + if len(d.Targets) != 4 { + t.Errorf("device %s: target count=%d, want 4", d.UUID, len(d.Targets)) + } + case uuid1: + // Truncated to DAOS_MAX_TARGETS_PER_DEVICE (96). If this + // ever changes, the struct size and this expectation move + // together. + const wantTruncated = 96 + if len(d.Targets) != wantTruncated { + t.Errorf("device %s: target count=%d, want %d (truncated)", + d.UUID, len(d.Targets), wantTruncated) + } + } + } +} + +func TestControlC_StorageQueryDeviceHealthPopulated(t *testing.T) { + devUUIDStr := "00000000-0000-0000-0000-000000000001" + + healthResp := &ctlpb.SmdQueryResp{ + Ranks: []*ctlpb.SmdQueryResp_RankResp{ + { + Devices: []*ctlpb.SmdDevice{ + { + Uuid: devUUIDStr, + Ctrlr: &ctlpb.NvmeController{ + HealthStats: &ctlpb.BioHealthResp{ + Temperature: 300, + BioReadErrs: 5, + BioWriteErrs: 2, + PowerOnHours: 999999999999, + }, + }, + }, + }, + }, + }, + } + + for name, tc := range map[string]struct { + statsKey string + bufSize int + expOut string + expRC int + }{ + "numeric stat fits": { + statsKey: "temperature", + bufSize: 256, + expOut: "300", + }, + "numeric stat tiny buffer truncates cleanly": { + // "300" is 3 chars + NUL; a 3-byte buffer can only hold + // "30\0". The library must truncate, not overflow. + statsKey: "temperature", + bufSize: 3, + expOut: "30", + }, + "long numeric stat truncates cleanly": { + // 999999999999 (12 digits) in a 5-byte buffer: 4 chars + NUL. + statsKey: "power_on_hours", + bufSize: 5, + expOut: "9999", + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host-0", nil, healthResp), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + devUUID := uuid.MustParse(devUUIDStr) + + out, rc := callStorageQueryDeviceHealthSized(handle, "host-0", tc.statsKey, devUUID, tc.bufSize) + if rc != tc.expRC { + t.Fatalf("rc=%d, want %d (out=%q)", rc, tc.expRC, out) + } + if out != tc.expOut { + t.Fatalf("output=%q, want %q", out, tc.expOut) + } + }) + } +} + +func TestControlC_StorageQueryDeviceHealthSeparateBuffers(t *testing.T) { + devUUIDStr := "00000000-0000-0000-0000-000000000001" + + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host-0", nil, &ctlpb.SmdQueryResp{ + Ranks: []*ctlpb.SmdQueryResp_RankResp{ + { + Devices: []*ctlpb.SmdDevice{ + { + Uuid: devUUIDStr, + Ctrlr: &ctlpb.NvmeController{ + HealthStats: &ctlpb.BioHealthResp{ + Temperature: 42, + }, + }, + }, + }, + }, + }, + }), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + devUUID := uuid.MustParse(devUUIDStr) + + const key = "temperature" + out, rc := callStorageQueryDeviceHealthSized(handle, "host-0", key, devUUID, 64) + if rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + if out != "42" { + t.Fatalf("output=%q, want %q", out, "42") + } + if key != "temperature" { + t.Fatalf("key modified: got %q", key) + } +} + +func TestControlC_StorageDeviceListUndersizedBuffer(t *testing.T) { + devs := []*ctlpb.SmdDevice{ + { + Uuid: "00000000-0000-0000-0000-000000000001", + Rank: 0, + Ctrlr: &ctlpb.NvmeController{ + DevState: ctlpb.NvmeDevState_NORMAL, + }, + }, + { + Uuid: "00000000-0000-0000-0000-000000000002", + Rank: 1, + Ctrlr: &ctlpb.NvmeController{ + DevState: ctlpb.NvmeDevState_NORMAL, + }, + }, + } + + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: populatedSmdQueryResp("host-0", devs), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + got, ndisks, rc := callStorageDeviceListPopulated(handle, "", 1) + if rc != int(daos.BufTooSmall) { + t.Fatalf("rc=%d, want BufTooSmall(%d)", rc, int(daos.BufTooSmall)) + } + if ndisks != 2 { + t.Fatalf("ndisks=%d, want 2 (required capacity)", ndisks) + } + if len(got) != 0 { + t.Fatalf("expected no populated entries on BufTooSmall, got %d", len(got)) + } + + got, ndisks, rc = callStorageDeviceListPopulated(handle, "", 2) + if rc != 0 { + t.Fatalf("retry rc=%d, want 0", rc) + } + if ndisks != 2 { + t.Fatalf("retry ndisks=%d, want 2", ndisks) + } + if len(got) != 2 { + t.Fatalf("retry len(got)=%d, want 2", len(got)) + } +} + +func TestControlC_StorageDeviceListHostPortStrip(t *testing.T) { + for name, tc := range map[string]struct { + respHost string + expHost string + }{ + "hostname with port": { + respHost: "host-0:10001", + expHost: "host-0", + }, + "hostname without port is preserved": { + respHost: "host-0", + expHost: "host-0", + }, + } { + t.Run(name, func(t *testing.T) { + devs := []*ctlpb.SmdDevice{ + { + Uuid: "00000000-0000-0000-0000-000000000010", + Rank: 0, + Ctrlr: &ctlpb.NvmeController{ + DevState: ctlpb.NvmeDevState_NORMAL, + }, + }, + } + + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: populatedSmdQueryResp(tc.respHost, devs), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + got, _, rc := callStorageDeviceListPopulated(handle, "", 1) + if rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + if len(got) != 1 { + t.Fatalf("len(got)=%d, want 1", len(got)) + } + if got[0].Host != tc.expHost { + t.Fatalf("host=%q, want %q", got[0].Host, tc.expHost) + } + }) + } +} + +func TestControlC_StorageDeviceListAbsentCtrlr(t *testing.T) { + devs := []*ctlpb.SmdDevice{ + { + Uuid: "00000000-0000-0000-0000-000000000020", + Rank: 0, + Ctrlr: &ctlpb.NvmeController{}, // zero-valued controller + }, + } + + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: populatedSmdQueryResp("host-0", devs), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + got, ndisks, rc := callStorageDeviceListPopulated(handle, "", 1) + if rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + if ndisks != 1 { + t.Fatalf("ndisks=%d, want 1", ndisks) + } + if got[0].State != "UNKNOWN" { + t.Fatalf("state=%q, want %q", got[0].State, "UNKNOWN") + } +} + +func TestControlC_StorageDeviceListHostFilter(t *testing.T) { + for name, tc := range map[string]struct { + host string + wantList []string + }{ + "no host forwards empty list": {host: "", wantList: nil}, + "host forwarded as singleton": {host: "host-3", wantList: []string{"host-3"}}, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host-3", nil, &ctlpb.SmdQueryResp{}), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if _, rc := callStorageDeviceList(handle, tc.host); rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + + if len(mi.SentReqs) != 1 { + t.Fatalf("SentReqs=%d, want 1", len(mi.SentReqs)) + } + req, ok := mi.SentReqs[0].(*control.SmdQueryReq) + if !ok { + t.Fatalf("SentReqs[0] type=%T, want *control.SmdQueryReq", mi.SentReqs[0]) + } + if diff := cmp.Diff(tc.wantList, req.HostList); diff != "" { + t.Fatalf("HostList mismatch (-want +got):\n%s", diff) + } + }) + } +} + +// TestStorageDeviceListErrorResetsNdisks pins that ndisks is zeroed on error, +// matching the daos_control_pool_list contract. The caller passes a non-zero +// input value and must see 0 back when the RPC fails. +func TestControlC_StorageDeviceListErrorResetsNdisks(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryError: daos.Unreachable, + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + ndisks, rc := callStorageDeviceListCount(handle, "", 99) + if rc != int(daos.Unreachable) { + t.Fatalf("rc=%d, want Unreachable(%d)", rc, int(daos.Unreachable)) + } + if ndisks != 0 { + t.Fatalf("ndisks=%d, want 0 on error", ndisks) + } +} + +func TestControlC_StorageOperationsInvalidHandle(t *testing.T) { + devUUID := uuid.MustParse("12345678-1234-1234-1234-123456789abc") + + tests := []struct { + name string + fn func() int + }{ + {"device_list", func() int { _, rc := callStorageDeviceList(0, ""); return rc }}, + {"set_nvme_fault", func() int { return callStorageSetNVMeFault(0, "host1", devUUID) }}, + {"query_device_health", func() int { _, rc := callStorageQueryDeviceHealth(0, "host1", "temperature", devUUID); return rc }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rc := tt.fn() + if rc == 0 { + t.Fatalf("expected error for invalid handle on %s, got success", tt.name) + } + }) + } +} diff --git a/src/control/lib/control/c/storage_test_helpers.go b/src/control/lib/control/c/storage_test_helpers.go new file mode 100644 index 00000000000..a689c887c36 --- /dev/null +++ b/src/control/lib/control/c/storage_test_helpers.go @@ -0,0 +1,120 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build test_stubs + +// cgo drivers for storage_ops_test.go (see test_helpers.go for why these +// can't live in the test file). + +package main + +/* +#include +#include +#include + +#include +*/ +import "C" +import ( + "runtime/cgo" + "unsafe" + + "github.com/google/uuid" +) + +func callStorageDeviceList(handle cgo.Handle, host string) (ndisks int, rc int) { + cHost, f := cString(host) + defer f() + var n C.int + rc = int(daos_control_storage_device_list(C.uintptr_t(handle), cHost, &n, nil)) + return int(n), rc +} + +// callStorageDeviceListCount runs device_list in count-only mode (devices=NULL) +// with the caller-side *ndisks set to inputCount on entry, returning the value +// the library writes back. +func callStorageDeviceListCount(handle cgo.Handle, host string, inputCount int) (ndisks int, rc int) { + cHost, f := cString(host) + defer f() + n := C.int(inputCount) + rc = int(daos_control_storage_device_list(C.uintptr_t(handle), cHost, &n, nil)) + return int(n), rc +} + +type testDeviceInfo struct { + UUID uuid.UUID + State string + Rank uint32 + Host string + Targets []int32 +} + +func callStorageDeviceListPopulated(handle cgo.Handle, host string, cap int) ([]testDeviceInfo, int, int) { + cHost, f := cString(host) + defer f() + + devices := make([]C.struct_device_list, cap) + n := C.int(cap) + var devPtr *C.struct_device_list + if cap > 0 { + devPtr = &devices[0] + } + rc := int(daos_control_storage_device_list(C.uintptr_t(handle), cHost, &n, devPtr)) + if rc != 0 { + return nil, int(n), rc + } + got := int(n) + if got > cap { + got = cap + } + out := make([]testDeviceInfo, got) + for i := 0; i < got; i++ { + tgtCount := int(devices[i].dl_n_tgtidx) + out[i] = testDeviceInfo{ + UUID: uuidFromC(&devices[i].dl_device_id), + State: C.GoString(&devices[i].dl_state[0]), + Rank: uint32(devices[i].dl_rank), + Host: C.GoString(&devices[i].dl_host[0]), + Targets: make([]int32, tgtCount), + } + for j := 0; j < tgtCount; j++ { + out[i].Targets[j] = int32(devices[i].dl_tgtidx[j]) + } + } + return out, int(n), rc +} + +func callStorageSetNVMeFault(handle cgo.Handle, host string, devUUID uuid.UUID) int { + cHost, f := cString(host) + defer f() + cu := cUUID(devUUID) + return int(daos_control_storage_set_nvme_fault(C.uintptr_t(handle), cHost, &cu)) +} + +func callStorageQueryDeviceHealth(handle cgo.Handle, host, statsKey string, devUUID uuid.UUID) (string, int) { + return callStorageQueryDeviceHealthSized(handle, host, statsKey, devUUID, 256) +} + +func callStorageQueryDeviceHealthSized(handle cgo.Handle, host, statsKey string, devUUID uuid.UUID, bufSize int) (string, int) { + cHost, hF := cString(host) + defer hF() + cKey := C.CString(statsKey) + defer C.free(unsafe.Pointer(cKey)) + cu := cUUID(devUUID) + + out := make([]C.char, bufSize) + rc := int(daos_control_storage_query_device_health(C.uintptr_t(handle), cHost, cKey, &out[0], C.int(bufSize), &cu)) + + var result []byte + for _, c := range out { + if c == 0 { + break + } + result = append(result, byte(c)) + } + return string(result), rc +} diff --git a/src/control/lib/control/c/system.go b/src/control/lib/control/c/system.go new file mode 100644 index 00000000000..ed687b56fd0 --- /dev/null +++ b/src/control/lib/control/c/system.go @@ -0,0 +1,111 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +/* +#include +#include +#include +*/ +import "C" +import ( + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/ranklist" +) + +//export daos_control_system_stop_rank +func daos_control_system_stop_rank(handle C.uintptr_t, rank C.d_rank_t, force C.int) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + if err := requireRank(rank); err != nil { + return err + } + req := &control.SystemStopReq{Force: force != 0} + req.Ranks.Add(ranklist.Rank(rank)) + resp, err := control.SystemStop(ctx.ctx(), ctx.client, req) + if err != nil { + return err + } + return firstMemberStatus(resp.Results) + }) +} + +// daos_control_system_stop stops every member of the system. +// +//export daos_control_system_stop +func daos_control_system_stop(handle C.uintptr_t, force C.int) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.SystemStopReq{Force: force != 0} + resp, err := control.SystemStop(ctx.ctx(), ctx.client, req) + if err != nil { + return err + } + return firstMemberStatus(resp.Results) + }) +} + +//export daos_control_system_start_rank +func daos_control_system_start_rank(handle C.uintptr_t, rank C.d_rank_t) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + if err := requireRank(rank); err != nil { + return err + } + req := &control.SystemStartReq{} + req.Ranks.Add(ranklist.Rank(rank)) + resp, err := control.SystemStart(ctx.ctx(), ctx.client, req) + if err != nil { + return err + } + return firstMemberStatus(resp.Results) + }) +} + +// daos_control_system_start starts every member of the system. +// +//export daos_control_system_start +func daos_control_system_start(handle C.uintptr_t) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + req := &control.SystemStartReq{} + resp, err := control.SystemStart(ctx.ctx(), ctx.client, req) + if err != nil { + return err + } + return firstMemberStatus(resp.Results) + }) +} + +//export daos_control_system_reint_rank +func daos_control_system_reint_rank(handle C.uintptr_t, rank C.d_rank_t) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + if err := requireRank(rank); err != nil { + return err + } + // Clear=true reintegrates a previously excluded rank. + req := &control.SystemExcludeReq{Clear: true} + req.Ranks.Add(ranklist.Rank(rank)) + resp, err := control.SystemExclude(ctx.ctx(), ctx.client, req) + if err != nil { + return err + } + return firstMemberStatus(resp.Results) + }) +} + +//export daos_control_system_exclude_rank +func daos_control_system_exclude_rank(handle C.uintptr_t, rank C.d_rank_t) C.int { + return withContext(handle, func(ctx *ctrlContext) error { + if err := requireRank(rank); err != nil { + return err + } + req := &control.SystemExcludeReq{Clear: false} + req.Ranks.Add(ranklist.Rank(rank)) + resp, err := control.SystemExclude(ctx.ctx(), ctx.client, req) + if err != nil { + return err + } + return firstMemberStatus(resp.Results) + }) +} diff --git a/src/control/lib/control/c/system_test.go b/src/control/lib/control/c/system_test.go new file mode 100644 index 00000000000..595313d2e5e --- /dev/null +++ b/src/control/lib/control/c/system_test.go @@ -0,0 +1,323 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "testing" + + mgmtpb "github.com/daos-stack/daos/src/control/common/proto/mgmt" + sharedpb "github.com/daos-stack/daos/src/control/common/proto/shared" + "github.com/daos-stack/daos/src/control/common/test" + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/logging" +) + +func TestControlC_SystemStopRank(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + rank uint32 + force bool + expRC int + }{ + "success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.SystemStopResp{ + Results: []*sharedpb.RankResult{ + {Rank: 0, State: "stopped"}, + }, + }), + }, + rank: 0, + force: false, + expRC: 0, + }, + "success with force": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.SystemStopResp{ + Results: []*sharedpb.RankResult{ + {Rank: 1, State: "stopped"}, + }, + }), + }, + rank: 1, + force: true, + expRC: 0, + }, + "failure - connection error": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.Unreachable, + }, + rank: 0, + expRC: int(daos.Unreachable), + }, + "CRT_NO_RANK rejected": { + mic: &control.MockInvokerConfig{}, + rank: 0xffffffff, + expRC: int(daos.InvalidInput), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + rc := callSystemStopRank(handle, tc.rank, tc.force) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestControlC_SystemStartRank(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + rank uint32 + expRC int + }{ + "success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.SystemStartResp{ + Results: []*sharedpb.RankResult{ + {Rank: 0, State: "joined"}, + }, + }), + }, + rank: 0, + expRC: 0, + }, + "CRT_NO_RANK rejected": { + mic: &control.MockInvokerConfig{}, + rank: 0xffffffff, + expRC: int(daos.InvalidInput), + }, + "failure - already started": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.SystemStartResp{ + Results: []*sharedpb.RankResult{ + {Rank: 0, State: "joined", Errored: true, Msg: "already started"}, + }, + }), + }, + rank: 0, + expRC: int(daos.MiscError), // RankResult error maps to MiscError + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + rc := callSystemStartRank(handle, tc.rank) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +// Whole-system stop: the request must carry an empty rank set. +func TestControlC_SystemStop(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.SystemStopResp{ + Results: []*sharedpb.RankResult{ + {Rank: 0, State: "stopped"}, + {Rank: 1, State: "stopped"}, + }, + }), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callSystemStop(handle, true); rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + + req := mi.SentReqs[0].(*control.SystemStopReq) + if req.Ranks.Count() != 0 { + t.Fatalf("Ranks=%s, want empty set for whole-system stop", req.Ranks.String()) + } + if !req.Force { + t.Fatal("Force not propagated") + } +} + +func TestControlC_SystemStart(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.SystemStartResp{ + Results: []*sharedpb.RankResult{ + {Rank: 0, State: "joined"}, + {Rank: 1, State: "joined"}, + }, + }), + }) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + if rc := callSystemStart(handle); rc != 0 { + t.Fatalf("rc=%d, want 0", rc) + } + + req := mi.SentReqs[0].(*control.SystemStartReq) + if req.Ranks.Count() != 0 { + t.Fatalf("Ranks=%s, want empty set for whole-system start", req.Ranks.String()) + } +} + +func TestControlC_SystemRankInvalidHandle(t *testing.T) { + // Test that system operations return error for invalid handle + rc := callSystemStopRank(0, 0, false) + if rc == 0 { + t.Fatal("expected error for invalid handle on stop, got success") + } + + rc = callSystemStartRank(0, 0) + if rc == 0 { + t.Fatal("expected error for invalid handle on start, got success") + } + + rc = callSystemReintRank(0, 0) + if rc == 0 { + t.Fatal("expected error for invalid handle on reint, got success") + } + + rc = callSystemExcludeRank(0, 0) + if rc == 0 { + t.Fatal("expected error for invalid handle on exclude, got success") + } +} + +func TestControlC_SystemReintRank(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + rank uint32 + expRC int + }{ + "success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.SystemExcludeResp{ + Results: []*sharedpb.RankResult{ + {Rank: 0, State: "joined"}, + }, + }), + }, + rank: 0, + expRC: 0, + }, + "rpc failure": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.NoPermission, + }, + rank: 0, + expRC: int(daos.NoPermission), + }, + "rank result errored": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.SystemExcludeResp{ + Results: []*sharedpb.RankResult{ + {Rank: 0, Errored: true, Msg: "rank reint failed"}, + }, + }), + }, + rank: 0, + expRC: int(daos.MiscError), + }, + "CRT_NO_RANK rejected": { + mic: &control.MockInvokerConfig{}, + rank: 0xffffffff, + expRC: int(daos.InvalidInput), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + rc := callSystemReintRank(handle, tc.rank) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestControlC_SystemExcludeRank(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + rank uint32 + expRC int + }{ + "success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.SystemExcludeResp{ + Results: []*sharedpb.RankResult{ + {Rank: 0, State: "excluded"}, + }, + }), + }, + rank: 0, + expRC: 0, + }, + "rpc failure": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.NoPermission, + }, + rank: 0, + expRC: int(daos.NoPermission), + }, + "rank result errored": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.SystemExcludeResp{ + Results: []*sharedpb.RankResult{ + {Rank: 0, Errored: true, Msg: "rank exclude failed"}, + }, + }), + }, + rank: 0, + expRC: int(daos.MiscError), + }, + "CRT_NO_RANK rejected": { + mic: &control.MockInvokerConfig{}, + rank: 0xffffffff, + expRC: int(daos.InvalidInput), + }, + } { + t.Run(name, func(t *testing.T) { + log, buf := logging.NewTestLogger(t.Name()) + defer test.ShowBufferOnFailure(t, buf) + + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + rc := callSystemExcludeRank(handle, tc.rank) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} diff --git a/src/control/lib/control/c/system_test_helpers.go b/src/control/lib/control/c/system_test_helpers.go new file mode 100644 index 00000000000..e8317c967cf --- /dev/null +++ b/src/control/lib/control/c/system_test_helpers.go @@ -0,0 +1,51 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build test_stubs + +// cgo drivers for system_test.go (see test_helpers.go for why these can't +// live in the test file). + +package main + +/* +#include +#include +*/ +import "C" +import "runtime/cgo" + +func callSystemStopRank(handle cgo.Handle, rank uint32, force bool) int { + var fi C.int + if force { + fi = 1 + } + return int(daos_control_system_stop_rank(C.uintptr_t(handle), C.d_rank_t(rank), fi)) +} + +func callSystemStop(handle cgo.Handle, force bool) int { + var fi C.int + if force { + fi = 1 + } + return int(daos_control_system_stop(C.uintptr_t(handle), fi)) +} + +func callSystemStartRank(handle cgo.Handle, rank uint32) int { + return int(daos_control_system_start_rank(C.uintptr_t(handle), C.d_rank_t(rank))) +} + +func callSystemStart(handle cgo.Handle) int { + return int(daos_control_system_start(C.uintptr_t(handle))) +} + +func callSystemReintRank(handle cgo.Handle, rank uint32) int { + return int(daos_control_system_reint_rank(C.uintptr_t(handle), C.d_rank_t(rank))) +} + +func callSystemExcludeRank(handle cgo.Handle, rank uint32) int { + return int(daos_control_system_exclude_rank(C.uintptr_t(handle), C.d_rank_t(rank))) +} diff --git a/src/control/lib/control/c/test_helpers.go b/src/control/lib/control/c/test_helpers.go new file mode 100644 index 00000000000..c02fc032170 --- /dev/null +++ b/src/control/lib/control/c/test_helpers.go @@ -0,0 +1,126 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build test_stubs + +// Test shim: _test.go files in this package can't `import "C"` (Go disallows +// cgo in test files), so every cgo-touching test helper lives in a non-test +// file. Shared conversion/fixture helpers are here; per-export drivers live +// in the *_test_helpers.go file named for the export file they exercise, +// mirroring lib/daos/api's per-domain stub layout. + +package main + +/* +#include +#include +#include +#include +#include +#include +#include + +// cgo can't write into the dpe_type/dpe_val/dpe_str union directly. +static void +set_prop_entry_val(daos_prop_t *prop, uint32_t idx, uint32_t type, uint64_t val) +{ + prop->dpp_entries[idx].dpe_type = type; + prop->dpp_entries[idx].dpe_val = val; +} + +static void +set_prop_entry_str(daos_prop_t *prop, uint32_t idx, uint32_t type, const char *str) +{ + prop->dpp_entries[idx].dpe_type = type; + D_STRNDUP(prop->dpp_entries[idx].dpe_str, str, DAOS_PROP_LABEL_MAX_LEN); +} +*/ +import "C" +import ( + "runtime/cgo" + "unsafe" + + "github.com/google/uuid" + + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/logging" +) + +// makeTestHandle creates a cgo.Handle for a test context with the given mock invoker. +func makeTestHandle(mi control.UnaryInvoker, log *logging.LeveledLogger) cgo.Handle { + return cgo.NewHandle(newTestContext(mi, log)) +} + +// testPropEntry describes one daos_prop_t entry for a test. Exactly one of +// NumVal or StrVal is used depending on the property's storage kind (see +// testPropNum / testPropStr). +type testPropEntry struct { + Type uint32 + NumVal uint64 + StrVal string + isStr bool +} + +func testPropNum(propType uint32, val uint64) testPropEntry { + return testPropEntry{Type: propType, NumVal: val} +} + +func testPropStr(propType uint32, val string) testPropEntry { + return testPropEntry{Type: propType, StrVal: val, isStr: true} +} + +// Re-exported from cgo so _test.go files can refer to these constants. +const ( + testPropPoLabel = uint32(C.DAOS_PROP_PO_LABEL) + testPropPoOwner = uint32(C.DAOS_PROP_PO_OWNER) + testPropPoOwnerGroup = uint32(C.DAOS_PROP_PO_OWNER_GROUP) + testPropPoRedunFac = uint32(C.DAOS_PROP_PO_REDUN_FAC) + testPropPoScrubMode = uint32(C.DAOS_PROP_PO_SCRUB_MODE) + testPropPoACL = uint32(C.DAOS_PROP_PO_ACL) + testPropPoSvcList = uint32(C.DAOS_PROP_PO_SVC_LIST) +) + +// buildCProp allocates a daos_prop_t from entries. Caller must C.daos_prop_free +// the result when done. +func buildCProp(entries []testPropEntry) *C.daos_prop_t { + prop := C.daos_prop_alloc(C.uint32_t(len(entries))) + for i, e := range entries { + if e.isStr { + cs := C.CString(e.StrVal) + C.set_prop_entry_str(prop, C.uint32_t(i), C.uint32_t(e.Type), cs) + C.free(unsafe.Pointer(cs)) + } else { + C.set_prop_entry_val(prop, C.uint32_t(i), C.uint32_t(e.Type), C.uint64_t(e.NumVal)) + } + } + return prop +} + +// cUUID builds a C uuid_t from a Go UUID. Returns the value; callers take its +// address via &... when passing to a function that wants *C.uuid_t. +func cUUID(u uuid.UUID) C.uuid_t { + var out C.uuid_t + copyUUIDToC(u, &out) + return out +} + +// cString wraps C.CString with a NULL-on-empty convention and a small closure +// for deferred freeing. +func cString(s string) (*C.char, func()) { + if s == "" { + return nil, func() {} + } + cs := C.CString(s) + return cs, func() { C.free(unsafe.Pointer(cs)) } +} + +// goCString reads a null-terminated C string pointer into a Go string. +func goCString(p *C.char) string { + if p == nil { + return "" + } + return C.GoString(p) +} diff --git a/src/control/lib/control/server.go b/src/control/lib/control/server.go index 632afc2b5a6..945384dcd1f 100644 --- a/src/control/lib/control/server.go +++ b/src/control/lib/control/server.go @@ -1,5 +1,6 @@ // // (C) Copyright 2021-2023 Intel Corporation. +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP // // SPDX-License-Identifier: BSD-2-Clause-Patent // @@ -152,6 +153,5 @@ func SetEngineLogMasks(ctx context.Context, rpcClient UnaryInvoker, req *SetEngi } } - rpcClient.Debugf("DAOS set engine log masks response: %+v", resp) return resp, nil } diff --git a/src/include/daos/control_types.h b/src/include/daos/control_types.h new file mode 100644 index 00000000000..b64a5db7d0a --- /dev/null +++ b/src/include/daos/control_types.h @@ -0,0 +1,162 @@ +/** + * (C) Copyright 2026 Hewlett Packard Enterprise Development LP + * + * SPDX-License-Identifier: BSD-2-Clause-Patent + */ + +/** + * \file + * + * DAOS Control Plane C API Types + * + * This header defines types used by the libdaos_control shared library, + * which provides C bindings to the DAOS management/control plane. + */ + +#ifndef __DAOS_CONTROL_TYPES_H__ +#define __DAOS_CONTROL_TYPES_H__ + +#include +#include +#include + +#include +#include +#include + +#if defined(__cplusplus) +extern "C" { +#endif + +/** + * Initialization options for the DAOS control library. + * + * All fields are optional; NULL selects the corresponding default. + */ +struct daos_control_init_args { + /** Path to the dmg config file (NULL for the default / insecure config) */ + const char *dcia_config_file; + /** Path to the log file (NULL disables logging) */ + const char *dcia_log_file; + /** Log level: debug, info, notice, error (NULL selects notice) */ + const char *dcia_log_level; +}; + +/** + * Arguments for daos_control_pool_create. + */ +struct daos_control_pool_create_args { + /** UID to record as the pool's owner. */ + uid_t dcpa_uid; + /** GID to record as the pool's owner group. */ + gid_t dcpa_gid; + /** System/group name (NULL selects the default system). */ + const char *dcpa_grp; + /** Target ranks to host the pool (NULL lets the server choose). */ + d_rank_list_t *dcpa_tgts; + /** SCM tier capacity per target, in bytes. */ + daos_size_t dcpa_scm_size; + /** NVMe tier capacity per target, in bytes. */ + daos_size_t dcpa_nvme_size; + /** Optional pool properties (NULL for defaults). */ + daos_prop_t *dcpa_prop; + /** Requested number of service replicas (dmg --nsvc); 0 = server picks. */ + uint32_t dcpa_nsvc; +}; + +/** + * Maximum number of targets that can be attached to a single device. Must stay + * in sync with BIO_MAX_VOS_TGT_CNT (daos_srv/bio.h) — enforced by a + * D_CASSERT in src/common/tests_dmg_helpers.c, which sees both headers. + */ +#define DAOS_MAX_TARGETS_PER_DEVICE 96 + +/** + * Maximum hostname length (matches POSIX _POSIX_HOST_NAME_MAX). + */ +#define DAOS_HOSTNAME_MAX_LEN 255 + +/** + * Maximum NVMe device state name length (longest current value is "UNPLUGGED", + * 9 chars + NUL). A D_CASSERT in src/common/tests_dmg_helpers.c pins this to + * the struct field width so the two stay in sync. + */ +#define DAOS_DEV_STATE_MAX_LEN 10 + +/** + * Server-side cap on interactive action choices per check report. Defined + * here (the one header both the engine and libdaos_control can see) and + * aliased by CHK_INTERACT_OPTION_MAX in src/chk/chk_internal.h. + */ +#define DAOS_CHECK_INTERACT_OPTION_MAX 3 + +/** + * Capacity of the report snapshot below; must hold every option the server + * may send. check.go truncates extra choices as a last line of defense. + */ +#define DAOS_CHECK_MAX_ACT_OPTIONS 4 + +#if defined(__cplusplus) +static_assert(DAOS_CHECK_MAX_ACT_OPTIONS >= DAOS_CHECK_INTERACT_OPTION_MAX, + "check report snapshot cannot hold every interactive option"); +#else +_Static_assert(DAOS_CHECK_MAX_ACT_OPTIONS >= DAOS_CHECK_INTERACT_OPTION_MAX, + "check report snapshot cannot hold every interactive option"); +#endif + +/** + * Storage device information. + */ +typedef struct device_list { + uuid_t dl_device_id; + char dl_state[DAOS_DEV_STATE_MAX_LEN]; + int dl_rank; + char dl_host[DAOS_HOSTNAME_MAX_LEN]; + int dl_tgtidx[DAOS_MAX_TARGETS_PER_DEVICE]; + int dl_n_tgtidx; +} device_list; + +/** + * DAOS checker pool information. + */ +struct daos_check_pool_info { + uuid_t dcpi_uuid; + char *dcpi_status; + char *dcpi_phase; +}; + +/** + * DAOS checker report information. + */ +struct daos_check_report_info { + uuid_t dcri_uuid; + uint64_t dcri_seq; + uint32_t dcri_class; + uint32_t dcri_act; + int dcri_rank; + int dcri_result; + int dcri_option_nr; + int dcri_options[DAOS_CHECK_MAX_ACT_OPTIONS]; +}; + +/** + * DAOS checker query results. + * + * All pointer fields are allocated by daos_control_check_query() and must + * be freed by calling daos_control_check_info_free(). + */ +struct daos_check_info { + char *dci_status; + char *dci_phase; + int dci_leader; + int dci_pool_nr; + int dci_report_nr; + struct daos_check_pool_info *dci_pools; + struct daos_check_report_info *dci_reports; +}; + +#if defined(__cplusplus) +} +#endif + +#endif /* __DAOS_CONTROL_TYPES_H__ */ diff --git a/src/include/daos/tests_lib.h b/src/include/daos/tests_lib.h index 461ee39ee05..30cf9b74b58 100644 --- a/src/include/daos/tests_lib.h +++ b/src/include/daos/tests_lib.h @@ -14,6 +14,7 @@ #include #include #include +#include #define assert_rc_equal(rc, expected_rc) \ do { \ @@ -85,19 +86,6 @@ tsc_create_cont(struct credit_context *tsc) return tsc_create_pool(tsc) || !tsc->tsc_skip_cont_create; } -/* match BIO_XS_CNT_MAX, which is the max VOS xstreams mapped to a device */ -#define MAX_TEST_TARGETS_PER_DEVICE 48 -#define DSS_HOSTNAME_MAX_LEN 255 - -typedef struct { - uuid_t device_id; - char state[10]; - int rank; - char host[DSS_HOSTNAME_MAX_LEN]; - int tgtidx[MAX_TEST_TARGETS_PER_DEVICE]; - int n_tgtidx; -} device_list; - enum test_cr_start_flags { TCSF_NONE = 0, TCSF_DRYRUN = (1 << 0), @@ -192,33 +180,6 @@ enum test_cr_action { TCA_TRUST_EC_DATA = 12, }; -struct daos_check_pool_info { - uuid_t dcpi_uuid; - char *dcpi_status; - char *dcpi_phase; -}; - -struct daos_check_report_info { - uuid_t dcri_uuid; - uint64_t dcri_seq; - uint32_t dcri_class; - uint32_t dcri_act; - int dcri_rank; - int dcri_result; - int dcri_option_nr; - int dcri_options[3]; -}; - -struct daos_check_info { - char *dci_status; - char *dci_phase; - int dci_leader; - int dci_pool_nr; - int dci_report_nr; - struct daos_check_pool_info *dci_pools; - struct daos_check_report_info *dci_reports; -}; - /** Initialize an SGL with a variable number of IOVs and set the IOV buffers * to the value of the strings passed. This will allocate memory for the iov * structures as well as the iov buffers, so d_sgl_fini(sgl, true) must be @@ -529,10 +490,9 @@ int dmg_storage_device_list(const char *dmg_config_file, int *ndisks, * \param host [IN] Nvme set to faulty on host name provided. Only single disk can be set to fault for now. * \param uuid [IN] UUID of the device. - * \param force [IN] Do not require confirmation */ -int dmg_storage_set_nvme_fault(const char *dmg_config_file, - char *host, const uuid_t uuid, int force); +int +dmg_storage_set_nvme_fault(const char *dmg_config_file, char *host, const uuid_t uuid); /** * Get NVMe Device health stats. * @@ -540,10 +500,15 @@ int dmg_storage_set_nvme_fault(const char *dmg_config_file, * \param[in] host Get device health from the given host. * \param[in] uuid UUID of the device. * \param[in,out] stats [in] Health stats for which to get counter value. - * [out] Stats counter value. + * [out] Stats counter value (NUL-terminated, + * truncated to \a stats_len - 1 if longer). + * \param[in] stats_len Size of the \a stats buffer in bytes. Must be + * large enough to hold both the longest key + * name and any value the caller cares about. */ -int dmg_storage_query_device_health(const char *dmg_config_file, char *host, - char *stats, const uuid_t uuid); +int + dmg_storage_query_device_health(const char *dmg_config_file, char *host, char *stats, + size_t stats_len, const uuid_t uuid); /** * Verify the assumed blobstore device state with the actual enum definition @@ -564,20 +529,41 @@ int verify_blobstore_state(int state, const char *state_str); * * \param dmg_config_file * [IN] DMG config file - * \param rank [IN] Rank to stop. + * \param rank [IN] Rank to stop. Must be a real rank; use + * dmg_system_stop() to stop the whole system. * \param force [IN] Terminate with extreme prejudice. */ int dmg_system_stop_rank(const char *dmg_config_file, d_rank_t rank, int force); +/** + * Stop every member of the system. + * + * \param dmg_config_file + * [IN] DMG config file + * \param force [IN] Terminate with extreme prejudice. + */ +int + dmg_system_stop(const char *dmg_config_file, int force); + /** * Start a rank. * * \param dmg_config_file * [IN] DMG config file - * \param rank [IN] Rank to start. + * \param rank [IN] Rank to start. Must be a real rank; use + * dmg_system_start() to start the whole system. */ int dmg_system_start_rank(const char *dmg_config_file, d_rank_t rank); +/** + * Start every member of the system. + * + * \param dmg_config_file + * [IN] DMG config file + */ +int + dmg_system_start(const char *dmg_config_file); + /** * Reintegrate a rank into the system. * diff --git a/src/tests/SConscript b/src/tests/SConscript index 30c838ad5df..199f91747f3 100644 --- a/src/tests/SConscript +++ b/src/tests/SConscript @@ -92,6 +92,8 @@ def scons(): denv.AppendUnique(LIBPATH=[Dir('../vos')]) denv.AppendUnique(LIBPATH=[Dir('../bio')]) denv.AppendUnique(LIBPATH=[Dir('../utils/wrap/mpi')]) + denv.AppendUnique(LIBPATH=[Dir('../control/lib/control/c')]) + denv.d_add_build_rpath('../control/lib/control/c') # Add runtime paths for daos libraries denv.AppendUnique(RPATH_FULL=['$PREFIX/lib64/daos_srv']) denv.AppendUnique(CPPPATH=[Dir('../mgmt').srcnode()]) @@ -106,6 +108,8 @@ def scons(): denv.AppendUnique(LIBPATH=[Dir('../client/api')]) denv.AppendUnique(LIBPATH=[Dir('../cart')]) denv.AppendUnique(LIBPATH=[Dir('../client/dfs')]) + denv.AppendUnique(LIBPATH=[Dir('../control/lib/control/c')]) + denv.d_add_build_rpath('../control/lib/control/c') libs_client = ['daos_tests', 'daos', 'daos_common', 'cart', 'gurt', 'uuid', 'dfs'] diff --git a/src/tests/ftest/util/cmocka_utils.py b/src/tests/ftest/util/cmocka_utils.py index 6a9f0441938..9faa9758208 100644 --- a/src/tests/ftest/util/cmocka_utils.py +++ b/src/tests/ftest/util/cmocka_utils.py @@ -1,5 +1,6 @@ """ (C) Copyright 2022-2024 Intel Corporation. + (C) Copyright 2026 Hewlett Packard Enterprise Development LP SPDX-License-Identifier: BSD-2-Clause-Patent """ @@ -9,6 +10,7 @@ from command_utils import ExecutableCommand from command_utils_base import BasicParameter, EnvironmentVariables from exception_utils import CommandFailure +from general_utils import get_log_file from results_utils import Job, Results, TestName, TestResult, create_xml from run_utils import get_clush_command, run_local, run_remote @@ -114,6 +116,14 @@ def get_cmocka_env(self): return EnvironmentVariables({ "CMOCKA_XML_FILE": os.path.join(self.cmocka_dir, "%g_cmocka_results.xml"), "CMOCKA_MESSAGE_OUTPUT": "xml", + # Route libdaos_control (the C-bindings backing dmg_* helpers in + # tests_dmg_helpers.c) log into DAOS_TEST_LOG_DIR. Every cmocka test + # links libdaos_control, so this belongs here rather than per-caller. + # collection_utils.py sweeps DAOS_TEST_LOG_DIR after each test, so + # the log gets archived. Without this, the C side falls back to + # /tmp/libdaos_control.log, which CI never collects. + "DAOS_TEST_CONTROL_LOG_FILE": get_log_file( + f"{self.test_name}_libdaos_control.log"), }) def run_cmocka_test(self, test, command): diff --git a/src/tests/ftest/util/daos_core_base.py b/src/tests/ftest/util/daos_core_base.py index dceca1d8429..8c658382991 100644 --- a/src/tests/ftest/util/daos_core_base.py +++ b/src/tests/ftest/util/daos_core_base.py @@ -1,12 +1,11 @@ """ (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 """ import os -import shutil from apricot import TestWithServers from cmocka_utils import CmockaUtils, get_cmocka_command @@ -86,6 +85,13 @@ def run_subtest(self, command=None): daos_test_env["D_LOG_FILE"] = get_log_file(self.client_log) daos_test_env["D_LOG_MASK"] = self.get_test_param("test_log_mask", "DEBUG") daos_test_env["DD_MASK"] = "mgmt,io,md,epc,rebuild,test" + # Route libdaos_control (the C-bindings for dmg_* helpers in + # tests_dmg_helpers.c) log directly into the test's outputdir. + # ftest --archive collects self.outputdir as part of each test's + # artifact bundle; DAOS_TEST_LOG_DIR on client nodes is NOT + # collected (clush --rcopy only pulls from server hosts). + daos_test_env["DAOS_TEST_CONTROL_LOG_FILE"] = os.path.join( + self.outputdir, f"{self.subtest_name}_libdaos_control.log") daos_test_env["COVFILE"] = "/tmp/test.cov" daos_test_env["POOL_SCM_SIZE"] = str(scm_size) daos_test_env["POOL_NVME_SIZE"] = str(nvme_size) @@ -111,13 +117,3 @@ def run_subtest(self, command=None): rank, ["Stopped", "Excluded"]) cmocka_utils.run_cmocka_test(self, job) - - try: - tmp_log_path = "/tmp/suite_dmg.log" - log_path = os.path.join(self.outputdir, f"{self.subtest_name}_dmg.log") - shutil.move(tmp_log_path, log_path) - except FileNotFoundError: - # if dmg wasn't called, there will not be a dmg log file - self.log.info("dmg log file not found") - except IOError as error: - self.log.error("unable to move dmg log: %s", error) diff --git a/src/tests/suite/daos_cr.c b/src/tests/suite/daos_cr.c index 54b3192efd5..20a17b84ac2 100644 --- a/src/tests/suite/daos_cr.c +++ b/src/tests/suite/daos_cr.c @@ -26,8 +26,14 @@ * #define CR_ACCURATE_QUERY_RESULT 1 */ -/* Start pool service may take sometime, let's wait for at most CR_WAIT_MAX * 2 seconds. */ -#define CR_WAIT_MAX (45) +/* + * Wall-clock budget for checker-progress waits. The pre-bindings dmg helpers + * fork/exec'd dmg for every query (~1-2s each), so the historical + * 45-iteration polls implied roughly three minutes of wall time; the + * bindings answer in milliseconds, which silently halved the wait. Budget + * time, not iterations. + */ +#define CR_WAIT_SECS 300 /* 256MB for CR pool size. */ #define CR_POOL_SIZE (1 << 28) @@ -241,14 +247,14 @@ static inline int cr_system_start(void) { print_message("CR: starting system ...\n"); - return dmg_system_start_rank(dmg_config_file, CRT_NO_RANK); + return dmg_system_start(dmg_config_file); } static inline int cr_system_stop(bool force) { print_message("CR: stopping system with %s ...\n", force ? "force" : "non-force"); - return dmg_system_stop_rank(dmg_config_file, CRT_NO_RANK, force); + return dmg_system_stop(dmg_config_file, force); } static inline int @@ -356,11 +362,11 @@ cr_check_query(uint32_t pool_nr, uuid_t uuids[], struct daos_check_info *dci) } static inline int -cr_check_repair(uint64_t seq, uint32_t opt) +cr_check_repair(uint64_t seq, uint32_t action) { - print_message("CR: handle check interaction for seq %lu, option %u ...\n", - (unsigned long)seq, opt); - return dmg_check_repair(dmg_config_file, seq, opt); + print_message("CR: handle check interaction for seq %lu, action %u ...\n", + (unsigned long)seq, action); + return dmg_check_repair(dmg_config_file, seq, action); } static inline int @@ -442,7 +448,7 @@ cr_cleanup(test_arg_t *arg, struct test_pool *pools, uint32_t nr) } rc = dmg_pool_destroy(dmg_config_file, pools[i].pool_uuid, arg->group, 1); - if (rc != 0 && rc != -DER_NONEXIST && rc != -DER_MISC) + if (rc != 0 && rc != -DER_NONEXIST) print_message("CR: dmg_pool_destroy failed: "DF_RC"\n", DP_RC(rc)); } } @@ -451,11 +457,12 @@ static void cr_ins_wait(uint32_t pool_nr, uuid_t uuids[], struct daos_check_info *dci) { int rc; - int i; + + time_t deadline = time(NULL) + CR_WAIT_SECS; print_message("CR: waiting check instance ...\n"); - for (i = 0; i < CR_WAIT_MAX; i++) { + for (;;) { cr_dci_fini(dci); rc = dmg_check_query(dmg_config_file, pool_nr, uuids, dci); @@ -464,6 +471,9 @@ cr_ins_wait(uint32_t pool_nr, uuid_t uuids[], struct daos_check_info *dci) if (!cr_ins_status_init(dci->dci_status) && !cr_ins_status_running(dci->dci_status)) break; + if (time(NULL) >= deadline) + break; + sleep(2); } } @@ -471,13 +481,13 @@ cr_ins_wait(uint32_t pool_nr, uuid_t uuids[], struct daos_check_info *dci) static void cr_pool_wait(uint32_t pool_nr, uuid_t uuids[], struct daos_check_info *dci) { - int rc; - int i; + time_t deadline = time(NULL) + CR_WAIT_SECS; + int rc; print_message("CR: waiting check pool ...\n"); cr_dump_pools(pool_nr, uuids); - for (i = 0; i < CR_WAIT_MAX; i++) { + for (;;) { cr_dci_fini(dci); rc = dmg_check_query(dmg_config_file, pool_nr, uuids, dci); @@ -487,6 +497,9 @@ cr_pool_wait(uint32_t pool_nr, uuid_t uuids[], struct daos_check_info *dci) !cr_pool_status_checking(dci->dci_pools[0].dcpi_status)) break; + if (time(NULL) >= deadline) + break; + sleep(2); } } @@ -1244,7 +1257,7 @@ cr_leader_interaction(void **state) for (i = 0; i < dcri->dcri_option_nr; i++) { if (dcri->dcri_options[i] == action) { - rc = cr_check_repair(dcri->dcri_seq, i); + rc = cr_check_repair(dcri->dcri_seq, dcri->dcri_options[i]); break; } } @@ -1335,7 +1348,7 @@ cr_engine_interaction(void **state) for (i = 0; i < dcri->dcri_option_nr; i++) { if (dcri->dcri_options[i] == action) { - rc = cr_check_repair(dcri->dcri_seq, i); + rc = cr_check_repair(dcri->dcri_seq, dcri->dcri_options[i]); break; } } @@ -1433,7 +1446,7 @@ cr_repair_forall_leader(void **state) for (i = 0; i < dcri->dcri_option_nr; i++) { if (dcri->dcri_options[i] == action) { - rc = cr_check_repair(dcri->dcri_seq, i); + rc = cr_check_repair(dcri->dcri_seq, dcri->dcri_options[i]); break; } } @@ -1550,7 +1563,7 @@ cr_repair_forall_engine(void **state) for (i = 0; i < dcri->dcri_option_nr; i++) { if (dcri->dcri_options[i] == action) { - rc = cr_check_repair(dcri->dcri_seq, i); + rc = cr_check_repair(dcri->dcri_seq, dcri->dcri_options[i]); break; } } @@ -1837,7 +1850,7 @@ cr_stop_specified(void **state) for (i = 0; i < dcri->dcri_option_nr; i++) { if (dcri->dcri_options[i] == action) { - rc = cr_check_repair(dcri->dcri_seq, i); + rc = cr_check_repair(dcri->dcri_seq, dcri->dcri_options[i]); break; } } @@ -1950,7 +1963,7 @@ cr_auto_reset(void **state) for (i = 0; i < dcri->dcri_option_nr; i++) { if (dcri->dcri_options[i] == action) { - rc = cr_check_repair(dcri->dcri_seq, i); + rc = cr_check_repair(dcri->dcri_seq, dcri->dcri_options[i]); break; } } @@ -2003,8 +2016,8 @@ cr_pause(void **state, bool force) struct daos_check_info dci = { 0 }; uint32_t class = TCC_POOL_BAD_LABEL; uint32_t action = TCA_INTERACT; - int rc; - int i; + time_t deadline; + int rc; rc = cr_pool_create(state, &pool, false, class); assert_rc_equal(rc, 0); @@ -2032,7 +2045,8 @@ cr_pause(void **state, bool force) rc = cr_system_start(); assert_rc_equal(rc, 0); - for (i = 0; i < CR_WAIT_MAX; i++) { + deadline = time(NULL) + CR_WAIT_SECS; + do { /* Sleep for a while after system re-started under check mode. */ sleep(2); @@ -2042,7 +2056,7 @@ cr_pause(void **state, bool force) break; assert_rc_equal(rc, -DER_INVAL); - } + } while (time(NULL) < deadline); rc = cr_ins_verify(&dci, TCIS_PAUSED); assert_rc_equal(rc, 0); @@ -2913,7 +2927,7 @@ cr_engine_death(void **state) for (i = 0; i < dcri->dcri_option_nr; i++) { if (dcri->dcri_options[i] == action) { /* Repair the pool label with the lost rank. */ - rc = cr_check_repair(dcri->dcri_seq, i); + rc = cr_check_repair(dcri->dcri_seq, dcri->dcri_options[i]); break; } } @@ -3029,7 +3043,7 @@ cr_engine_rejoin_succ(void **state) for (i = 0; i < dcri->dcri_option_nr; i++) { if (dcri->dcri_options[i] == action) { - rc = cr_check_repair(dcri->dcri_seq, i); + rc = cr_check_repair(dcri->dcri_seq, dcri->dcri_options[i]); break; } } @@ -3134,7 +3148,7 @@ cr_engine_rejoin_fail(void **state) for (i = 0; i < dcri->dcri_option_nr; i++) { if (dcri->dcri_options[i] == action) { /* Repair the inconsistency with the lost rank. */ - rc = cr_check_repair(dcri->dcri_seq, i); + rc = cr_check_repair(dcri->dcri_seq, dcri->dcri_options[i]); break; } } @@ -3279,7 +3293,7 @@ cr_multiple_pools(void **state) for (i = 0; i < dcri->dcri_option_nr; i++) { if (dcri->dcri_options[i] == actions[1]) { - rc = cr_check_repair(dcri->dcri_seq, i); + rc = cr_check_repair(dcri->dcri_seq, dcri->dcri_options[i]); break; } } @@ -3336,7 +3350,7 @@ cr_multiple_pools(void **state) dcri = cr_locate_dcri(&dci, dcri, uuids[i]); for (j = 0; j < dcri->dcri_option_nr; j++) { if (dcri->dcri_options[j] == actions[0]) { - rc = cr_check_repair(dcri->dcri_seq, j); + rc = cr_check_repair(dcri->dcri_seq, dcri->dcri_options[j]); break; } } @@ -3377,7 +3391,7 @@ cr_multiple_pools(void **state) for (i = 0; i < dcri->dcri_option_nr; i++) { if (dcri->dcri_options[i] == actions[1]) { - rc = cr_check_repair(dcri->dcri_seq, i); + rc = cr_check_repair(dcri->dcri_seq, dcri->dcri_options[i]); break; } } diff --git a/src/tests/suite/daos_nvme_recovery.c b/src/tests/suite/daos_nvme_recovery.c index a5ee67a0e3f..f4dde68a282 100644 --- a/src/tests/suite/daos_nvme_recovery.c +++ b/src/tests/suite/daos_nvme_recovery.c @@ -1,5 +1,6 @@ /** * (C) Copyright 2019-2024 Intel Corporation. + * (C) Copyright 2026 Hewlett Packard Enterprise Development LP * * SPDX-License-Identifier: BSD-2-Clause-Patent */ @@ -90,17 +91,17 @@ pick_faulty_device(device_list *devices, int num_dev, int faulty_rank) bool is_sys_dev; for (i = 0; i < num_dev; i++) { - if (devices[i].rank != faulty_rank) + if (devices[i].dl_rank != faulty_rank) continue; is_sys_dev = false; - print_message("Rank=%d UUID=" DF_UUIDF " state=%s host=%s tgts=", - devices[i].rank, DP_UUID(devices[i].device_id), - devices[i].state, devices[i].host); - for (j = 0; j < devices[i].n_tgtidx; j++) { - print_message("%d,", devices[i].tgtidx[j]); + print_message( + "Rank=%d UUID=" DF_UUIDF " state=%s host=%s tgts=", devices[i].dl_rank, + DP_UUID(devices[i].dl_device_id), devices[i].dl_state, devices[i].dl_host); + for (j = 0; j < devices[i].dl_n_tgtidx; j++) { + print_message("%d,", devices[i].dl_tgtidx[j]); /* XXX Target 0 and sys xstream share same SSD in md-on-ssd mode */ - if (devices[i].tgtidx[j] == 0 && is_md_on_ssd(devices[i].host)) + if (devices[i].dl_tgtidx[j] == 0 && is_md_on_ssd(devices[i].dl_host)) is_sys_dev = true; } print_message("\n"); @@ -152,7 +153,7 @@ nvme_fault_reaction(void **state, int mode) * Get the Device info of all NVMe devices. */ D_ALLOC_ARRAY(devices, ndisks); - rc = dmg_storage_device_list(dmg_config_file, NULL, devices); + rc = dmg_storage_device_list(dmg_config_file, &ndisks, devices); assert_rc_equal(rc, 0); faulty_disk_idx = pick_faulty_device(devices, ndisks, rank); @@ -161,7 +162,7 @@ nvme_fault_reaction(void **state, int mode) skip(); } /* XXX Convert engine target index to pool target index on rank 0 */ - tgt_idx = devices[faulty_disk_idx].tgtidx[0]; + tgt_idx = devices[faulty_disk_idx].dl_tgtidx[0]; /** * If test need multiple pool with both mode offline and online @@ -260,9 +261,8 @@ nvme_fault_reaction(void **state, int mode) * Continue to check blobstore until state is "OUT" * or max test retry count is hit (5 min). */ - rc = wait_and_verify_blobstore_state( - devices[faulty_disk_idx].device_id, - /*expected state*/"out", arg->group); + rc = wait_and_verify_blobstore_state(devices[faulty_disk_idx].dl_device_id, + /*expected state*/ "out", arg->group); assert_rc_equal(rc, 0); /** @@ -297,10 +297,10 @@ nvme_fault_reaction(void **state, int mode) /** * Verify all mapped device targets are in DOWNOUT state. */ - for (i = 0; i < devices[faulty_disk_idx].n_tgtidx; i++) { + for (i = 0; i < devices[faulty_disk_idx].dl_n_tgtidx; i++) { /* XXX The engine target index is same to the pool target index on rank 0 */ - rc = wait_and_verify_pool_tgt_state(arg->pool.poh, - devices[faulty_disk_idx].tgtidx[i], rank, "DOWNOUT"); + rc = wait_and_verify_pool_tgt_state( + arg->pool.poh, devices[faulty_disk_idx].dl_tgtidx[i], rank, "DOWNOUT"); assert_rc_equal(rc, 0); } print_message("All mapped device targets are in DOWNOUT\n"); @@ -366,12 +366,12 @@ nvme_test_verify_device_stats(void **state) *Get the Device info of all NVMe devices. */ D_ALLOC_ARRAY(devices, ndisks); - rc = dmg_storage_device_list(dmg_config_file, NULL, devices); + rc = dmg_storage_device_list(dmg_config_file, &ndisks, devices); assert_rc_equal(rc, 0); for (i = 0; i < ndisks; i++) - print_message("Rank=%d UUID=" DF_UUIDF " state=%s host=%s\n", - devices[i].rank, DP_UUID(devices[i].device_id), - devices[i].state, devices[i].host); + print_message("Rank=%d UUID=" DF_UUIDF " state=%s host=%s\n", devices[i].dl_rank, + DP_UUID(devices[i].dl_device_id), devices[i].dl_state, + devices[i].dl_host); if (ndisks <= 1) { print_message("Need Minimum 2 disks for test\n"); @@ -394,15 +394,12 @@ nvme_test_verify_device_stats(void **state) */ D_ALLOC(server_config_file, DAOS_SERVER_CONF_LENGTH); D_ALLOC(log_file, 1024); - rc = get_server_config(devices[rank_pos].host, - server_config_file); + rc = get_server_config(devices[rank_pos].dl_host, server_config_file); assert_rc_equal(rc, 0); print_message("server_config_file = %s\n", server_config_file); - get_log_file(devices[rank_pos].host, server_config_file, - "control_log_file", log_file); - rc = verify_server_log_mask(devices[rank_pos].host, - server_config_file, "DEBUG"); + get_log_file(devices[rank_pos].dl_host, server_config_file, "control_log_file", log_file); + rc = verify_server_log_mask(devices[rank_pos].dl_host, server_config_file, "DEBUG"); if (rc) { print_message("Log Mask != DEBUG in %s.\n", server_config_file); @@ -418,11 +415,9 @@ nvme_test_verify_device_stats(void **state) *Set single device for rank0 to faulty. */ print_message("NVMe with UUID=" DF_UUIDF " on host=%s\" set to Faulty\n", - DP_UUID(devices[rank_pos].device_id), - devices[rank_pos].host); - rc = dmg_storage_set_nvme_fault(dmg_config_file, - devices[rank_pos].host, - devices[rank_pos].device_id, 1); + DP_UUID(devices[rank_pos].dl_device_id), devices[rank_pos].dl_host); + rc = dmg_storage_set_nvme_fault(dmg_config_file, devices[rank_pos].dl_host, + devices[rank_pos].dl_device_id); assert_rc_equal(rc, 0); sleep(60); @@ -431,33 +426,30 @@ nvme_test_verify_device_stats(void **state) * Verify "FAULTY -> TEARDOWN" and "TEARDOWN -> OUT" device states found * in server log. */ - rc = dmg_storage_device_list(dmg_config_file, NULL, devices); + rc = dmg_storage_device_list(dmg_config_file, &ndisks, devices); assert_rc_equal(rc, 0); /* The device position could be changed, re-calculate the index */ rank_pos = pick_faulty_device(devices, ndisks, 0); assert_true(rank_pos != -1); - assert_string_equal(devices[rank_pos].state, "\"EVICTED\""); + assert_string_equal(devices[rank_pos].dl_state, "EVICTED"); - rc = verify_state_in_log(devices[rank_pos].host, log_file, - "NORMAL -> FAULTY"); + rc = verify_state_in_log(devices[rank_pos].dl_host, log_file, "NORMAL -> FAULTY"); if (rc != 0) { print_message("NORMAL -> FAULTY not found in log %s\n", log_file); assert_rc_equal(rc, 0); } - rc = verify_state_in_log(devices[rank_pos].host, log_file, - "FAULTY -> TEARDOWN"); + rc = verify_state_in_log(devices[rank_pos].dl_host, log_file, "FAULTY -> TEARDOWN"); if (rc != 0) { print_message("FAULTY -> TEARDOWN not found in %s\n", log_file); assert_rc_equal(rc, 0); } - rc = verify_state_in_log(devices[rank_pos].host, log_file, - "TEARDOWN -> OUT"); + rc = verify_state_in_log(devices[rank_pos].dl_host, log_file, "TEARDOWN -> OUT"); if (rc != 0) { print_message("TEARDOWN -> OUT not found in log %s\n", log_file); @@ -512,7 +504,7 @@ nvme_test_get_blobstore_state(void **state) * Get the Device info of all NVMe devices. */ D_ALLOC_ARRAY(devices, ndisks); - rc = dmg_storage_device_list(dmg_config_file, NULL, devices); + rc = dmg_storage_device_list(dmg_config_file, &ndisks, devices); assert_rc_equal(rc, 0); faulty_disk_idx = pick_faulty_device(devices, ndisks, rank); if (faulty_disk_idx == -1) { @@ -520,7 +512,7 @@ nvme_test_get_blobstore_state(void **state) skip(); } /* XXX Convert engine target index to pool target index on rank 0 */ - tgt_idx = devices[faulty_disk_idx].tgtidx[0]; + tgt_idx = devices[faulty_disk_idx].dl_tgtidx[0]; /** * Set the object class and generate data on objects. @@ -550,8 +542,7 @@ nvme_test_get_blobstore_state(void **state) * Verify blobstore of first device returned is in "NORMAL" state * before setting to faulty. */ - rc = daos_mgmt_get_bs_state(arg->group, - devices[faulty_disk_idx].device_id, + rc = daos_mgmt_get_bs_state(arg->group, devices[faulty_disk_idx].dl_device_id, &blobstore_state, NULL /*ev*/); assert_rc_equal(rc, 0); @@ -564,21 +555,18 @@ nvme_test_get_blobstore_state(void **state) * 'dmg storage set nvme-faulty'. */ print_message("NVMe with UUID=" DF_UUIDF " on host=%s\" set to Faulty\n", - DP_UUID(devices[faulty_disk_idx].device_id), - devices[faulty_disk_idx].host); - rc = dmg_storage_set_nvme_fault(dmg_config_file, - devices[faulty_disk_idx].host, - devices[faulty_disk_idx].device_id, - 1); + DP_UUID(devices[faulty_disk_idx].dl_device_id), + devices[faulty_disk_idx].dl_host); + rc = dmg_storage_set_nvme_fault(dmg_config_file, devices[faulty_disk_idx].dl_host, + devices[faulty_disk_idx].dl_device_id); assert_rc_equal(rc, 0); /** * Continue to check blobstore state until "OUT" state is returned * or max test retry count is hit (5 min). */ - rc = wait_and_verify_blobstore_state(devices[faulty_disk_idx].device_id, - /*expected state*/"out", - arg->group); + rc = wait_and_verify_blobstore_state(devices[faulty_disk_idx].dl_device_id, + /*expected state*/ "out", arg->group); assert_rc_equal(rc, 0); print_message("Blobstore is in OUT state\n"); @@ -601,9 +589,12 @@ nvme_test_simulate_IO_error(void **state) daos_size_t size = 4 * 4096; /* record size */ char *ow_buf; char *fbuf; - char *write_errors; - char *read_errors; - char *check_errors; + /* Scratch buffers for dmg_storage_query_device_health: oversized for + * both the longest key name ("bio_write_errs") and any numeric counter + * value we expect back. */ + char write_errors[32]; + char read_errors[32]; + char check_errors[32]; char *control_log_file; char *server_config_file; int rx_nr; /* number of record extents */ @@ -628,7 +619,7 @@ nvme_test_simulate_IO_error(void **state) * Get the Device info of all NVMe devices */ D_ALLOC_ARRAY(devices, ndisks); - rc = dmg_storage_device_list(dmg_config_file, NULL, devices); + rc = dmg_storage_device_list(dmg_config_file, &ndisks, devices); assert_rc_equal(rc, 0); rank_pos = pick_faulty_device(devices, ndisks, rank); @@ -637,7 +628,7 @@ nvme_test_simulate_IO_error(void **state) skip(); } /* XXX Convert engine target index to pool target index on rank 0 */ - tgt_idx = devices[rank_pos].tgtidx[0]; + tgt_idx = devices[rank_pos].dl_tgtidx[0]; /* * Allocate and set write buffer with data @@ -675,37 +666,37 @@ nvme_test_simulate_IO_error(void **state) D_ASSERT(control_log_file); D_ALLOC(server_config_file, DAOS_SERVER_CONF_LENGTH); D_ASSERT(server_config_file); - rc = get_server_config(devices[rank_pos].host, server_config_file); + rc = get_server_config(devices[rank_pos].dl_host, server_config_file); assert_rc_equal(rc, 0); print_message("server_config_file = %s\n", server_config_file); /* * Get DAOS control log file */ - get_log_file(devices[rank_pos].host, server_config_file, - "control_log_file", control_log_file); + get_log_file(devices[rank_pos].dl_host, server_config_file, "control_log_file", + control_log_file); print_message("Control Log File = %s\n", control_log_file); D_FREE(server_config_file); /* * Get the Initial write error */ - D_STRNDUP_S(write_errors, "bio_write_errs"); - rc = dmg_storage_query_device_health(dmg_config_file, - devices[rank_pos].host, - write_errors, - devices[rank_pos].device_id); + strncpy(write_errors, "bio_write_errs", sizeof(write_errors) - 1); + write_errors[sizeof(write_errors) - 1] = '\0'; + rc = dmg_storage_query_device_health(dmg_config_file, devices[rank_pos].dl_host, + write_errors, sizeof(write_errors), + devices[rank_pos].dl_device_id); assert_rc_equal(rc, 0); print_message("Initial write_errors = %s\n", write_errors); /* * Get the Initial read error */ - D_STRNDUP_S(read_errors, "bio_read_errs"); - rc = dmg_storage_query_device_health(dmg_config_file, - devices[rank_pos].host, - read_errors, - devices[rank_pos].device_id); + strncpy(read_errors, "bio_read_errs", sizeof(read_errors) - 1); + read_errors[sizeof(read_errors) - 1] = '\0'; + rc = + dmg_storage_query_device_health(dmg_config_file, devices[rank_pos].dl_host, read_errors, + sizeof(read_errors), devices[rank_pos].dl_device_id); assert_rc_equal(rc, 0); print_message("Initial read_errors = %s\n", read_errors); @@ -743,11 +734,11 @@ nvme_test_simulate_IO_error(void **state) * Verify the recent write err count is > the initial err count. */ arg->expect_result = 0; - D_STRNDUP_S(check_errors, "bio_write_errs"); - rc = dmg_storage_query_device_health(dmg_config_file, - devices[rank_pos].host, - check_errors, - devices[rank_pos].device_id); + strncpy(check_errors, "bio_write_errs", sizeof(check_errors) - 1); + check_errors[sizeof(check_errors) - 1] = '\0'; + rc = dmg_storage_query_device_health(dmg_config_file, devices[rank_pos].dl_host, + check_errors, sizeof(check_errors), + devices[rank_pos].dl_device_id); assert_rc_equal(rc, 0); print_message("Final write_error = %s\n", check_errors); assert_true(atoi(check_errors) == atoi(write_errors) + 1); @@ -756,11 +747,11 @@ nvme_test_simulate_IO_error(void **state) * Get the read error count after Injecting BIO read error * Verify the recent read err count is > the initial err count. */ - strcpy(check_errors, "bio_read_errs"); - rc = dmg_storage_query_device_health(dmg_config_file, - devices[rank_pos].host, - check_errors, - devices[rank_pos].device_id); + strncpy(check_errors, "bio_read_errs", sizeof(check_errors) - 1); + check_errors[sizeof(check_errors) - 1] = '\0'; + rc = dmg_storage_query_device_health(dmg_config_file, devices[rank_pos].dl_host, + check_errors, sizeof(check_errors), + devices[rank_pos].dl_device_id); assert_rc_equal(rc, 0); print_message("Final read_errors = %s\n", check_errors); assert_true(atoi(check_errors) == atoi(read_errors) + 1); @@ -769,9 +760,6 @@ nvme_test_simulate_IO_error(void **state) D_FREE(ow_buf); D_FREE(fbuf); D_FREE(devices); - D_FREE(write_errors); - D_FREE(read_errors); - D_FREE(check_errors); D_FREE(control_log_file); ioreq_fini(&req); } diff --git a/src/tests/suite/daos_pool.c b/src/tests/suite/daos_pool.c index 354b1117aa2..f5bc68eb6a2 100644 --- a/src/tests/suite/daos_pool.c +++ b/src/tests/suite/daos_pool.c @@ -744,9 +744,7 @@ pool_op_retry(void **state) orig_self_heal); rc = daos_pool_set_prop(arg->pool.pool_uuid, "self_heal", "none"); assert_rc_equal(rc, 0); - char *orig_self_heal_escaped = test_escape_self_heal(orig_self_heal); - rc = daos_pool_set_prop(arg->pool.pool_uuid, "self_heal", orig_self_heal_escaped); - free(orig_self_heal_escaped); + rc = daos_pool_set_prop(arg->pool.pool_uuid, "self_heal", orig_self_heal); assert_rc_equal(rc, 0); print_message("success (restored self_heal to %s)\n", orig_self_heal); free(orig_self_heal); diff --git a/src/tests/suite/daos_test.c b/src/tests/suite/daos_test.c index 18c6fcbab7d..1588a5020b7 100644 --- a/src/tests/suite/daos_test.c +++ b/src/tests/suite/daos_test.c @@ -1,6 +1,6 @@ /** * (C) Copyright 2016-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 */ diff --git a/src/tests/suite/daos_test.h b/src/tests/suite/daos_test.h index 501a943c942..e3608c16be7 100644 --- a/src/tests/suite/daos_test.h +++ b/src/tests/suite/daos_test.h @@ -766,9 +766,6 @@ void void test_set_engine_fail_value(test_arg_t *arg, d_rank_t engine_rank, uint64_t fail_value); void test_set_engine_fail_num(test_arg_t *arg, d_rank_t engine_rank, uint64_t fail_num); -char * -test_escape_self_heal(const char *value); - void test_verify_cont(test_arg_t *arg, struct test_pool *pool, struct test_cont *conts, int cont_nr); diff --git a/src/tests/suite/daos_test_common.c b/src/tests/suite/daos_test_common.c index a28706ed4a4..0685ac09020 100644 --- a/src/tests/suite/daos_test_common.c +++ b/src/tests/suite/daos_test_common.c @@ -1832,41 +1832,3 @@ test_set_engine_fail_num(test_arg_t *arg, d_rank_t engine_rank, uint64_t fail_nu rc = daos_debug_set_params(arg->group, engine_rank, DMG_KEY_FAIL_NUM, fail_num, 0, NULL); assert_rc_equal(rc, 0); } - -/** - * Duplicate unescaped \a value, escaping every ';' with '\\'. The caller is - * responsible for freeing the returned string. - * - * \param[in] value self_heal value to escape - */ -char * -test_escape_self_heal(const char *value) -{ - size_t len = 0; - char *new_value; - const char *src; - char *dst; - - for (src = value; *src != '\0'; src++) { - D_ASSERT(*src != '\\'); - len++; - if (*src == ';') - len++; /* for '\\' */ - } - - D_ALLOC(new_value, len + 1 /* '\0' */); - D_ASSERT(new_value != NULL); - - dst = new_value; - for (src = value; *src != '\0'; src++) { - if (*src == ';') { - *dst++ = '\\'; - *dst++ = ';'; - } else { - *dst++ = *src; - } - } - *dst = '\0'; - - return new_value; -} diff --git a/src/vos/tests/evt_ctl.sh b/src/vos/tests/evt_ctl.sh index 2586b37ebc6..9ef31fdea7e 100755 --- a/src/vos/tests/evt_ctl.sh +++ b/src/vos/tests/evt_ctl.sh @@ -1,7 +1,10 @@ #!/bin/bash -if [ "$USE_VALGRIND" = "memcheck" ]; then VCMD="valgrind --leak-check=full --show-reachable=yes --error-limit=no \ - --suppressions=${VALGRIND_SUPP} --error-exitcode=42 --xml=yes \ +if [ "$USE_VALGRIND" = "memcheck" ]; then + supp_args="--suppressions=${VALGRIND_SUPP}" + [ -n "${VALGRIND_GO_SUPP:-}" ] && supp_args="${supp_args} --suppressions=${VALGRIND_GO_SUPP}" + VCMD="valgrind --leak-check=full --show-reachable=yes --error-limit=no \ + ${supp_args} --error-exitcode=42 --xml=yes \ --xml-file=unit-test-evt_ctl-%p.memcheck.xml" elif [ "$USE_VALGRIND" = "pmemcheck" ]; then VCMD="valgrind --tool=pmemcheck " diff --git a/utils/cq/words.dict b/utils/cq/words.dict index 562e6ea1822..ebcc9beeb7d 100644 --- a/utils/cq/words.dict +++ b/utils/cq/words.dict @@ -101,6 +101,7 @@ bytearray cancelled cd centric +cgo chdir checksum chgrp @@ -204,6 +205,7 @@ getent getfattr getxattr gid +goroutine groupadd groupdel groupname @@ -423,6 +425,7 @@ slurmctld spdk squeue src +srcnode srun ssd ssds @@ -432,6 +435,8 @@ statvfs stddev stderr stdin +stdint +stdlib stdout str stripesize diff --git a/utils/node_local_test.py b/utils/node_local_test.py index 7004d38bb32..a9b9dcfdcdd 100755 --- a/utils/node_local_test.py +++ b/utils/node_local_test.py @@ -457,7 +457,7 @@ def get_base_env(clean=False): def check_memcheck_build(conf): - """Fail early if the daos binary is not valgrind-tagged for a memcheck run.""" + """Fail early if Go binaries are not valgrind-tagged for a memcheck run.""" daos_bin = join(conf['PREFIX'], 'bin', 'daos') with open(daos_bin, 'rb') as fd: if b'runtime.valgrindRegisterStack' not in fd.read(): @@ -465,6 +465,14 @@ def check_memcheck_build(conf): f'{daos_bin} is not built with the Go "valgrind" tag (needs ' 'Go 1.25+ and BUILD_GO_VALGRIND=1), to run under memcheck.') + control_so = join(conf['PREFIX'], 'lib64', 'libdaos_control.so') + if os.path.exists(control_so): + with open(control_so, 'rb') as fd: + if b'runtime.valgrindRegisterStack' not in fd.read(): + raise NLTestFail( + f'{control_so} is not built with the Go "valgrind" tag ' + '(needs BUILD_GO_VALGRIND=1), to run under memcheck.') + class DaosPool(): """Class to store data about daos pools""" @@ -725,9 +733,13 @@ def _start(self): suppression_file = join('src', 'cart', 'utils', 'memcheck-cart.supp') if not os.path.exists(suppression_file): suppression_file = join(self.conf['PREFIX'], 'etc', 'memcheck-cart.supp') - valgrind_args.append(f'--suppressions={os.path.realpath(suppression_file)}') + go_suppression_file = join('src', 'cart', 'utils', 'memcheck-go.supp') + if not os.path.exists(go_suppression_file): + go_suppression_file = join(self.conf['PREFIX'], 'etc', 'memcheck-go.supp') + valgrind_args.append(f'--suppressions={os.path.realpath(go_suppression_file)}') + self._io_server_dir = tempfile.TemporaryDirectory(prefix='dnt_io_') with open(join(self._io_server_dir.name, 'daos_engine'), 'w') as fd: @@ -1310,6 +1322,12 @@ def get_cmd_prefix(self): else: cmd.append(f"--suppressions={join(self.conf['PREFIX'], 'etc', 'memcheck-cart.supp')}") + src_go_suppression_file = join('src', 'cart', 'utils', 'memcheck-go.supp') + if os.path.exists(src_go_suppression_file): + cmd.append(f'--suppressions={src_go_suppression_file}') + else: + cmd.append(f"--suppressions={join(self.conf['PREFIX'], 'etc', 'memcheck-go.supp')}") + return cmd def add_memcheck_env(self, env): diff --git a/utils/rpms/daos.sh b/utils/rpms/daos.sh index a71991cc525..5299ac4a5ab 100755 --- a/utils/rpms/daos.sh +++ b/utils/rpms/daos.sh @@ -368,7 +368,8 @@ clean_bin "${files[@]}" append_install_list "${files[@]}" TARGET_PATH="${libdir}" -list_files files "${SL_PREFIX}/lib64/libdaos_tests.so" \ +list_files files "${SL_PREFIX}/lib64/libdaos_control.so" \ + "${SL_PREFIX}/lib64/libdaos_tests.so" \ "${SL_PREFIX}/lib64/libdpar.so" clean_bin "${files[@]}" append_install_list "${files[@]}" diff --git a/utils/rpms/daos.spec b/utils/rpms/daos.spec index 9adc6ec0bbc..cdffcbce618 100644 --- a/utils/rpms/daos.spec +++ b/utils/rpms/daos.spec @@ -568,6 +568,7 @@ fi %{daoshome}/TESTING %exclude %{daoshome}/TESTING/ftest/avocado_tests.yaml %{_bindir}/hello_drpc +%{_libdir}/libdaos_control.so %{_libdir}/libdaos_tests.so %{_bindir}/acl_dump_test %{_bindir}/agent_tests diff --git a/utils/run_utest.py b/utils/run_utest.py index f16288d4d79..7d262a60a7a 100755 --- a/utils/run_utest.py +++ b/utils/run_utest.py @@ -135,16 +135,33 @@ def get_supp(base): """Get suppression file""" return os.path.join(base, 'utils', 'test_memcheck.supp') + @staticmethod + def get_go_supp(base): + """Get Go-runtime suppression file (shared with NLT)""" + return os.path.join(base, 'src', 'cart', 'utils', 'memcheck-go.supp') + @staticmethod def setup_cmd(base, cmd, name): """Return a new command using valgrind""" cmd_prefix = ["valgrind", "--leak-check=full", "--show-reachable=yes", "--num-callers=20", "--error-limit=no", "--fair-sched=try", f"--suppressions={ValgrindHelper.get_supp(base)}", + f"--suppressions={ValgrindHelper.get_go_supp(base)}", "--gen-suppressions=all", "--error-exitcode=42", "--xml=yes", f"--xml-file={ValgrindHelper.get_xml_name(name)}"] return cmd_prefix + cmd + @staticmethod + def set_godebug(env): + """Set GODEBUG=asyncpreemptoff=1 for memcheck runs. + + Async preemption signals can write to goroutine stacks mid-instruction, + producing spurious Memcheck:Addr/Cond reports that the valgrind tag + alone does not eliminate. + """ + godebug = env.get('GODEBUG') + env['GODEBUG'] = f'{godebug},asyncpreemptoff=1' if godebug else 'asyncpreemptoff=1' + def run_cmd(cmd, output_log=None, env=None): """Run a command""" @@ -440,9 +457,11 @@ def run(self, base, memcheck, sudo): """Run the test""" cmd = [os.path.join(base, self.cmd[0])] + self.cmd[1:] if memcheck: + ValgrindHelper.set_godebug(self.env) if os.path.splitext(cmd[0])[-1] in [".sh", ".py"]: self.env.update({"USE_VALGRIND": "memcheck", - "VALGRIND_SUPP": ValgrindHelper.get_supp(self.root_dir())}) + "VALGRIND_SUPP": ValgrindHelper.get_supp(self.root_dir()), + "VALGRIND_GO_SUPP": ValgrindHelper.get_go_supp(self.root_dir())}) else: cmd = ValgrindHelper.setup_cmd(self.root_dir(), cmd, self.name) if sudo: diff --git a/utils/test_memcheck.supp b/utils/test_memcheck.supp index 2f83a412f34..4202a0d1551 100644 --- a/utils/test_memcheck.supp +++ b/utils/test_memcheck.supp @@ -439,6 +439,7 @@ ... fun:runtime.persistentalloc } + { DAOS-16866 Memcheck:Leak