From aa22df5984d4de18452326c777c4e2c3eea33d42 Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Wed, 4 Feb 2026 12:30:11 -0500 Subject: [PATCH] DAOS-16311 control: Add C bindings for control API Use Go's c-shared-library mode to build a .so that exposes a C API for the Go Control API. The initial use case for this library is to replace the problematic dmg helpers test library that uses fork/exec to allow tests to drive dmg commands. Signed-off-by: Michael MacDonald --- site_scons/site_tools/go_builder.py | 6 +- src/SConscript | 2 + src/common/SConscript | 6 +- src/common/tests_dmg_helpers.c | 2159 ++--------------- src/control/lib/control/c/SConscript | 82 + src/control/lib/control/c/bindings.go | 58 + src/control/lib/control/c/bindings_test.go | 63 + src/control/lib/control/c/check.go | 316 +++ src/control/lib/control/c/check_test.go | 330 +++ src/control/lib/control/c/context.go | 103 + src/control/lib/control/c/convert.go | 172 ++ src/control/lib/control/c/daos_control_util.h | 42 + src/control/lib/control/c/errors.go | 53 + src/control/lib/control/c/errors_test.go | 80 + src/control/lib/control/c/fi.go | 76 + src/control/lib/control/c/no_fi.go | 30 + src/control/lib/control/c/pool.go | 516 ++++ src/control/lib/control/c/pool_test.go | 1022 ++++++++ src/control/lib/control/c/server.go | 62 + src/control/lib/control/c/server_test.go | 56 + src/control/lib/control/c/storage.go | 282 +++ src/control/lib/control/c/storage_ops_test.go | 154 ++ src/control/lib/control/c/storage_test.go | 124 + src/control/lib/control/c/system.go | 116 + src/control/lib/control/c/system_test.go | 213 ++ src/control/lib/control/c/test_helpers.go | 779 ++++++ src/control/lib/control/check.go | 9 +- src/include/daos/control_types.h | 86 + src/include/daos/tests_lib.h | 58 +- src/tests/SConscript | 4 + 30 files changed, 5085 insertions(+), 1974 deletions(-) create mode 100644 src/control/lib/control/c/SConscript create mode 100644 src/control/lib/control/c/bindings.go create mode 100644 src/control/lib/control/c/bindings_test.go create mode 100644 src/control/lib/control/c/check.go create mode 100644 src/control/lib/control/c/check_test.go create mode 100644 src/control/lib/control/c/context.go create mode 100644 src/control/lib/control/c/convert.go create mode 100644 src/control/lib/control/c/daos_control_util.h create mode 100644 src/control/lib/control/c/errors.go create mode 100644 src/control/lib/control/c/errors_test.go create mode 100644 src/control/lib/control/c/fi.go create mode 100644 src/control/lib/control/c/no_fi.go create mode 100644 src/control/lib/control/c/pool.go create mode 100644 src/control/lib/control/c/pool_test.go create mode 100644 src/control/lib/control/c/server.go create mode 100644 src/control/lib/control/c/server_test.go create mode 100644 src/control/lib/control/c/storage.go create mode 100644 src/control/lib/control/c/storage_ops_test.go create mode 100644 src/control/lib/control/c/storage_test.go create mode 100644 src/control/lib/control/c/system.go create mode 100644 src/control/lib/control/c/system_test.go create mode 100644 src/control/lib/control/c/test_helpers.go create mode 100644 src/include/daos/control_types.h diff --git a/site_scons/site_tools/go_builder.py b/site_scons/site_tools/go_builder.py index e6657b3b314..8c1b2a73f66 100644 --- a/site_scons/site_tools/go_builder.py +++ b/site_scons/site_tools/go_builder.py @@ -43,7 +43,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..59f6332ba7b 100644 --- a/src/SConscript +++ b/src/SConscript @@ -72,6 +72,7 @@ def scons(): env.Install(os.path.join('$PREFIX', 'include/daos'), 'include/daos/profile.h') env.Install(os.path.join('$PREFIX', 'include/daos'), 'include/daos/dtx.h') env.Install(os.path.join('$PREFIX', 'include/daos'), 'include/daos/cmd_parser.h') + env.Install(os.path.join('$PREFIX', 'include/daos'), 'include/daos/control_types.h') # Generic DAOS includes env.AppendUnique(CPPPATH=[Dir('include').srcnode()]) @@ -104,6 +105,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/common/SConscript b/src/common/SConscript index 83f248e7d31..550616c8a2a 100644 --- a/src/common/SConscript +++ b/src/common/SConscript @@ -75,11 +75,15 @@ def scons(): if not prereqs.test_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']) + tlibenv.Requires(tests_lib, daos_control_tgts) tlibenv.Install('$PREFIX/lib64/', tests_lib) tenv = denv.Clone() diff --git a/src/common/tests_dmg_helpers.c b/src/common/tests_dmg_helpers.c index 851119e8f88..16e71ddea1d 100644 --- a/src/common/tests_dmg_helpers.c +++ b/src/common/tests_dmg_helpers.c @@ -7,573 +7,46 @@ #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. - */ -#define D_LOG_DMG_STDERR_ENV "D_TEST_LOG_DMG_STDERR" - -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) -{ - 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; +/* Handle for libdaos_control context */ +static uintptr_t dmg_ctx; - 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); - } - - return rc; -} - -static int -parse_pool_info(struct json_object *json_pool, daos_mgmt_pool_info_t *pool_info) +int +dmg_init(const char *dmg_config_file) { - struct json_object *tmp, *rank; - int n_svcranks; - const char *uuid_str; - int i, rc; + struct daos_control_init_args args = { + .config_file = dmg_config_file, + .log_file = "/tmp/suite_dmg.log", + .log_level = "debug", + }; + int rc; - if (json_pool == NULL || pool_info == NULL) - return -DER_INVAL; + if (dmg_ctx != 0) + return 0; /* Already initialized */ - 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); + rc = daos_control_init(&args, &dmg_ctx); if (rc != 0) { - D_ERROR("failed parsing uuid_str\n"); - return -DER_INVAL; + D_ERROR("daos_control_init failed: %d\n", rc); + dmg_ctx = 0; } - 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); - -out: return rc; } -static int -parse_dmg_string(struct json_object *obj, const char *key, char **tgt) -{ - 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; -} - -static int -parse_dmg_uuid(struct json_object *obj, const char *key, uuid_t uuid) +void +dmg_fini(void) { - 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; - } - - str = json_object_get_string(tmp); - if (str == NULL) { - D_ERROR("Got empty %s from check query result\n", key); - return -DER_INVAL; + if (dmg_ctx != 0) { + daos_control_fini(dmg_ctx); + dmg_ctx = 0; } - - rc = uuid_parse(str, uuid); - if (rc != 0) - D_ERROR("Failed to parse uuid %s from check query result\n", str); - - return rc; } int @@ -581,87 +54,31 @@ 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; - } + int rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: - return rc; + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; + + return daos_control_pool_set_prop(dmg_ctx, (uuid_t *)pool_uuid, (char *)prop_name, + (char *)prop_value); } int 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; + 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); - - cmd_free_args(args, argcount); + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; -out: - return rc; + return daos_control_pool_get_prop(dmg_ctx, (char *)label, (uuid_t *)uuid, (char *)name, + value); } int @@ -672,927 +89,263 @@ dmg_pool_create(const char *dmg_config_file, 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); + struct daos_prop_entry *entry; + daos_prop_t *new_prop = NULL; + bool has_label = false; + int 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); - } - - passwd = getpwuid(uid); - if (passwd == NULL) { - D_ERROR("unable to resolve %d to passwd entry\n", uid); - D_GOTO(out_cmd, rc = -DER_INVAL); - } - - args = cmd_push_arg(args, &argcount, - "--user=%s ", passwd->pw_name); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - group = getgrgid(gid); - if (group == NULL) { - D_ERROR("unable to resolve %d to group name\n", gid); - D_GOTO(out_cmd, rc = -DER_INVAL); - } - - 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) - 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 (rc != 0) { - D_ERROR("failed to write ACL to tmpfile\n"); - goto out_cmd; - } - args = cmd_push_arg(args, &argcount, - "--acl-file=%s ", tmp_name); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; + /* Check if label property already exists */ + if (prop != NULL && prop->dpp_nr > 0) { 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) - D_GOTO(out, rc = -DER_NOMEM); + if (entry != NULL) 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); - } - } - - 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); - } - } - - /* 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]; + char path[] = "/tmp/test_XXXXXX"; + char label[DAOS_PROP_LABEL_MAX_LEN + 1]; + int tmp_fd; /* 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)); + D_ERROR("failed to generate unique label: %s\n", strerror(errno)); + return 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; + /* Copy label portion (after /tmp/) to properly sized buffer */ + strncpy(label, &path[5], sizeof(label) - 1); + label[sizeof(label) - 1] = '\0'; + + /* Create new prop with label - treat empty prop same as NULL */ + if (prop == NULL || prop->dpp_nr == 0) { + new_prop = daos_prop_alloc(1); + if (new_prop == NULL) + return -DER_NOMEM; + new_prop->dpp_entries[0].dpe_type = DAOS_PROP_PO_LABEL; + D_STRNDUP(new_prop->dpp_entries[0].dpe_str, label, DAOS_PROP_LABEL_MAX_LEN); + if (new_prop->dpp_entries[0].dpe_str == NULL) { + daos_prop_free(new_prop); + return -DER_NOMEM; + } + } else { + /* Copy existing props and add label */ + new_prop = daos_prop_alloc(prop->dpp_nr + 1); + if (new_prop == NULL) + return -DER_NOMEM; + rc = daos_prop_copy(new_prop, prop); + if (rc != 0) { + daos_prop_free(new_prop); + return rc; + } + new_prop->dpp_entries[prop->dpp_nr].dpe_type = DAOS_PROP_PO_LABEL; + D_STRNDUP(new_prop->dpp_entries[prop->dpp_nr].dpe_str, label, + DAOS_PROP_LABEL_MAX_LEN); + if (new_prop->dpp_entries[prop->dpp_nr].dpe_str == NULL) { + daos_prop_free(new_prop); + return -DER_NOMEM; + } + } + prop = new_prop; } - rc = parse_pool_info(dmg_out, &pool_info); - if (rc != 0) { - D_ERROR("failed to parse pool info\n"); - goto out_json; - } + rc = daos_control_pool_create(dmg_ctx, uid, gid, (char *)grp, (d_rank_list_t *)tgts, + scm_size, nvme_size, prop, svc, (uuid_t *)uuid); - uuid_copy(uuid, pool_info.mgpi_uuid); - if (svc == NULL) - goto out_svc; + if (new_prop != NULL) + daos_prop_free(new_prop); - 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; - } - -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); 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); - } + 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_init(dmg_config_file); + if (rc != 0) + return rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: - return rc; + return daos_control_pool_destroy(dmg_ctx, (uuid_t *)uuid, (char *)grp, force); } 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; - } + int rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: - return rc; + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; + + return daos_control_pool_evict(dmg_ctx, (uuid_t *)uuid, (char *)grp); } 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; - } + int rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: - return rc; + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; + + return daos_control_pool_update_ace(dmg_ctx, (uuid_t *)uuid, (char *)grp, (char *)ace); } 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); - } - - if (tgt_idx >= 0) { - args = cmd_push_arg(args, &argcount, "--target-idx=%d ", tgt_idx); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + int 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_init(dmg_config_file); + if (rc != 0) + return rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: - return rc; + return daos_control_pool_delete_ace(dmg_ctx, (uuid_t *)uuid, (char *)grp, + (char *)principal); } 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 rc; + + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; + + return daos_control_pool_exclude(dmg_ctx, (uuid_t *)uuid, (char *)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 rc; + + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; + + return daos_control_pool_reintegrate(dmg_ctx, (uuid_t *)uuid, (char *)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) { - return dmg_pool_target("pool drain", dmg_config_file, uuid, grp, rank, tgt_idx); -} + 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_init(dmg_config_file); 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); + return rc; - if (grp != NULL) { - args = cmd_push_arg(args, &argcount, "--sys=%s ", grp); - if (args == NULL) - D_GOTO(out_rankstr, rc = -DER_NOMEM); - } + return daos_control_pool_drain(dmg_ctx, (uuid_t *)uuid, (char *)grp, rank, tgt_idx); +} - args = cmd_push_arg(args, &argcount, "--ranks=%s ", rank_str); - if (args == NULL) - D_GOTO(out_rankstr, rc = -DER_NOMEM); +int +dmg_pool_extend(const char *dmg_config_file, const uuid_t uuid, + const char *grp, d_rank_t *ranks, int rank_nr) +{ + int rc; - 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; - } + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out_rankstr: - D_FREE(rank_str); -out: - return rc; + return daos_control_pool_extend(dmg_ctx, (uuid_t *)uuid, (char *)grp, ranks, rank_nr); } int dmg_pool_list(const char *dmg_config_file, const char *group, daos_size_t *npools, daos_mgmt_pool_info_t *pools) { - 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; + 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; - } -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; - return rc; + return daos_control_pool_list(dmg_ctx, (char *)group, npools, pools); } int dmg_pool_rebuild_stop(const char *dmg_config_file, const uuid_t uuid, const char *grp, bool 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); - - if (grp != NULL) { - args = cmd_push_arg(args, &argcount, "--sys=%s ", grp); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } - - if (force) { - args = cmd_push_arg(args, &argcount, "--force"); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + int rc; - 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; - } + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: - return rc; + return daos_control_pool_rebuild_stop(dmg_ctx, (uuid_t *)uuid, (char *)grp, force ? 1 : 0); } 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 = 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; - } - -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: - return rc; -} - -static int -parse_device_info(struct json_object *smd_dev, device_list *devices, - char *host, int dev_length, int *disks) -{ - 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; - } - - snprintf(devices[*disks].host, sizeof(devices[*disks].host), - "%s", tmp_var + 1); - - if (!json_object_object_get_ex(dev, "uuid", &tmp)) { - D_ERROR("unable to extract uuid from JSON\n"); - return -DER_INVAL; - } - - 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; - } - - if (!json_object_object_get_ex(dev, "tgt_ids", - &targets)) { - D_ERROR("unable to extract tgtids from JSON\n"); - return -DER_INVAL; - } - - if (targets != NULL) - tgts_len = json_object_array_length(targets); - else - tgts_len = 0; - - 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; + int 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)); - - if (!json_object_object_get_ex(dev, "ctrlr", &ctrlr)) { - D_ERROR("unable to extract ctrlr obj from JSON\n"); - return -DER_INVAL; - } - - if (!json_object_object_get_ex(ctrlr, "dev_state", &tmp)) { - D_ERROR("unable to extract state from JSON\n"); - return -DER_INVAL; - } - - snprintf(devices[*disks].state, sizeof(devices[*disks].state), "%s", - json_object_to_json_string(tmp)); - *disks = *disks + 1; - } + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; - return 0; + return daos_control_pool_rebuild_start(dmg_ctx, (uuid_t *)uuid, (char *)grp); } int dmg_storage_device_list(const char *dmg_config_file, int *ndisks, device_list *devices) { - 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; - } - - 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); - } - - json_object_object_foreach(storage_map, key, val) { - D_DEBUG(DB_TEST, "key:\"%s\",val=%s\n", key, - json_object_to_json_string(val)); - - if (!json_object_object_get_ex(val, "hosts", &hosts)) { - D_ERROR("unable to extract hosts from JSON\n"); - D_GOTO(out, rc = -DER_INVAL); - } + int 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); - } - -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; -out: - D_FREE(disk); - return rc; + return daos_control_storage_device_list(dmg_ctx, ndisks, devices); } int dmg_storage_set_nvme_fault(const char *dmg_config_file, char *host, const uuid_t uuid, 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, " --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); - } + int rc; - args = cmd_push_arg(args, &argcount, " --host=%s ", host); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - - 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_init(dmg_config_file); + if (rc != 0) + return rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: - return rc; + return daos_control_storage_set_nvme_fault(dmg_ctx, host, (uuid_t *)uuid, force); } int dmg_storage_query_device_health(const char *dmg_config_file, char *host, char *stats, 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); - } - - json_object_object_foreach(storage_map, key, val) { - D_DEBUG(DB_TEST, "key:\"%s\",val=%s\n", key, - json_object_to_json_string(val)); - - 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); - } + int rc; - 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_init(dmg_config_file); + if (rc != 0) + return rc; -out_json: - if (dmg_out != NULL) - json_object_put(dmg_out); - cmd_free_args(args, argcount); -out: - return rc; + /* stats is used both as input (key name) and output (value) */ + return daos_control_storage_query_device_health(dmg_ctx, host, stats, stats, 256, + (uuid_t *)uuid); } int verify_blobstore_state(int state, const char *state_str) @@ -1627,159 +380,72 @@ int verify_blobstore_state(int state, const char *state_str) 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); - } + int rc; - if (force != 0) { - args = cmd_push_arg(args, &argcount, " --force "); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + if (rank == CRT_NO_RANK) + return -DER_INVAL; - rc = daos_dmg_json_pipe("system stop", dmg_config_file, - args, argcount, &dmg_out); + rc = dmg_init(dmg_config_file); if (rc != 0) - D_ERROR("dmg failed\n"); - - if (dmg_out != NULL) - json_object_put(dmg_out); + return rc; - cmd_free_args(args, argcount); -out: - return rc; + return daos_control_system_stop_rank(dmg_ctx, rank, force); } 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) { - args = cmd_push_arg(args, &argcount, " -r %d ", rank); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); - } + int rc; - rc = daos_dmg_json_pipe("system start", dmg_config_file, - args, argcount, &dmg_out); - if (rc != 0) - D_ERROR("dmg failed\n"); + if (rank == CRT_NO_RANK) + return -DER_INVAL; - if (dmg_out != NULL) - json_object_put(dmg_out); + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; - cmd_free_args(args, argcount); -out: - return rc; + return daos_control_system_start_rank(dmg_ctx, rank); } int 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; + int rc; if (rank == CRT_NO_RANK) - D_GOTO(out, rc = -DER_INVAL); - - args = cmd_push_arg(args, &argcount, " -r %d ", rank); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); + return -DER_INVAL; - rc = daos_dmg_json_pipe("system clear-exclude", dmg_config_file, - args, argcount, &dmg_out); + rc = dmg_init(dmg_config_file); if (rc != 0) - D_ERROR("dmg system clear-exclude failed\n"); - - if (dmg_out != NULL) - json_object_put(dmg_out); - - cmd_free_args(args, argcount); + return rc; -out: - return rc; + return daos_control_system_reint_rank(dmg_ctx, rank); } int dmg_system_exclude_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; + int rc; if (rank == CRT_NO_RANK) - D_GOTO(out, rc = -DER_INVAL); - - args = cmd_push_arg(args, &argcount, " -r %d ", rank); - if (args == NULL) - D_GOTO(out, rc = -DER_NOMEM); + return -DER_INVAL; - rc = daos_dmg_json_pipe("system exclude", dmg_config_file, - args, argcount, &dmg_out); + rc = dmg_init(dmg_config_file); if (rc != 0) - D_ERROR("dmg system exclude failed\n"); - - if (dmg_out != NULL) - json_object_put(dmg_out); - - cmd_free_args(args, argcount); + return rc; -out: - return rc; + return daos_control_system_exclude_rank(dmg_ctx, rank); } int dmg_server_set_logmasks(const char *dmg_config_file, const char *masks, const char *streams, const char *subsystems) { - 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); - } + int rc; - /* 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); - } - - /* 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_init(dmg_config_file); if (rc != 0) - D_ERROR("dmg failed\n"); - - if (dmg_out != NULL) - json_object_put(dmg_out); + return rc; - cmd_free_args(args, argcount); -out: - return rc; + return daos_control_server_set_logmasks(dmg_ctx, (char *)masks, (char *)streams, + (char *)subsystems); } const char * @@ -1800,455 +466,84 @@ daos_target_state_enum_to_str(int state) int 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; - - 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); + int rc; - if (dmg_out != NULL) - json_object_put(dmg_out); - - cmd_free_args(args, argcount); + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; -out: - return rc; + return daos_control_fault_inject(dmg_ctx, (uuid_t *)uuid, mgmt ? 1 : 0, (char *)fault); } int dmg_check_switch(const char *dmg_config_file, bool enable) { - 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); - - if (dmg_out != NULL) - json_object_put(dmg_out); + int rc; - cmd_free_args(args, argcount); + rc = dmg_init(dmg_config_file); + if (rc != 0) + return rc; -out: - return rc; + return daos_control_check_switch(dmg_ctx, enable ? 1 : 0); } int dmg_check_start(const char *dmg_config_file, uint32_t flags, uint32_t pool_nr, uuid_t uuids[], const char *policies) { - 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); - } + int rc; - 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); - } - - rc = daos_dmg_json_pipe("check start", dmg_config_file, args, argcount, &dmg_out); + rc = dmg_init(dmg_config_file); 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 rc; + return daos_control_check_start(dmg_ctx, flags, pool_nr, uuids, (char *)policies); } int dmg_check_stop(const char *dmg_config_file, uint32_t pool_nr, uuid_t uuids[]) { - 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); - } - - rc = daos_dmg_json_pipe("check stop", dmg_config_file, args, argcount, &dmg_out); - if (rc != 0) - D_ERROR("dmg check stop failed: %d\n", rc); - - if (dmg_out != NULL) - json_object_put(dmg_out); - - cmd_free_args(args, argcount); - -out: - return rc; -} - -static int -check_query_reports_cmp(const void *p1, const void *p2) -{ - const struct daos_check_report_info *dcri1 = p1; - const struct daos_check_report_info *dcri2 = p2; - - if (dcri1->dcri_class > dcri2->dcri_class) - return 1; - - if (dcri1->dcri_class < dcri2->dcri_class) - return -1; - - return 0; -} - -static int -parse_check_query_pool(struct json_object *obj, uuid_t uuid, struct daos_check_info *dci) -{ - 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); - - /* 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); - if (rc != 0) - return rc; - - rc = parse_dmg_string(pool, "phase", &dcpi->dcpi_phase); - if (rc == 0) - dci->dci_pool_nr++; - - return rc; -} - -static int -parse_check_query_report(struct json_object *obj, struct daos_check_report_info *dcri) -{ - struct json_object *tmp; - int rc; - int i; - - rc = parse_dmg_uuid(obj, "pool_uuid", dcri->dcri_uuid); - 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, "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); - - 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; -} - -static int -parse_check_query_info(struct json_object *query_output, uint32_t pool_nr, uuid_t uuids[], - struct daos_check_info *dci) -{ - struct json_object *obj; - int i; - int rc; - - rc = parse_dmg_string(query_output, "status", &dci->dci_status); - if (rc != 0) - return rc; + int rc; - rc = parse_dmg_string(query_output, "scan_phase", &dci->dci_phase); + rc = dmg_init(dmg_config_file); if (rc != 0) return rc; - 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; - } - - /* Sort the inconsistency reports for easy verification. */ - if (dci->dci_report_nr > 1) - qsort(dci->dci_reports, dci->dci_report_nr, sizeof(dci->dci_reports[0]), - check_query_reports_cmp); - - return 0; + return daos_control_check_stop(dmg_ctx, pool_nr, uuids); } 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); - } + int rc; - rc = daos_dmg_json_pipe("check query", dmg_config_file, args, argcount, &dmg_out); + rc = dmg_init(dmg_config_file); 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); - - cmd_free_args(args, argcount); + return rc; -out: - return rc; + return daos_control_check_query(dmg_ctx, pool_nr, uuids, dci); } 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; + 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_init(dmg_config_file); 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); - - cmd_free_args(args, argcount); + return rc; -out: - return rc; + return daos_control_check_repair(dmg_ctx, seq, opt); } 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); - } + int rc; - 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); - } - - rc = daos_dmg_json_pipe("check set-policy", dmg_config_file, args, argcount, &dmg_out); + rc = dmg_init(dmg_config_file); 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); - - cmd_free_args(args, argcount); + return rc; -out: - return rc; + return daos_control_check_set_policy(dmg_ctx, flags, (char *)policies); } diff --git a/src/control/lib/control/c/SConscript b/src/control/lib/control/c/SConscript new file mode 100644 index 00000000000..8e78c0ce7d7 --- /dev/null +++ b/src/control/lib/control/c/SConscript @@ -0,0 +1,82 @@ +"""Build DAOS Control C Bindings Shared Library""" +import os +from os.path import join + + +def scons(): + """Execute build""" + Import('env', 'base_env', 'prereqs') + + # Add library and include paths so other components can find libdaos_control + 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') + + # Sets CGO_LDFLAGS for rpath options + cenv.d_add_rpaths("..", True, True) + cenv.require('protobufc') + cenv.AppendENVPath("CGO_CFLAGS", cenv.subst("$_CPPINCFLAGS"), sep=" ") + + # Add library paths for CGO link step + 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')) + + # Get version for ldflags + Import('daos_version', 'conf_dir') + ldflags_path = 'github.com/daos-stack/daos/src/control/build' + ldflags = f'-X {ldflags_path}.DaosVersion={daos_version} -X {ldflags_path}.ConfigDir={conf_dir}' + + # Build as a C shared library using cgo (generates both .so and .h) + cmd = (f'cd {gosrc} && {cenv.d_go_bin} build -mod vendor ' + f'-ldflags "{ldflags}" ' + f'-buildmode=c-shared ' + f'-o {build_lib} .') + + # Collect Go source files and C headers from source directory as dependencies. + # The Go scanner will find additional dependencies (other Go packages, local C files). + # Use srcnode to get proper SCons File nodes from source dir, not build dir. + srcdir = Dir('.').srcnode() + sources = [srcdir.File(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], sources, cmd) + lib_target = targets[0] + hdr_target = targets[1] + + # Ensure library dependencies are built first + cenv.Requires(targets, cenv.subst('$BUILD_DIR/src/common/libdaos_common.so')) + cenv.Requires(targets, cenv.subst('$BUILD_DIR/src/cart/libcart.so')) + cenv.Requires(targets, cenv.subst('$BUILD_DIR/src/gurt/libgurt.so')) + + # Install the shared library and generated header + cenv.Install('$PREFIX/lib64', lib_target) + cenv.InstallAs('$PREFIX/include/daos/control.h', hdr_target) + + # Export targets so other components can declare dependencies + 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..862d7735a94 --- /dev/null +++ b/src/control/lib/control/c/bindings.go @@ -0,0 +1,58 @@ +// +// (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 + +// daos_control_init_args contains initialization options. +// All fields are optional; NULL/empty uses defaults. +struct daos_control_init_args { + const char *config_file; // Path to dmg config file (NULL for default/insecure) + const char *log_file; // Path to log file (NULL disables logging) + const char *log_level; // Log level: debug, info, notice, error (NULL for notice) +}; +*/ +import "C" +import ( + "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) C.int { + var cfgPath, logFilePath, logLevelStr string + if args != nil { + cfgPath = goString(args.config_file) + logFilePath = goString(args.log_file) + logLevelStr = goString(args.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) { + if handle == 0 { + return + } + ctx := cgo.Handle(handle).Value().(*ctrlContext) + ctx.close() + cgo.Handle(handle).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..ea6abf206cc --- /dev/null +++ b/src/control/lib/control/c/bindings_test.go @@ -0,0 +1,63 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestInitFini(t *testing.T) { + // Test with default config (nil args uses insecure localhost) + handle, rc := callInit("", "", "") + if rc != 0 { + t.Fatalf("expected RC 0, got %d", rc) + } + if handle == 0 { + t.Fatal("expected non-zero handle") + } + + // Clean up + callFini(handle) +} + +func TestInitWithLogging(t *testing.T) { + // Create temp log file + tmpDir := t.TempDir() + logFile := filepath.Join(tmpDir, "test.log") + + handle, rc := callInit("", logFile, "debug") + if rc != 0 { + t.Fatalf("expected RC 0, got %d", rc) + } + if handle == 0 { + t.Fatal("expected non-zero handle") + } + + // Clean up + callFini(handle) + + // Verify log file was created + if _, err := os.Stat(logFile); os.IsNotExist(err) { + t.Fatal("expected log file to be created") + } +} + +func TestInitInvalidConfig(t *testing.T) { + // Test with non-existent config file + handle, rc := callInit("/nonexistent/config/file.yml", "", "") + if rc == 0 { + callFini(handle) + t.Fatal("expected error for non-existent config file, got success") + } +} + +func TestFiniZeroHandle(t *testing.T) { + // Should not panic with zero handle + callFini(0) +} diff --git a/src/control/lib/control/c/check.go b/src/control/lib/control/c/check.go new file mode 100644 index 00000000000..73c9178fc8f --- /dev/null +++ b/src/control/lib/control/c/check.go @@ -0,0 +1,316 @@ +// +// (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 ( + "runtime/cgo" + "strings" + "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_check_switch +func daos_control_check_switch( + handle C.uintptr_t, + enable C.int, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + var err error + if enable != 0 { + req := &control.SystemCheckEnableReq{} + err = control.SystemCheckEnable(ctx.ctx(), ctx.client, req) + } else { + req := &control.SystemCheckDisableReq{} + err = control.SystemCheckDisable(ctx.ctx(), ctx.client, req) + } + + return C.int(errorToRC(err)) +} + +//export daos_control_check_start +func daos_control_check_start( + handle C.uintptr_t, + flags C.uint32_t, + poolNr C.uint32_t, + uuids *C.uuid_t, + policies *C.char, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.SystemCheckStartReq{} + req.CheckStartReq.Flags = uint32(flags) + + // Convert pool UUIDs if provided + if uuids != nil && poolNr > 0 { + uuidSlice := unsafe.Slice(uuids, poolNr) + for i := uint32(0); i < uint32(poolNr); i++ { + u := uuidFromC(&uuidSlice[i]) + if u != uuid.Nil { + req.CheckStartReq.Uuids = append(req.CheckStartReq.Uuids, u.String()) + } + } + } + + // Parse policies string if provided (format: "CLASS:ACTION,CLASS:ACTION,...") + if policies != nil { + policiesStr := goString(policies) + if policiesStr != "" { + for _, policyStr := range strings.Split(policiesStr, ",") { + policyStr = strings.TrimSpace(policyStr) + if policyStr == "" { + continue + } + parts := strings.SplitN(policyStr, ":", 2) + if len(parts) != 2 { + return C.int(daos.InvalidInput) + } + policy, err := control.NewSystemCheckPolicy(parts[0], parts[1]) + if err != nil { + return C.int(daos.InvalidInput) + } + req.Policies = append(req.Policies, policy) + } + } + } + + err := control.SystemCheckStart(ctx.ctx(), ctx.client, req) + return C.int(errorToRC(err)) +} + +//export daos_control_check_stop +func daos_control_check_stop( + handle C.uintptr_t, + poolNr C.uint32_t, + uuids *C.uuid_t, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.SystemCheckStopReq{} + + // Convert pool UUIDs if provided + if uuids != nil && poolNr > 0 { + uuidSlice := unsafe.Slice(uuids, poolNr) + for i := uint32(0); i < uint32(poolNr); i++ { + u := uuidFromC(&uuidSlice[i]) + if u != uuid.Nil { + req.CheckStopReq.Uuids = append(req.CheckStopReq.Uuids, u.String()) + } + } + } + + err := control.SystemCheckStop(ctx.ctx(), ctx.client, req) + return C.int(errorToRC(err)) +} + +//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 { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.SystemCheckQueryReq{} + + // Build list of UUIDs to query + var queryUUIDs []string + if uuids != nil && poolNr > 0 { + uuidSlice := unsafe.Slice(uuids, poolNr) + for i := uint32(0); i < uint32(poolNr); i++ { + u := uuidFromC(&uuidSlice[i]) + if u != uuid.Nil { + queryUUIDs = append(queryUUIDs, u.String()) + req.CheckQueryReq.Uuids = append(req.CheckQueryReq.Uuids, u.String()) + } + } + } + + resp, err := control.SystemCheckQuery(ctx.ctx(), ctx.client, req) + if err != nil { + return C.int(errorToRC(err)) + } + + if dci == nil { + return 0 + } + + // Populate the daos_check_info structure + dci.dci_status = C.CString(resp.Status.String()) + dci.dci_phase = C.CString(resp.ScanPhase.String()) + + // Populate pools if requested + if len(queryUUIDs) > 0 && len(resp.Pools) > 0 { + // Count matching pools + matchingPools := 0 + for _, qUUID := range queryUUIDs { + if _, ok := resp.Pools[qUUID]; ok { + matchingPools++ + } + } + + if matchingPools > 0 { + dci.dci_pools = (*C.struct_daos_check_pool_info)(C.calloc(C.size_t(matchingPools), + C.size_t(unsafe.Sizeof(C.struct_daos_check_pool_info{})))) + poolSlice := unsafe.Slice(dci.dci_pools, matchingPools) + + idx := 0 + for _, qUUID := range queryUUIDs { + pool, ok := resp.Pools[qUUID] + if !ok { + continue + } + + // Parse and copy UUID + if parsedUUID, err := uuid.Parse(pool.UUID); err == nil { + copyUUIDToC(parsedUUID, &poolSlice[idx].dcpi_uuid) + } + poolSlice[idx].dcpi_status = C.CString(pool.Status) + poolSlice[idx].dcpi_phase = C.CString(pool.Phase) + idx++ + } + dci.dci_pool_nr = C.int(idx) + } + } + + // Populate reports + 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{})))) + reportSlice := unsafe.Slice(dci.dci_reports, len(resp.Reports)) + + for i, rpt := range resp.Reports { + // Parse and copy pool UUID + 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) + + // Copy action choices + nChoices := len(rpt.ActChoices) + if nChoices > 4 { + nChoices = 4 + } + 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 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 { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.SystemCheckRepairReq{} + req.CheckActReq.Seq = uint64(seq) + if err := req.SetAction(int32(action)); err != nil { + return C.int(daos.InvalidInput) + } + + err := control.SystemCheckRepair(ctx.ctx(), ctx.client, req) + return C.int(errorToRC(err)) +} + +//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 { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.SystemCheckSetPolicyReq{} + + // TCPF_RESET = (1 << 0), TCPF_INTERACT = (1 << 1) + const ( + tcpfReset = 1 << 0 + tcpfInteract = 1 << 1 + ) + + if uint32(flags)&tcpfReset != 0 { + req.ResetToDefaults = true + } + if uint32(flags)&tcpfInteract != 0 { + req.AllInteractive = true + } + + // Parse policies string if provided (format: "CLASS:ACTION,CLASS:ACTION,...") + if policies != nil { + policiesStr := goString(policies) + if policiesStr != "" { + // Parse policies from string format + for _, policyStr := range strings.Split(policiesStr, ",") { + policyStr = strings.TrimSpace(policyStr) + if policyStr == "" { + continue + } + parts := strings.SplitN(policyStr, ":", 2) + if len(parts) != 2 { + return C.int(daos.InvalidInput) + } + policy, err := control.NewSystemCheckPolicy(parts[0], parts[1]) + if err != nil { + return C.int(daos.InvalidInput) + } + req.Policies = append(req.Policies, policy) + } + } + } + + err := control.SystemCheckSetPolicy(ctx.ctx(), ctx.client, req) + return C.int(errorToRC(err)) +} 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..71a57bc762d --- /dev/null +++ b/src/control/lib/control/c/check_test.go @@ -0,0 +1,330 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "testing" + + "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" +) + +func TestCheckSwitch(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 TestCheckStart(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 TestCheckStop(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 TestCheckQuery(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 TestCheckRepair(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 TestCheckSetPolicy(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) + } + }) + } +} + +func TestCheckOperationsInvalidHandle(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/context.go b/src/control/lib/control/c/context.go new file mode 100644 index 00000000000..1d1c57edf36 --- /dev/null +++ b/src/control/lib/control/c/context.go @@ -0,0 +1,103 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "context" + "io" + "os" + + "github.com/daos-stack/daos/src/control/build" + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/logging" +) + +// ctrlContext holds the client connection state for a management context. +type ctrlContext struct { + client control.UnaryInvoker + cfg *control.Config + 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 == "" { + // No config file - use default config (similar to dmg -i) + cfg = control.DefaultConfig() + cfg.TransportConfig.AllowInsecure = true + } else { + cfg, err = control.LoadConfig(configFile) + if err != nil { + return nil, err + } + } + + // Logging is quiet by default. + 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 { + logDest = logFile + } + } + + log := logging.NewCombinedLogger("daos_control", logDest). + WithLogLevel(logging.LogLevelNotice) + + if logLevelStr != "" { + var level logging.LogLevel + if err := level.SetString(logLevelStr); err == nil { + log.SetLevel(level) + } + } + + client := control.NewClient( + control.WithConfig(cfg), + control.WithClientLogger(log), + control.WithClientComponent(build.ComponentAdmin), + ) + + return &ctrlContext{ + client: client, + cfg: cfg, + 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, + cfg: control.DefaultConfig(), + log: log, + } +} diff --git a/src/control/lib/control/c/convert.go b/src/control/lib/control/c/convert.go new file mode 100644 index 00000000000..2dd4aa07f88 --- /dev/null +++ b/src/control/lib/control/c/convert.go @@ -0,0 +1,172 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +/* +#include +#include +#include +#include + +#include "daos_control_util.h" +*/ +import "C" +import ( + "os/user" + "strconv" + "unsafe" + + "github.com/google/uuid" + + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/lib/ranklist" +) + +// 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 +} + +// 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 a slice of ranklist.Rank to a pre-allocated C d_rank_list_t. +// The destination's rl_nr is used as the capacity; only that many ranks will be copied. +func copyRankListToC(ranks []ranklist.Rank, cRankList *C.d_rank_list_t) { + if cRankList == nil || len(ranks) == 0 { + if cRankList != nil { + cRankList.rl_nr = 0 + } + return + } + + // Verify rl_ranks is valid before attempting to write + if cRankList.rl_ranks == nil { + cRankList.rl_nr = 0 + return + } + + // Use destination's rl_nr as capacity - don't write more than it can hold + toCopy := len(ranks) + if int(cRankList.rl_nr) < toCopy { + toCopy = int(cRankList.rl_nr) + } + + // Don't write anything if there's nothing to copy + if toCopy == 0 { + cRankList.rl_nr = 0 + return + } + + cRankList.rl_nr = C.uint32_t(toCopy) + 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 string. +func uidToUsername(uid uint32) string { + u, err := user.LookupId(strconv.FormatUint(uint64(uid), 10)) + if err != nil { + return "" + } + return u.Username +} + +// gidToGroupname converts a numeric GID to a group name string. +func gidToGroupname(gid uint32) string { + g, err := user.LookupGroupId(strconv.FormatUint(uint64(gid), 10)) + if err != nil { + return "" + } + return g.Name +} + +// propsFromC converts a C daos_prop_t to Go pool properties. +// This extracts properties that are relevant for pool creation. +func propsFromC(cProps *C.daos_prop_t) []*daos.PoolProperty { + if cProps == nil || cProps.dpp_nr == 0 { + return nil + } + + entries := unsafe.Slice(cProps.dpp_entries, cProps.dpp_nr) + allProps := daos.PoolProperties() + var props []*daos.PoolProperty + + for i := range entries { + entry := &entries[i] + propType := uint32(entry.dpe_type) + + // Find the property name for this type + var propName string + for name, handler := range allProps { + if handler.GetProperty(name).Number == propType { + propName = name + break + } + } + if propName == "" { + continue // Unknown property type + } + + prop := allProps[propName].GetProperty(propName) + + // Extract value based on property type + switch propType { + case C.DAOS_PROP_PO_LABEL: + // String property - use helper function to access union + strPtr := C.get_dpe_str(entry) + if strPtr != nil { + prop.Value.SetString(C.GoString(strPtr)) + } + default: + // Numeric property - use helper function to access union + prop.Value.SetNumber(uint64(C.get_dpe_val(entry))) + } + + props = append(props, prop) + } + + return props +} 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.go b/src/control/lib/control/c/errors.go new file mode 100644 index 00000000000..4d3c1f90b26 --- /dev/null +++ b/src/control/lib/control/c/errors.go @@ -0,0 +1,53 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "context" + "errors" + "os" + + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" +) + +// errorToRC converts a Go error to a DAOS return code. +func errorToRC(err error) int { + if err == nil { + return 0 + } + + // Check for daos.Status errors first + var ds daos.Status + if errors.As(err, &ds) { + return int(ds) + } + + // Check for control plane specific errors + if errors.Is(err, control.ErrNoConfigFile) { + return int(daos.BadPath) + } + + // Check for common OS errors + if errors.Is(err, os.ErrNotExist) { + return int(daos.Nonexistent) + } + if errors.Is(err, os.ErrPermission) { + return int(daos.NoPermission) + } + + // Check for context errors + if errors.Is(err, context.DeadlineExceeded) { + return int(daos.TimedOut) + } + if errors.Is(err, context.Canceled) { + return int(daos.Canceled) + } + + // Default to DER_MISC for unknown errors + return int(daos.MiscError) +} 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..ebf21ffad00 --- /dev/null +++ b/src/control/lib/control/c/errors_test.go @@ -0,0 +1,80 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" +) + +func TestErrorToRC(t *testing.T) { + for name, tc := range map[string]struct { + err error + expRC int + }{ + "nil error": { + err: nil, + expRC: 0, + }, + "daos.Status - NoSpace": { + err: daos.NoSpace, + expRC: int(daos.NoSpace), + }, + "daos.Status - NoPermission": { + err: daos.NoPermission, + expRC: int(daos.NoPermission), + }, + "daos.Status - NotFound": { + err: daos.Nonexistent, + expRC: int(daos.Nonexistent), + }, + "daos.Status - TimedOut": { + err: daos.TimedOut, + expRC: int(daos.TimedOut), + }, + "wrapped daos.Status": { + err: errors.New("wrapper: " + daos.NoSpace.Error()), + expRC: int(daos.MiscError), // wrapped string doesn't unwrap + }, + "control.ErrNoConfigFile": { + err: control.ErrNoConfigFile, + expRC: int(daos.BadPath), + }, + "os.ErrNotExist": { + err: os.ErrNotExist, + expRC: int(daos.Nonexistent), + }, + "os.ErrPermission": { + err: os.ErrPermission, + expRC: int(daos.NoPermission), + }, + "context.DeadlineExceeded": { + err: context.DeadlineExceeded, + expRC: int(daos.TimedOut), + }, + "context.Canceled": { + err: context.Canceled, + expRC: int(daos.Canceled), + }, + "unknown error": { + err: errors.New("some unknown error"), + expRC: int(daos.MiscError), + }, + } { + t.Run(name, func(t *testing.T) { + got := testErrorToRC(tc.err) + if got != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, got) + } + }) + } +} diff --git a/src/control/lib/control/c/fi.go b/src/control/lib/control/c/fi.go new file mode 100644 index 00000000000..167c266abbe --- /dev/null +++ b/src/control/lib/control/c/fi.go @@ -0,0 +1,76 @@ +// +// (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" + "runtime/cgo" + + "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" +) + +//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 { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + goUUID := uuidFromC(poolUUID).String() + goFault := goString(fault) + + var rpcFn func(context.Context, *grpc.ClientConn) (proto.Message, error) + if mgmtSvc != 0 { + rpcFn = func(ctx context.Context, conn *grpc.ClientConn) (proto.Message, error) { + req := &chkpb.Fault{ + Strings: []string{goUUID}, + } + if err := req.Class.UnmarshalJSON([]byte(`"` + goFault + `"`)); err != nil { + // Try with CIC_ prefix + if err2 := req.Class.UnmarshalJSON([]byte(`"CIC_` + goFault + `"`)); err2 != nil { + return nil, err + } + } + return mgmtpb.NewMgmtSvcClient(conn).FaultInjectMgmtPoolFault(ctx, req) + } + } else { + rpcFn = func(ctx context.Context, conn *grpc.ClientConn) (proto.Message, error) { + req := &chkpb.Fault{ + Strings: []string{goUUID}, + } + if err := req.Class.UnmarshalJSON([]byte(`"` + goFault + `"`)); err != nil { + // Try with CIC_ prefix + if err2 := req.Class.UnmarshalJSON([]byte(`"CIC_` + goFault + `"`)); err2 != nil { + return nil, err + } + } + return mgmtpb.NewMgmtSvcClient(conn).FaultInjectPoolFault(ctx, req) + } + } + + _, err := control.InvokeFaultRPC(ctx.ctx(), ctx.client, rpcFn) + return C.int(errorToRC(err)) +} 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..4eade5c1a60 --- /dev/null +++ b/src/control/lib/control/c/pool.go @@ -0,0 +1,516 @@ +// +// (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 "daos_control_util.h" +*/ +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/lib/daos" + "github.com/daos-stack/daos/src/control/lib/ranklist" +) + +//export daos_control_pool_create +func daos_control_pool_create( + handle C.uintptr_t, + uid C.uid_t, + gid C.gid_t, + _ *C.char, // grp - unused, reserved for future use + tgts *C.d_rank_list_t, + scmSize C.daos_size_t, + nvmeSize C.daos_size_t, + prop *C.daos_prop_t, + svc *C.d_rank_list_t, + poolUUID *C.uuid_t, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + // Convert C types to Go types + goTgts := rankListFromC(tgts) + goProps := propsFromC(prop) + + // Build the pool create request + req := &control.PoolCreateReq{ + User: uidToUsername(uint32(uid)), + UserGroup: gidToGroupname(uint32(gid)), + } + + if len(goTgts) > 0 { + req.Ranks = goTgts + } + + // Set tier bytes for manual sizing + req.TierBytes = []uint64{uint64(scmSize), uint64(nvmeSize)} + + req.Properties = goProps + + // Call the control API + resp, err := control.PoolCreate(ctx.ctx(), ctx.client, req) + if err != nil { + ctx.log.Errorf("PoolCreate failed: %v", err) + return C.int(errorToRC(err)) + } + + // Convert results back to C types + respUUID, err := uuid.Parse(resp.UUID) + if err != nil { + return C.int(errorToRC(err)) + } + copyUUIDToC(respUUID, poolUUID) + + // Copy service replicas + if svc != nil && len(resp.SvcReps) > 0 { + svcRanks := make([]ranklist.Rank, len(resp.SvcReps)) + for i, r := range resp.SvcReps { + svcRanks[i] = ranklist.Rank(r) + } + copyRankListToC(svcRanks, svc) + } + + return 0 +} + +//export daos_control_pool_destroy +func daos_control_pool_destroy( + handle C.uintptr_t, + poolUUID *C.uuid_t, + _ *C.char, // grp - unused, reserved for future use + force C.int, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.PoolDestroyReq{ + ID: uuidFromC(poolUUID).String(), + Recursive: true, + Force: force != 0, + } + + err := control.PoolDestroy(ctx.ctx(), ctx.client, req) + return C.int(errorToRC(err)) +} + +//export daos_control_pool_evict +func daos_control_pool_evict( + handle C.uintptr_t, + poolUUID *C.uuid_t, + _ *C.char, // grp - unused, reserved for future use +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.PoolEvictReq{ + ID: uuidFromC(poolUUID).String(), + } + + err := control.PoolEvict(ctx.ctx(), ctx.client, req) + return C.int(errorToRC(err)) +} + +//export daos_control_pool_exclude +func daos_control_pool_exclude( + handle C.uintptr_t, + poolUUID *C.uuid_t, + _ *C.char, // grp - unused, reserved for future use + rank C.d_rank_t, + tgtIdx C.int, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.PoolRanksReq{ + ID: uuidFromC(poolUUID).String(), + Ranks: []ranklist.Rank{ranklist.Rank(rank)}, + } + + if tgtIdx >= 0 { + req.TargetIdx = []uint32{uint32(tgtIdx)} + } + + resp, err := control.PoolExclude(ctx.ctx(), ctx.client, req) + if err != nil { + return C.int(errorToRC(err)) + } + + return C.int(errorToRC(resp.Errors())) +} + +//export daos_control_pool_reintegrate +func daos_control_pool_reintegrate( + handle C.uintptr_t, + poolUUID *C.uuid_t, + _ *C.char, // grp - unused, reserved for future use + rank C.d_rank_t, + tgtIdx C.int, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.PoolRanksReq{ + ID: uuidFromC(poolUUID).String(), + Ranks: []ranklist.Rank{ranklist.Rank(rank)}, + } + + if tgtIdx >= 0 { + req.TargetIdx = []uint32{uint32(tgtIdx)} + } + + resp, err := control.PoolReintegrate(ctx.ctx(), ctx.client, req) + if err != nil { + return C.int(errorToRC(err)) + } + + return C.int(errorToRC(resp.Errors())) +} + +//export daos_control_pool_drain +func daos_control_pool_drain( + handle C.uintptr_t, + poolUUID *C.uuid_t, + _ *C.char, // grp - unused, reserved for future use + rank C.d_rank_t, + tgtIdx C.int, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.PoolRanksReq{ + ID: uuidFromC(poolUUID).String(), + Ranks: []ranklist.Rank{ranklist.Rank(rank)}, + } + + if tgtIdx >= 0 { + req.TargetIdx = []uint32{uint32(tgtIdx)} + } + + resp, err := control.PoolDrain(ctx.ctx(), ctx.client, req) + if err != nil { + return C.int(errorToRC(err)) + } + + return C.int(errorToRC(resp.Errors())) +} + +//export daos_control_pool_extend +func daos_control_pool_extend( + handle C.uintptr_t, + poolUUID *C.uuid_t, + _ *C.char, // grp - unused, reserved for future use + ranks *C.d_rank_t, + ranksNr C.int, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + // Convert C rank array to Go slice + var goRanks []ranklist.Rank + if ranks != nil && ranksNr > 0 { + cRanks := (*[1 << 30]C.d_rank_t)(unsafe.Pointer(ranks))[:ranksNr:ranksNr] + goRanks = make([]ranklist.Rank, ranksNr) + for i, r := range cRanks { + goRanks[i] = ranklist.Rank(r) + } + } + + req := &control.PoolExtendReq{ + ID: uuidFromC(poolUUID).String(), + Ranks: goRanks, + } + + err := control.PoolExtend(ctx.ctx(), ctx.client, req) + return C.int(errorToRC(err)) +} + +//export daos_control_pool_list +func daos_control_pool_list( + handle C.uintptr_t, + _ *C.char, // grp - unused, reserved for future use + npools *C.daos_size_t, + pools *C.daos_mgmt_pool_info_t, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.ListPoolsReq{ + NoQuery: true, // Don't query each pool for details + } + + resp, err := control.ListPools(ctx.ctx(), ctx.client, req) + if err != nil { + return C.int(errorToRC(err)) + } + + // Set the number of pools + numPools := C.daos_size_t(len(resp.Pools)) + if npools == nil { + return C.int(errorToRC(err)) + } + + npoolsIn := *npools + *npools = numPools + + // If pools is nil, just return the count + if pools == nil { + return 0 + } + + // Check if the provided array is large enough + if npoolsIn < numPools { + return C.int(daos.BufTooSmall) + } + + // Fill in the pool info structures + poolsSlice := unsafe.Slice(pools, numPools) + for i, p := range resp.Pools { + // Copy UUID + copyUUIDToC(p.UUID, &poolsSlice[i].mgpi_uuid) + + // Copy label (C.CString allocates memory - caller must free) + if p.Label != "" { + poolsSlice[i].mgpi_label = C.CString(p.Label) + } + + // Copy service leader + poolsSlice[i].mgpi_ldr = C.d_rank_t(p.ServiceLeader) + + // Copy service replicas - allocate rank list if needed (like original parse_pool_info) + if len(p.ServiceReplicas) > 0 { + if poolsSlice[i].mgpi_svc == nil { + poolsSlice[i].mgpi_svc = C.d_rank_list_alloc(C.uint32_t(len(p.ServiceReplicas))) + if poolsSlice[i].mgpi_svc == nil { + return C.int(daos.NoMemory) + } + } + copyRankListToC(p.ServiceReplicas, poolsSlice[i].mgpi_svc) + } + } + + return 0 +} + +//export daos_control_pool_set_prop +func daos_control_pool_set_prop( + handle C.uintptr_t, + poolUUID *C.uuid_t, + propName *C.char, + propValue *C.char, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + goName := goString(propName) + goValue := goString(propValue) + + // Look up property by name and set its value + allProps := daos.PoolProperties() + handler, ok := allProps[goName] + if !ok { + return C.int(daos.InvalidInput) + } + + prop := handler.GetProperty(goName) + if err := prop.SetValue(goValue); err != nil { + return C.int(errorToRC(err)) + } + + req := &control.PoolSetPropReq{ + ID: uuidFromC(poolUUID).String(), + Properties: []*daos.PoolProperty{prop}, + } + + err := control.PoolSetProp(ctx.ctx(), ctx.client, req) + return C.int(errorToRC(err)) +} + +//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 { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + goName := goString(propName) + + // Look up property by name + allProps := daos.PoolProperties() + handler, ok := allProps[goName] + if !ok { + return C.int(daos.InvalidInput) + } + + // Determine pool ID (label or UUID) + var poolID string + goLabel := goString(label) + if goLabel != "" { + poolID = goLabel + } else { + poolID = uuidFromC(poolUUID).String() + } + + req := &control.PoolGetPropReq{ + ID: poolID, + Properties: []*daos.PoolProperty{handler.GetProperty(goName)}, + } + + props, err := control.PoolGetProp(ctx.ctx(), ctx.client, req) + if err != nil { + return C.int(errorToRC(err)) + } + + if len(props) == 0 { + return C.int(daos.Nonexistent) + } + + // Return the value as a C string (caller must free) + if propValue != nil { + *propValue = C.CString(props[0].Value.String()) + } + + return 0 +} + +//export daos_control_pool_update_ace +func daos_control_pool_update_ace( + handle C.uintptr_t, + poolUUID *C.uuid_t, + _ *C.char, // grp - unused, reserved for future use + ace *C.char, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + goACE := goString(ace) + + req := &control.PoolUpdateACLReq{ + ID: uuidFromC(poolUUID).String(), + ACL: &control.AccessControlList{ + Entries: []string{goACE}, + }, + } + + _, err := control.PoolUpdateACL(ctx.ctx(), ctx.client, req) + return C.int(errorToRC(err)) +} + +//export daos_control_pool_delete_ace +func daos_control_pool_delete_ace( + handle C.uintptr_t, + poolUUID *C.uuid_t, + _ *C.char, // grp - unused, reserved for future use + principal *C.char, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.PoolDeleteACLReq{ + ID: uuidFromC(poolUUID).String(), + Principal: goString(principal), + } + + _, err := control.PoolDeleteACL(ctx.ctx(), ctx.client, req) + return C.int(errorToRC(err)) +} + +//export daos_control_pool_rebuild_stop +func daos_control_pool_rebuild_stop( + handle C.uintptr_t, + poolUUID *C.uuid_t, + _ *C.char, // grp - unused, reserved for future use + force C.int, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.PoolRebuildManageReq{ + ID: uuidFromC(poolUUID).String(), + OpCode: control.PoolRebuildOpCodeStop, + Force: force != 0, + } + + err := control.PoolRebuildManage(ctx.ctx(), ctx.client, req) + return C.int(errorToRC(err)) +} + +//export daos_control_pool_rebuild_start +func daos_control_pool_rebuild_start( + handle C.uintptr_t, + poolUUID *C.uuid_t, + _ *C.char, // grp - unused, reserved for future use +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.PoolRebuildManageReq{ + ID: uuidFromC(poolUUID).String(), + OpCode: control.PoolRebuildOpCodeStart, + } + + err := control.PoolRebuildManage(ctx.ctx(), ctx.client, req) + return C.int(errorToRC(err)) +} 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..7644543f5bd --- /dev/null +++ b/src/control/lib/control/c/pool_test.go @@ -0,0 +1,1022 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "testing" + + "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" +) + +func TestPoolCreate(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}, + }), + }, + expRC: 0, + 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) + + // Create mock invoker and test context + mi := control.NewMockInvoker(log, tc.mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + // Allocate test types for outputs + poolUUID := newTestUUID() + svc := newTestRankList(3) // Allocate space for 3 ranks + defer svc.free() + + // Call the exported function via wrapper + rc := callPoolCreate( + handle, + 1000, // uid + 1000, // gid + 1<<30, // 1GB SCM + 0, // 0 NVMe + svc, + poolUUID, + ) + + // Verify return code + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + + // If we expected an error, we're done + if tc.expRC != 0 { + return + } + + // Verify service ranks were copied + if len(tc.expSvcRanks) > 0 { + if svc.nr() != uint32(len(tc.expSvcRanks)) { + t.Fatalf("expected %d svc ranks, got %d", len(tc.expSvcRanks), svc.nr()) + } + for i, expRank := range tc.expSvcRanks { + gotRank := svc.getRank(uint32(i)) + if gotRank != expRank { + t.Fatalf("svc rank[%d]: expected %d, got %d", i, expRank, gotRank) + } + } + } + }) + } +} + +func TestPoolCreateInvalidHandle(t *testing.T) { + poolUUID := newTestUUID() + + // Call with invalid handle (0) + rc := callPoolCreateInvalidHandle(poolUUID) + + // Should return error for invalid handle + if rc == 0 { + t.Fatal("expected error for invalid handle, got success") + } +} + +func TestUUIDConversion(t *testing.T) { + // Test UUID round-trip conversion + testUUID := uuid.MustParse("12345678-1234-1234-1234-123456789abc") + + tu := newTestUUID() + tu.set(testUUID) + + gotUUID := tu.get() + if gotUUID != testUUID { + t.Fatalf("UUID round-trip failed: expected %s, got %s", testUUID, gotUUID) + } +} + +func TestUUIDFromCNil(t *testing.T) { + // The uuidFromC function should handle nil input + // We can't directly test nil with the wrapper, but we verify the wrapper works + tu := newTestUUID() + // Zero UUID should round-trip correctly + tu.set(uuid.Nil) + gotUUID := tu.get() + if gotUUID != uuid.Nil { + t.Fatalf("expected nil UUID, got %s", gotUUID) + } +} + +func TestRankListConversion(t *testing.T) { + for name, tc := range map[string]struct { + ranks []uint32 + }{ + "empty": { + ranks: nil, + }, + "single rank": { + ranks: []uint32{5}, + }, + "multiple ranks": { + ranks: []uint32{0, 1, 2, 3, 4}, + }, + } { + t.Run(name, func(t *testing.T) { + if len(tc.ranks) == 0 { + // Test nil input via empty rank list + rl := newTestRankList(0) + defer rl.free() + + got := testConvertRankListFromC(rl) + if len(got) != 0 { + t.Fatalf("expected empty slice, got %v", got) + } + return + } + + // Allocate and populate rank list + rl := newTestRankList(uint32(len(tc.ranks))) + if rl.ptr() == nil { + t.Fatal("failed to allocate rank list") + } + defer rl.free() + + for i, r := range tc.ranks { + rl.setRank(uint32(i), r) + } + + // Convert to Go and verify + goRanks := testConvertRankListFromC(rl) + if len(goRanks) != len(tc.ranks) { + t.Fatalf("expected %d ranks, got %d", len(tc.ranks), len(goRanks)) + } + + for i, exp := range tc.ranks { + if goRanks[i] != exp { + t.Fatalf("rank[%d]: expected %d, got %d", i, exp, goRanks[i]) + } + } + + // Now test copying back to C + outRL := newTestRankList(uint32(len(tc.ranks))) + if outRL.ptr() == nil { + t.Fatal("failed to allocate output rank list") + } + defer outRL.free() + + testConvertRankListToC(goRanks, outRL) + + if outRL.nr() != uint32(len(tc.ranks)) { + t.Fatalf("expected rl_nr %d, got %d", len(tc.ranks), outRL.nr()) + } + + for i, exp := range tc.ranks { + got := outRL.getRank(uint32(i)) + if got != exp { + t.Fatalf("output rank[%d]: expected %d, got %d", i, exp, got) + } + } + }) + } +} + +func TestGoString(t *testing.T) { + for name, tc := range map[string]struct { + input string + makeNil bool + expected string + }{ + "nil": { + makeNil: true, + expected: "", + }, + "empty": { + input: "", + expected: "", + }, + "simple": { + input: "hello", + expected: "hello", + }, + "with spaces": { + input: "hello world", + expected: "hello world", + }, + } { + t.Run(name, func(t *testing.T) { + var cs *testCString + if tc.makeNil { + cs = &testCString{cstr: nil} + } else { + cs = newTestCString(tc.input) + defer cs.free() + } + + got := cs.toGo() + if got != tc.expected { + t.Fatalf("expected %q, got %q", tc.expected, got) + } + }) + } +} + +func TestPoolDestroy(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{}), + }, + expRC: 0, + }, + "success with force": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolDestroyResp{}), + }, + force: true, + expRC: 0, + }, + "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() + + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callPoolDestroy(handle, poolUUID, tc.force) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestPoolEvict(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{}), + }, + expRC: 0, + }, + "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() + + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callPoolEvict(handle, poolUUID) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestPoolExclude(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{}), + }, + rank: 0, + tgtIdx: -1, // no target index + expRC: 0, + }, + "success - rank and target": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolExcludeResp{}), + }, + rank: 1, + tgtIdx: 2, + expRC: 0, + }, + "failure - response error": { + // Note: Status errors get wrapped by resp.Errors() into a generic error, + // which maps to MiscError. The original status is not preserved. + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolExcludeResp{ + Status: int32(daos.Nonexistent), + }), + }, + rank: 0, + tgtIdx: -1, + expRC: int(daos.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() + + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callPoolExclude(handle, poolUUID, tc.rank, tc.tgtIdx) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestPoolDrain(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + rank uint32 + tgtIdx int + expRC int + }{ + "success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolDrainResp{}), + }, + rank: 0, + tgtIdx: -1, + expRC: 0, + }, + "failure - response error": { + // Note: Status errors get wrapped by resp.Errors() into a generic error, + // which maps to MiscError. The original status is not preserved. + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolDrainResp{ + Status: int32(daos.Busy), + }), + }, + rank: 0, + tgtIdx: -1, + expRC: int(daos.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() + + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callPoolDrain(handle, poolUUID, tc.rank, tc.tgtIdx) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestPoolReintegrate(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + rank uint32 + tgtIdx int + expRC int + }{ + "success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolReintResp{}), + }, + rank: 0, + tgtIdx: -1, + expRC: 0, + }, + "failure - response error": { + // Note: Status errors get wrapped by resp.Errors() into a generic error, + // which maps to MiscError. The original status is not preserved. + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolReintResp{ + Status: int32(daos.Busy), + }), + }, + rank: 0, + tgtIdx: -1, + expRC: int(daos.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() + + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callPoolReintegrate(handle, poolUUID, tc.rank, tc.tgtIdx) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestPoolOperationsInvalidHandle(t *testing.T) { + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + // All pool operations should fail with invalid handle + tests := []struct { + name string + fn func() int + }{ + {"destroy", func() int { return callPoolDestroy(0, poolUUID, false) }}, + {"evict", func() int { return callPoolEvict(0, poolUUID) }}, + {"exclude", func() int { return callPoolExclude(0, poolUUID, 0, -1) }}, + {"drain", func() int { return callPoolDrain(0, poolUUID, 0, -1) }}, + {"reintegrate", func() int { return callPoolReintegrate(0, poolUUID, 0, -1) }}, + {"extend", func() int { return callPoolExtend(0, poolUUID, []uint32{1, 2}) }}, + {"set_prop", func() int { return callPoolSetProp(0, poolUUID, "label", "test") }}, + {"get_prop", func() int { _, rc := callPoolGetProp(0, "", poolUUID, "label"); return rc }}, + {"update_ace", func() int { return callPoolUpdateACE(0, poolUUID, "A::user@:rw") }}, + {"delete_ace", func() int { return callPoolDeleteACE(0, poolUUID, "user@") }}, + } + + 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) + } + }) + } +} + +func TestPoolExtend(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}, + expRC: 0, + }, + "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() + + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callPoolExtend(handle, poolUUID, tc.ranks) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestPoolSetProp(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + propName string + propVal string + expRC int + }{ + "success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.PoolSetPropResp{}), + }, + propName: "label", + propVal: "testpool", + expRC: 0, + }, + "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() + + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callPoolSetProp(handle, poolUUID, tc.propName, tc.propVal) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestPoolGetProp(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", + expRC: 0, + }, + "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", + expRC: 0, + }, + "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() + + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + val, rc := callPoolGetProp(handle, tc.label, poolUUID, tc.propName) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + + if rc == 0 && val != tc.expVal { + t.Fatalf("expected value %q, got %q", tc.expVal, val) + } + }) + } +} + +func TestPoolUpdateACE(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + ace string + expRC int + }{ + "success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.ACLResp{}), + }, + ace: "A::user@:rw", + expRC: 0, + }, + "failure": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.NoPermission, + }, + ace: "A::user@:rw", + 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() + + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callPoolUpdateACE(handle, poolUUID, tc.ace) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestPoolDeleteACE(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + principal string + expRC int + }{ + "success": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.ACLResp{}), + }, + principal: "user@", + expRC: 0, + }, + "failure": { + mic: &control.MockInvokerConfig{ + UnaryError: daos.NoPermission, + }, + principal: "user@", + 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() + + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callPoolDeleteACE(handle, poolUUID, tc.principal) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestPoolRebuildStop(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{}), + }, + expRC: 0, + }, + "success with force": { + mic: &control.MockInvokerConfig{ + UnaryResponse: control.MockMSResponse("host1", nil, &mgmtpb.DaosResp{}), + }, + force: true, + 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() + + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callPoolRebuildStop(handle, poolUUID, tc.force) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestPoolRebuildStart(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{}), + }, + 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() + + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callPoolRebuildStart(handle, poolUUID) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestPoolList(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + npoolsIn uint64 + 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"}, + }, + }), + }, + npoolsIn: 0, + expNpools: 2, + expRC: 0, + }, + "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() + + npools := tc.npoolsIn + rc := callPoolList(handle, &npools, nil) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + + if rc == 0 && npools != tc.expNpools { + t.Fatalf("expected %d pools, got %d", tc.expNpools, npools) + } + }) + } +} + +func TestPoolListWithData(t *testing.T) { + // Test that pool list properly populates the output structures + 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}, + RebuildState: "idle", + State: "Ready", + }, + { + Uuid: "22345678-1234-1234-1234-123456789abc", + Label: "pool2", + SvcReps: []uint32{0}, + RebuildState: "idle", + State: "Ready", + }, + }, + }), + } + + mi := control.NewMockInvoker(log, mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + // First get count + var npools uint64 + rc := callPoolList(handle, &npools, nil) + if rc != 0 { + t.Fatalf("expected RC 0, got %d", rc) + } + if npools != 2 { + t.Fatalf("expected 2 pools, got %d", npools) + } + + // Now get with data - need a fresh invoker since mock is consumed + mi = control.NewMockInvoker(log, mic) + handle2 := makeTestHandle(mi, log) + defer handle2.Delete() + + pools := newTestPoolListInfo(int(npools)) + defer pools.free() + + npools = 2 // Set capacity + rc = callPoolList(handle2, &npools, pools) + if rc != 0 { + t.Fatalf("expected RC 0, got %d", rc) + } + + // Verify pool data was populated + expUUID1 := uuid.MustParse("12345678-1234-1234-1234-123456789abc") + gotUUID1 := pools.getUUID(0) + if gotUUID1 != expUUID1 { + t.Fatalf("pool[0] UUID: expected %s, got %s", expUUID1, gotUUID1) + } + + gotLabel1 := pools.getLabel(0) + if gotLabel1 != "pool1" { + t.Fatalf("pool[0] label: expected 'pool1', got %q", gotLabel1) + } + + expUUID2 := uuid.MustParse("22345678-1234-1234-1234-123456789abc") + gotUUID2 := pools.getUUID(1) + if gotUUID2 != expUUID2 { + t.Fatalf("pool[1] UUID: expected %s, got %s", expUUID2, gotUUID2) + } + + gotLabel2 := pools.getLabel(1) + if gotLabel2 != "pool2" { + t.Fatalf("pool[1] label: expected 'pool2', got %q", gotLabel2) + } +} + +func TestPoolListBufferTooSmall(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"}, + {Uuid: "22345678-1234-1234-1234-123456789abc", Label: "pool2"}, + {Uuid: "32345678-1234-1234-1234-123456789abc", Label: "pool3"}, + }, + }), + } + + mi := control.NewMockInvoker(log, mic) + handle := makeTestHandle(mi, log) + defer handle.Delete() + + // Allocate buffer for only 1 pool but there are 3 + pools := newTestPoolListInfo(1) + defer pools.free() + + npools := uint64(1) // Capacity is 1 + rc := callPoolList(handle, &npools, pools) + + // Should return buffer too small error + if rc != int(daos.BufTooSmall) { + t.Fatalf("expected RC %d (BufTooSmall), got %d", int(daos.BufTooSmall), rc) + } +} + +func TestPoolListInvalidHandle(t *testing.T) { + var npools uint64 + rc := callPoolList(0, &npools, nil) + if rc == 0 { + t.Fatal("expected error for invalid handle, got success") + } +} + +func TestPoolRebuildInvalidHandle(t *testing.T) { + poolUUID := newTestUUID() + poolUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callPoolRebuildStop(0, poolUUID, false) + if rc == 0 { + t.Fatal("expected error for invalid handle on rebuild stop, got success") + } + + rc = callPoolRebuildStart(0, poolUUID) + if rc == 0 { + t.Fatal("expected error for invalid handle on rebuild start, got success") + } +} diff --git a/src/control/lib/control/c/server.go b/src/control/lib/control/c/server.go new file mode 100644 index 00000000000..60d31f0b237 --- /dev/null +++ b/src/control/lib/control/c/server.go @@ -0,0 +1,62 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +/* +#include +#include +*/ +import "C" +import ( + "runtime/cgo" + + "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, + masks *C.char, + streams *C.char, + subsystems *C.char, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.SetEngineLogMasksReq{} + + 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 C.int(errorToRC(err)) + } + + // Check for host errors in the response + if resp.HostErrorsResp.GetHostErrors().ErrorCount() > 0 { + // Return the first error + for _, hes := range resp.HostErrorsResp.GetHostErrors() { + return C.int(errorToRC(hes.HostError)) + } + } + + return 0 +} 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..adbc6c7581d --- /dev/null +++ b/src/control/lib/control/c/server_test.go @@ -0,0 +1,56 @@ +// +// (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/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 TestServerSetLogmasks(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 TestServerSetLogmasksInvalidHandle(t *testing.T) { + rc := callServerSetLogmasks(0, "DEBUG", "", "") + if rc == 0 { + t.Fatal("expected error for invalid handle, got success") + } +} diff --git a/src/control/lib/control/c/storage.go b/src/control/lib/control/c/storage.go new file mode 100644 index 00000000000..6b68d36e2f6 --- /dev/null +++ b/src/control/lib/control/c/storage.go @@ -0,0 +1,282 @@ +// +// (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" + "runtime/cgo" + "strconv" + "strings" + "unsafe" + + "github.com/pkg/errors" + + "github.com/daos-stack/daos/src/control/lib/control" +) + +//export daos_control_storage_device_list +func daos_control_storage_device_list( + handle C.uintptr_t, + ndisks *C.int, + devices *C.struct_device_list, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.SmdQueryReq{ + OmitPools: true, + } + + resp, err := control.SmdQuery(ctx.ctx(), ctx.client, req) + if err != nil { + return C.int(errorToRC(err)) + } + + // Count total devices across all hosts + totalDevices := 0 + for _, hss := range resp.HostStorage { + if hss.HostStorage != nil && hss.HostStorage.SmdInfo != nil { + totalDevices += len(hss.HostStorage.SmdInfo.Devices) + } + } + + if ndisks != nil { + *ndisks = C.int(totalDevices) + } + + // If devices is nil, caller just wants the count + if devices == nil { + return 0 + } + + // Fill in device information + deviceSlice := unsafe.Slice(devices, totalDevices) + idx := 0 + for _, hss := range resp.HostStorage { + if hss.HostStorage == nil || hss.HostStorage.SmdInfo == nil { + continue + } + + // Get hostname from host set + hostAddr := hss.HostSet.RangedString() + // Strip port if present (format may be "host:port") + if colonIdx := strings.LastIndex(hostAddr, ":"); colonIdx > 0 { + hostAddr = hostAddr[:colonIdx] + } + + for _, dev := range hss.HostStorage.SmdInfo.Devices { + if idx >= totalDevices { + break + } + + // Copy device UUID + devUUID, err := parseUUID(dev.UUID) + if err == nil { + copyUUIDToC(devUUID, &deviceSlice[idx].device_id) + } + + // Copy state (truncate if needed) + state := dev.Ctrlr.NvmeState.String() + copyStringToCharArray(state, &deviceSlice[idx].state[0], 10) + + // Copy rank + deviceSlice[idx].rank = C.int(dev.Rank) + + // Copy hostname + copyStringToCharArray(hostAddr, &deviceSlice[idx].host[0], C.DSS_HOSTNAME_MAX_LEN) + + // Copy target IDs + nTgts := len(dev.TargetIDs) + if nTgts > C.MAX_TEST_TARGETS_PER_DEVICE { + nTgts = C.MAX_TEST_TARGETS_PER_DEVICE + } + for i := 0; i < nTgts; i++ { + deviceSlice[idx].tgtidx[i] = C.int(dev.TargetIDs[i]) + } + deviceSlice[idx].n_tgtidx = C.int(nTgts) + + idx++ + } + } + + return 0 +} + +//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, + force C.int, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + goUUID := uuidFromC(devUUID) + + req := &control.SmdManageReq{ + IDs: goUUID.String(), + Operation: control.SetFaultyOp, + } + + // Set host list if provided + if host != nil { + hostStr := goString(host) + if hostStr != "" { + req.SetHostList([]string{hostStr}) + } + } + + resp, err := control.SmdManage(ctx.ctx(), ctx.client, req) + if err != nil { + return C.int(errorToRC(err)) + } + + // Check for host errors + if resp.HostErrorsResp.GetHostErrors().ErrorCount() > 0 { + for _, hes := range resp.HostErrorsResp.GetHostErrors() { + return C.int(errorToRC(hes.HostError)) + } + } + + return 0 +} + +//export daos_control_storage_query_device_health +func daos_control_storage_query_device_health( + handle C.uintptr_t, + host *C.char, + statsKey *C.char, + statsOut *C.char, + statsOutLen C.int, + devUUID *C.uuid_t, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + goUUID := uuidFromC(devUUID) + + req := &control.SmdQueryReq{ + OmitPools: true, + IncludeBioHealth: true, + UUID: goUUID.String(), + } + + // Set host list if provided + if host != nil { + hostStr := goString(host) + if hostStr != "" { + req.SetHostList([]string{hostStr}) + } + } + + resp, err := control.SmdQuery(ctx.ctx(), ctx.client, req) + if err != nil { + return C.int(errorToRC(err)) + } + + // Find the device and extract the requested health stat + key := goString(statsKey) + for _, hss := range resp.HostStorage { + if hss.HostStorage == nil || hss.HostStorage.SmdInfo == nil { + continue + } + for _, dev := range hss.HostStorage.SmdInfo.Devices { + if dev.Ctrlr.HealthStats == nil { + continue + } + + // Get the health stat value based on key + var value string + stats := dev.Ctrlr.HealthStats + switch key { + case "temperature": + value = fmt.Sprintf("%d", stats.Temperature) + case "media_errs": + value = fmt.Sprintf("%d", stats.MediaErrors) + case "bio_read_errs": + value = fmt.Sprintf("%d", stats.ReadErrors) + case "bio_write_errs": + value = fmt.Sprintf("%d", stats.WriteErrors) + case "bio_unmap_errs": + value = fmt.Sprintf("%d", stats.UnmapErrors) + case "checksum_errs": + value = fmt.Sprintf("%d", stats.ChecksumErrors) + case "power_cycles": + value = fmt.Sprintf("%d", stats.PowerCycles) + case "unsafe_shutdowns": + value = fmt.Sprintf("%d", stats.UnsafeShutdowns) + default: + // Unknown key, return empty + value = "" + } + + if value != "" && statsOut != nil && statsOutLen > 0 { + copyStringToCharArray(value, statsOut, int(statsOutLen)) + } + return 0 + } + } + + return 0 +} + +// Helper function to copy a Go string to a C char array +func copyStringToCharArray(s string, dest *C.char, maxLen int) { + if dest == nil || maxLen <= 0 { + return + } + + // Create a slice backed by the C array + destSlice := unsafe.Slice(dest, maxLen) + + // Copy string bytes + copyLen := len(s) + if copyLen >= maxLen { + copyLen = maxLen - 1 + } + for i := 0; i < copyLen; i++ { + destSlice[i] = C.char(s[i]) + } + destSlice[copyLen] = 0 // null terminate +} + +// Helper to parse UUID string +func parseUUID(s string) ([16]byte, error) { + var uuid [16]byte + // Simple UUID parsing - expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + s = strings.ReplaceAll(s, "-", "") + if len(s) != 32 { + return uuid, errors.New("invalid UUID length") + } + for i := 0; i < 16; i++ { + b, err := strconv.ParseUint(s[i*2:i*2+2], 16, 8) + if err != nil { + return uuid, err + } + uuid[i] = byte(b) + } + return uuid, nil +} 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..6bfce6902f4 --- /dev/null +++ b/src/control/lib/control/c/storage_ops_test.go @@ -0,0 +1,154 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "testing" + + "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 TestStorageDeviceList(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 TestStorageSetNVMeFault(t *testing.T) { + for name, tc := range map[string]struct { + mic *control.MockInvokerConfig + host string + force bool + 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 := newTestUUID() + devUUID.set(uuid.MustParse("12345678-1234-1234-1234-123456789abc")) + + rc := callStorageSetNVMeFault(handle, tc.host, devUUID, tc.force) + + if rc != tc.expRC { + t.Fatalf("expected RC %d, got %d", tc.expRC, rc) + } + }) + } +} + +func TestStorageQueryDeviceHealth(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 := newTestUUID() + devUUID.set(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 TestStorageOperationsInvalidHandle(t *testing.T) { + devUUID := newTestUUID() + devUUID.set(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, false) }}, + {"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.go b/src/control/lib/control/c/storage_test.go new file mode 100644 index 00000000000..5160bfe4309 --- /dev/null +++ b/src/control/lib/control/c/storage_test.go @@ -0,0 +1,124 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +import ( + "testing" +) + +func TestCopyStringToCharArray(t *testing.T) { + for name, tc := range map[string]struct { + input string + bufSize int + expected string + }{ + "simple string": { + input: "hello", + bufSize: 10, + expected: "hello", + }, + "exact fit": { + input: "hello", + bufSize: 6, // 5 chars + null + expected: "hello", + }, + "truncated": { + input: "hello world", + bufSize: 6, + expected: "hello", // truncated to fit with null terminator + }, + "empty string": { + input: "", + bufSize: 10, + expected: "", + }, + "single char buffer": { + input: "hello", + bufSize: 1, + expected: "", // only null terminator fits + }, + } { + t.Run(name, func(t *testing.T) { + buf := newTestCharArray(tc.bufSize) + testCopyStringToCharArray(tc.input, buf, tc.bufSize) + + got := buf.toString() + if got != tc.expected { + t.Fatalf("expected %q, got %q", tc.expected, got) + } + }) + } +} + +func TestCopyStringToCharArrayNilDest(t *testing.T) { + // Should not panic with nil destination + testCopyStringToCharArray("hello", nil, 10) +} + +func TestParseUUID(t *testing.T) { + for name, tc := range map[string]struct { + input string + expectErr bool + expected [16]byte + }{ + "valid UUID with dashes": { + input: "12345678-1234-1234-1234-123456789abc", + expectErr: false, + expected: [16]byte{0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, + }, + "valid UUID without dashes": { + input: "123456781234123412341234567890ab", + expectErr: false, + expected: [16]byte{0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab}, + }, + "all zeros": { + input: "00000000-0000-0000-0000-000000000000", + expectErr: false, + expected: [16]byte{}, + }, + "all ones": { + input: "ffffffff-ffff-ffff-ffff-ffffffffffff", + expectErr: false, + expected: [16]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, + "too short": { + input: "12345678-1234-1234-1234", + expectErr: true, + }, + "too long": { + input: "12345678-1234-1234-1234-123456789abc-extra", + expectErr: true, + }, + "invalid hex": { + input: "12345678-1234-1234-1234-12345678xxxx", + expectErr: true, + }, + "empty string": { + input: "", + expectErr: true, + }, + } { + t.Run(name, func(t *testing.T) { + got, err := testParseUUID(tc.input) + + if tc.expectErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if got != tc.expected { + t.Fatalf("expected %v, got %v", tc.expected, got) + } + }) + } +} diff --git a/src/control/lib/control/c/system.go b/src/control/lib/control/c/system.go new file mode 100644 index 00000000000..13267fdea54 --- /dev/null +++ b/src/control/lib/control/c/system.go @@ -0,0 +1,116 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package main + +/* +#include +#include +#include +*/ +import "C" +import ( + "runtime/cgo" + + "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 { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + 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 C.int(errorToRC(err)) + } + + return C.int(errorToRC(resp.Errors())) +} + +//export daos_control_system_start_rank +func daos_control_system_start_rank( + handle C.uintptr_t, + rank C.d_rank_t, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.SystemStartReq{} + req.Ranks.Add(ranklist.Rank(rank)) + + resp, err := control.SystemStart(ctx.ctx(), ctx.client, req) + if err != nil { + return C.int(errorToRC(err)) + } + + return C.int(errorToRC(resp.Errors())) +} + +//export daos_control_system_reint_rank +func daos_control_system_reint_rank( + handle C.uintptr_t, + rank C.d_rank_t, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + // SystemExclude with Clear=true clears the exclude state (reintegrates) + req := &control.SystemExcludeReq{ + Clear: true, + } + req.Ranks.Add(ranklist.Rank(rank)) + + resp, err := control.SystemExclude(ctx.ctx(), ctx.client, req) + if err != nil { + return C.int(errorToRC(err)) + } + + return C.int(errorToRC(resp.Errors())) +} + +//export daos_control_system_exclude_rank +func daos_control_system_exclude_rank( + handle C.uintptr_t, + rank C.d_rank_t, +) C.int { + if handle == 0 { + return C.int(errorToRC(control.ErrNoConfigFile)) + } + + ctx := cgo.Handle(handle).Value().(*ctrlContext) + + req := &control.SystemExcludeReq{ + Clear: false, + } + req.Ranks.Add(ranklist.Rank(rank)) + + resp, err := control.SystemExclude(ctx.ctx(), ctx.client, req) + if err != nil { + return C.int(errorToRC(err)) + } + + return C.int(errorToRC(resp.Errors())) +} 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..4871094d30a --- /dev/null +++ b/src/control/lib/control/c/system_test.go @@ -0,0 +1,213 @@ +// +// (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 TestSystemStopRank(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), + }, + } { + 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 TestSystemStartRank(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, + }, + "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) + } + }) + } +} + +func TestSystemRankInvalidHandle(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 TestSystemReintRank(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, + }, + } { + 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 TestSystemExcludeRank(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, + }, + } { + 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/test_helpers.go b/src/control/lib/control/c/test_helpers.go new file mode 100644 index 00000000000..7eb74eb330b --- /dev/null +++ b/src/control/lib/control/c/test_helpers.go @@ -0,0 +1,779 @@ +// +// (C) Copyright 2026 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build !release + +package main + +/* +#include +#include +#include +#include +#include +#include +#include + +// Helper to allocate a rank list for testing +static d_rank_list_t *alloc_rank_list(uint32_t nr) { + return d_rank_list_alloc(nr); +} + +// Helper to free a rank list +static void free_rank_list(d_rank_list_t *rl) { + d_rank_list_free(rl); +} + +// Helper to set a rank in a rank list +static void set_rank(d_rank_list_t *rl, uint32_t idx, d_rank_t rank) { + if (rl != NULL && rl->rl_ranks != NULL && idx < rl->rl_nr) { + rl->rl_ranks[idx] = rank; + } +} + +// Helper to get a rank from a rank list +static d_rank_t get_rank(d_rank_list_t *rl, uint32_t idx) { + if (rl != NULL && rl->rl_ranks != NULL && idx < rl->rl_nr) { + return rl->rl_ranks[idx]; + } + return 0; +} +*/ +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" +) + +// testRankList wraps a C rank list for testing. +type testRankList struct { + crl *C.d_rank_list_t +} + +// newTestRankList allocates a rank list with the given capacity. +func newTestRankList(capacity uint32) *testRankList { + return &testRankList{crl: C.alloc_rank_list(C.uint32_t(capacity))} +} + +// free releases the rank list memory. +func (t *testRankList) free() { + if t.crl != nil { + C.free_rank_list(t.crl) + t.crl = nil + } +} + +// setRank sets a rank at the given index. +func (t *testRankList) setRank(idx, rank uint32) { + C.set_rank(t.crl, C.uint32_t(idx), C.d_rank_t(rank)) +} + +// getRank gets a rank at the given index. +func (t *testRankList) getRank(idx uint32) uint32 { + return uint32(C.get_rank(t.crl, C.uint32_t(idx))) +} + +// nr returns the number of ranks. +func (t *testRankList) nr() uint32 { + if t.crl == nil { + return 0 + } + return uint32(t.crl.rl_nr) +} + +// ptr returns the underlying C pointer for passing to functions. +func (t *testRankList) ptr() *C.d_rank_list_t { + return t.crl +} + +// testUUID wraps a C uuid_t for testing. +type testUUID struct { + cuuid C.uuid_t +} + +// newTestUUID creates a new test UUID wrapper. +func newTestUUID() *testUUID { + return &testUUID{} +} + +// set copies a Go UUID into the C uuid. +func (t *testUUID) set(u uuid.UUID) { + copyUUIDToC(u, &t.cuuid) +} + +// get returns the UUID as a Go uuid.UUID. +func (t *testUUID) get() uuid.UUID { + return uuidFromC(&t.cuuid) +} + +// ptr returns a pointer to the C uuid for passing to functions. +func (t *testUUID) ptr() *C.uuid_t { + return &t.cuuid +} + +// testCString wraps a C string for testing. +type testCString struct { + cstr *C.char +} + +// newTestCString creates a C string from a Go string. +func newTestCString(s string) *testCString { + if s == "" { + return &testCString{cstr: nil} + } + return &testCString{cstr: C.CString(s)} +} + +// free releases the C string memory. +func (t *testCString) free() { + if t.cstr != nil { + C.free(unsafe.Pointer(t.cstr)) + t.cstr = nil + } +} + +// ptr returns the C string pointer. +func (t *testCString) ptr() *C.char { + return t.cstr +} + +// toGo converts to Go string using the library's goString function. +func (t *testCString) toGo() string { + return goString(t.cstr) +} + +// callPoolCreate is a test helper that calls daos_control_pool_create +// with the given parameters and returns the result. +func callPoolCreate( + handle cgo.Handle, + uid, gid uint32, + scmSize, nvmeSize uint64, + svc *testRankList, + poolUUID *testUUID, +) int { + return int(daos_control_pool_create( + C.uintptr_t(handle), + C.uid_t(uid), + C.gid_t(gid), + nil, // grp + nil, // tgts + C.daos_size_t(scmSize), + C.daos_size_t(nvmeSize), + nil, // prop + svc.ptr(), + poolUUID.ptr(), + )) +} + +// callPoolCreateInvalidHandle calls pool create with a zero handle. +func callPoolCreateInvalidHandle(poolUUID *testUUID) int { + return int(daos_control_pool_create( + C.uintptr_t(0), + C.uid_t(1000), + C.gid_t(1000), + nil, + nil, + C.daos_size_t(1<<30), + C.daos_size_t(0), + nil, + nil, + poolUUID.ptr(), + )) +} + +// testConvertRankListFromC tests the rankListFromC conversion. +func testConvertRankListFromC(rl *testRankList) []uint32 { + if rl == nil { + return nil + } + ranks := rankListFromC(rl.ptr()) + result := make([]uint32, len(ranks)) + for i, r := range ranks { + result[i] = uint32(r) + } + return result +} + +// testConvertRankListToC tests the copyRankListToC conversion. +// It populates the source rank list with the given ranks, converts to Go, +// then copies back to the destination C rank list. +func testConvertRankListToC(ranks []uint32, rl *testRankList) { + if rl == nil || len(ranks) == 0 { + return + } + // Create a source rank list with the input ranks + from := newTestRankList(uint32(len(ranks))) + defer from.free() + for i, r := range ranks { + from.setRank(uint32(i), r) + } + // Convert to Go slice of ranklist.Rank + goRanks := rankListFromC(from.ptr()) + // Copy back to the destination C rank list + copyRankListToC(goRanks, rl.ptr()) +} + +// makeTestHandle creates a cgo.Handle for a test context with the given mock invoker. +func makeTestHandle(mi control.UnaryInvoker, log *logging.LeveledLogger) cgo.Handle { + ctx := newTestContext(mi, log) + return cgo.NewHandle(ctx) +} + +// callPoolDestroy is a test helper for daos_control_pool_destroy. +func callPoolDestroy(handle cgo.Handle, poolUUID *testUUID, force bool) int { + var forceInt C.int + if force { + forceInt = 1 + } + return int(daos_control_pool_destroy( + C.uintptr_t(handle), + poolUUID.ptr(), + nil, // grp + forceInt, + )) +} + +// callPoolEvict is a test helper for daos_control_pool_evict. +func callPoolEvict(handle cgo.Handle, poolUUID *testUUID) int { + return int(daos_control_pool_evict( + C.uintptr_t(handle), + poolUUID.ptr(), + nil, // grp + )) +} + +// callPoolExclude is a test helper for daos_control_pool_exclude. +func callPoolExclude(handle cgo.Handle, poolUUID *testUUID, rank uint32, tgtIdx int) int { + return int(daos_control_pool_exclude( + C.uintptr_t(handle), + poolUUID.ptr(), + nil, // grp + C.d_rank_t(rank), + C.int(tgtIdx), + )) +} + +// callPoolDrain is a test helper for daos_control_pool_drain. +func callPoolDrain(handle cgo.Handle, poolUUID *testUUID, rank uint32, tgtIdx int) int { + return int(daos_control_pool_drain( + C.uintptr_t(handle), + poolUUID.ptr(), + nil, // grp + C.d_rank_t(rank), + C.int(tgtIdx), + )) +} + +// callPoolReintegrate is a test helper for daos_control_pool_reintegrate. +func callPoolReintegrate(handle cgo.Handle, poolUUID *testUUID, rank uint32, tgtIdx int) int { + return int(daos_control_pool_reintegrate( + C.uintptr_t(handle), + poolUUID.ptr(), + nil, // grp + C.d_rank_t(rank), + C.int(tgtIdx), + )) +} + +// callSystemStopRank is a test helper for daos_control_system_stop_rank. +func callSystemStopRank(handle cgo.Handle, rank uint32, force bool) int { + var forceInt C.int + if force { + forceInt = 1 + } + return int(daos_control_system_stop_rank( + C.uintptr_t(handle), + C.d_rank_t(rank), + forceInt, + )) +} + +// callSystemStartRank is a test helper for daos_control_system_start_rank. +func callSystemStartRank(handle cgo.Handle, rank uint32) int { + return int(daos_control_system_start_rank( + C.uintptr_t(handle), + C.d_rank_t(rank), + )) +} + +// testCharArray wraps a C char array for testing copyStringToCharArray. +type testCharArray struct { + data []C.char +} + +// newTestCharArray creates a char array of the given size. +func newTestCharArray(size int) *testCharArray { + return &testCharArray{data: make([]C.char, size)} +} + +// ptr returns a pointer to the first element. +func (t *testCharArray) ptr() *C.char { + if len(t.data) == 0 { + return nil + } + return &t.data[0] +} + +// toString converts the char array to a Go string (up to null terminator). +func (t *testCharArray) toString() string { + var result []byte + for _, c := range t.data { + if c == 0 { + break + } + result = append(result, byte(c)) + } + return string(result) +} + +// testCopyStringToCharArray tests the copyStringToCharArray helper. +func testCopyStringToCharArray(s string, dest *testCharArray, maxLen int) { + if dest == nil { + return + } + copyStringToCharArray(s, dest.ptr(), maxLen) +} + +// testParseUUID tests the parseUUID helper. +func testParseUUID(s string) ([16]byte, error) { + return parseUUID(s) +} + +// testErrorToRC tests the errorToRC function. +func testErrorToRC(err error) int { + return errorToRC(err) +} + +// callInit is a test helper for daos_control_init with default (insecure) config. +func callInit(configFile, logFile, logLevel string) (cgo.Handle, int) { + var args C.struct_daos_control_init_args + + if configFile != "" { + args.config_file = C.CString(configFile) + defer C.free(unsafe.Pointer(args.config_file)) + } + if logFile != "" { + args.log_file = C.CString(logFile) + defer C.free(unsafe.Pointer(args.log_file)) + } + if logLevel != "" { + args.log_level = C.CString(logLevel) + defer C.free(unsafe.Pointer(args.log_level)) + } + + var handle C.uintptr_t + rc := int(daos_control_init(&args, &handle)) + return cgo.Handle(handle), rc +} + +// callFini is a test helper for daos_control_fini. +func callFini(handle cgo.Handle) { + daos_control_fini(C.uintptr_t(handle)) +} + +// callCheckSwitch is a test helper for daos_control_check_switch. +func callCheckSwitch(handle cgo.Handle, enable bool) int { + var enableInt C.int + if enable { + enableInt = 1 + } + return int(daos_control_check_switch(C.uintptr_t(handle), enableInt)) +} + +// callCheckStart is a test helper for daos_control_check_start. +func callCheckStart(handle cgo.Handle, flags uint32, poolUUIDs []uuid.UUID, policies string) int { + var uuids *C.uuid_t + var cPolicies *C.char + + if len(poolUUIDs) > 0 { + uuidArray := make([]C.uuid_t, len(poolUUIDs)) + for i, u := range poolUUIDs { + copyUUIDToC(u, &uuidArray[i]) + } + uuids = &uuidArray[0] + } + + if policies != "" { + cPolicies = C.CString(policies) + defer C.free(unsafe.Pointer(cPolicies)) + } + + return int(daos_control_check_start( + C.uintptr_t(handle), + C.uint32_t(flags), + C.uint32_t(len(poolUUIDs)), + uuids, + cPolicies, + )) +} + +// callCheckStop is a test helper for daos_control_check_stop. +func callCheckStop(handle cgo.Handle, poolUUIDs []uuid.UUID) int { + var uuids *C.uuid_t + + if len(poolUUIDs) > 0 { + uuidArray := make([]C.uuid_t, len(poolUUIDs)) + for i, u := range poolUUIDs { + copyUUIDToC(u, &uuidArray[i]) + } + uuids = &uuidArray[0] + } + + return int(daos_control_check_stop( + C.uintptr_t(handle), + C.uint32_t(len(poolUUIDs)), + uuids, + )) +} + +// callCheckRepair is a test helper for daos_control_check_repair. +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), + )) +} + +// callCheckQuery is a test helper for daos_control_check_query. +func callCheckQuery(handle cgo.Handle, poolUUIDs []uuid.UUID) int { + var uuids *C.uuid_t + + if len(poolUUIDs) > 0 { + uuidArray := make([]C.uuid_t, len(poolUUIDs)) + for i, u := range poolUUIDs { + copyUUIDToC(u, &uuidArray[i]) + } + uuids = &uuidArray[0] + } + + // Call with nil dci to just test the query path + return int(daos_control_check_query( + C.uintptr_t(handle), + C.uint32_t(len(poolUUIDs)), + uuids, + nil, + )) +} + +// callCheckSetPolicy is a test helper for daos_control_check_set_policy. +func callCheckSetPolicy(handle cgo.Handle, flags uint32, policies string) int { + var cPolicies *C.char + if policies != "" { + cPolicies = C.CString(policies) + defer C.free(unsafe.Pointer(cPolicies)) + } + + return int(daos_control_check_set_policy( + C.uintptr_t(handle), + C.uint32_t(flags), + cPolicies, + )) +} + +// callServerSetLogmasks is a test helper for daos_control_server_set_logmasks. +func callServerSetLogmasks(handle cgo.Handle, masks, streams, subsystems string) int { + var cMasks, cStreams, cSubsystems *C.char + + if masks != "" { + cMasks = C.CString(masks) + defer C.free(unsafe.Pointer(cMasks)) + } + if streams != "" { + cStreams = C.CString(streams) + defer C.free(unsafe.Pointer(cStreams)) + } + if subsystems != "" { + cSubsystems = C.CString(subsystems) + defer C.free(unsafe.Pointer(cSubsystems)) + } + + return int(daos_control_server_set_logmasks( + C.uintptr_t(handle), + cMasks, + cStreams, + cSubsystems, + )) +} + +// callStorageDeviceList is a test helper for daos_control_storage_device_list. +func callStorageDeviceList(handle cgo.Handle) (int, int) { + var ndisks C.int + rc := int(daos_control_storage_device_list( + C.uintptr_t(handle), + &ndisks, + nil, + )) + return int(ndisks), rc +} + +// callStorageSetNVMeFault is a test helper for daos_control_storage_set_nvme_fault. +func callStorageSetNVMeFault(handle cgo.Handle, host string, devUUID *testUUID, force bool) int { + var cHost *C.char + if host != "" { + cHost = C.CString(host) + defer C.free(unsafe.Pointer(cHost)) + } + + var forceInt C.int + if force { + forceInt = 1 + } + + return int(daos_control_storage_set_nvme_fault( + C.uintptr_t(handle), + cHost, + devUUID.ptr(), + forceInt, + )) +} + +// callStorageQueryDeviceHealth is a test helper for daos_control_storage_query_device_health. +func callStorageQueryDeviceHealth(handle cgo.Handle, host, statsKey string, devUUID *testUUID) (string, int) { + var cHost *C.char + if host != "" { + cHost = C.CString(host) + defer C.free(unsafe.Pointer(cHost)) + } + + cStatsKey := C.CString(statsKey) + defer C.free(unsafe.Pointer(cStatsKey)) + + // Allocate output buffer + statsOut := make([]C.char, 256) + rc := int(daos_control_storage_query_device_health( + C.uintptr_t(handle), + cHost, + cStatsKey, + &statsOut[0], + C.int(len(statsOut)), + devUUID.ptr(), + )) + + // Convert output to string + var result []byte + for _, c := range statsOut { + if c == 0 { + break + } + result = append(result, byte(c)) + } + + return string(result), rc +} + +// callSystemReintRank is a test helper for daos_control_system_reint_rank. +func callSystemReintRank(handle cgo.Handle, rank uint32) int { + return int(daos_control_system_reint_rank( + C.uintptr_t(handle), + C.d_rank_t(rank), + )) +} + +// callSystemExcludeRank is a test helper for daos_control_system_exclude_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), + )) +} + +// callPoolExtend is a test helper for daos_control_pool_extend. +func callPoolExtend(handle cgo.Handle, poolUUID *testUUID, ranks []uint32) int { + if len(ranks) == 0 { + return int(daos_control_pool_extend( + C.uintptr_t(handle), + poolUUID.ptr(), + 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), + poolUUID.ptr(), + nil, + &cRanks[0], + C.int(len(ranks)), + )) +} + +// callPoolSetProp is a test helper for daos_control_pool_set_prop. +func callPoolSetProp(handle cgo.Handle, poolUUID *testUUID, name, value string) int { + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + cValue := C.CString(value) + defer C.free(unsafe.Pointer(cValue)) + + return int(daos_control_pool_set_prop( + C.uintptr_t(handle), + poolUUID.ptr(), + cName, + cValue, + )) +} + +// callPoolGetProp is a test helper for daos_control_pool_get_prop. +func callPoolGetProp(handle cgo.Handle, label string, poolUUID *testUUID, name string) (string, int) { + var cLabel *C.char + if label != "" { + cLabel = C.CString(label) + defer C.free(unsafe.Pointer(cLabel)) + } + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + + var cValue *C.char + rc := int(daos_control_pool_get_prop( + C.uintptr_t(handle), + cLabel, + poolUUID.ptr(), + cName, + &cValue, + )) + + var value string + if cValue != nil { + value = C.GoString(cValue) + C.free(unsafe.Pointer(cValue)) + } + return value, rc +} + +// callPoolUpdateACE is a test helper for daos_control_pool_update_ace. +func callPoolUpdateACE(handle cgo.Handle, poolUUID *testUUID, ace string) int { + cACE := C.CString(ace) + defer C.free(unsafe.Pointer(cACE)) + + return int(daos_control_pool_update_ace( + C.uintptr_t(handle), + poolUUID.ptr(), + nil, + cACE, + )) +} + +// callPoolDeleteACE is a test helper for daos_control_pool_delete_ace. +func callPoolDeleteACE(handle cgo.Handle, poolUUID *testUUID, principal string) int { + cPrincipal := C.CString(principal) + defer C.free(unsafe.Pointer(cPrincipal)) + + return int(daos_control_pool_delete_ace( + C.uintptr_t(handle), + poolUUID.ptr(), + nil, + cPrincipal, + )) +} + +// callPoolRebuildStop is a test helper for daos_control_pool_rebuild_stop. +func callPoolRebuildStop(handle cgo.Handle, poolUUID *testUUID, force bool) int { + var forceInt C.int + if force { + forceInt = 1 + } + return int(daos_control_pool_rebuild_stop( + C.uintptr_t(handle), + poolUUID.ptr(), + nil, + forceInt, + )) +} + +// callPoolRebuildStart is a test helper for daos_control_pool_rebuild_start. +func callPoolRebuildStart(handle cgo.Handle, poolUUID *testUUID) int { + return int(daos_control_pool_rebuild_start( + C.uintptr_t(handle), + poolUUID.ptr(), + nil, + )) +} + +// testPoolListInfo wraps pool list info for testing. +type testPoolListInfo struct { + data []C.daos_mgmt_pool_info_t +} + +// newTestPoolListInfo creates a pool list info array. +func newTestPoolListInfo(count int) *testPoolListInfo { + if count == 0 { + return &testPoolListInfo{} + } + return &testPoolListInfo{data: make([]C.daos_mgmt_pool_info_t, count)} +} + +// ptr returns a pointer to the first element. +func (t *testPoolListInfo) ptr() *C.daos_mgmt_pool_info_t { + if len(t.data) == 0 { + return nil + } + return &t.data[0] +} + +// callPoolList is a test helper for daos_control_pool_list. +func callPoolList(handle cgo.Handle, npools *uint64, pools *testPoolListInfo) int { + var cNpools C.daos_size_t + if npools != nil { + cNpools = C.daos_size_t(*npools) + } + + var poolsPtr *C.daos_mgmt_pool_info_t + if pools != nil { + poolsPtr = pools.ptr() + } + + rc := int(daos_control_pool_list( + C.uintptr_t(handle), + nil, + &cNpools, + poolsPtr, + )) + + if npools != nil { + *npools = uint64(cNpools) + } + return rc +} + +// getPoolListLabel returns the label from a pool list entry. +func (t *testPoolListInfo) getLabel(idx int) string { + if idx >= len(t.data) { + return "" + } + if t.data[idx].mgpi_label == nil { + return "" + } + return C.GoString(t.data[idx].mgpi_label) +} + +// getPoolListUUID returns the UUID from a pool list entry. +func (t *testPoolListInfo) getUUID(idx int) uuid.UUID { + if idx >= len(t.data) { + return uuid.Nil + } + return uuidFromC(&t.data[idx].mgpi_uuid) +} + +// freePoolList frees memory allocated by pool list. +func (t *testPoolListInfo) free() { + for i := range t.data { + if t.data[i].mgpi_label != nil { + C.free(unsafe.Pointer(t.data[i].mgpi_label)) + } + if t.data[i].mgpi_svc != nil { + C.free_rank_list(t.data[i].mgpi_svc) + } + } +} diff --git a/src/control/lib/control/check.go b/src/control/lib/control/check.go index c7297e05989..23a5e6a4e4a 100644 --- a/src/control/lib/control/check.go +++ b/src/control/lib/control/check.go @@ -1,6 +1,6 @@ // // (C) Copyright 2022-2023 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 // @@ -11,6 +11,7 @@ import ( "context" "encoding/json" "fmt" + "sort" "strings" "time" @@ -602,6 +603,12 @@ func SystemCheckQuery(ctx context.Context, rpcClient UnaryInvoker, req *SystemCh proto.Merge(rpt, pbReport) resp.Reports = append(resp.Reports, rpt) } + + // Sort reports by class for consistent ordering. + sort.Slice(resp.Reports, func(i, j int) bool { + return resp.Reports[i].Class < resp.Reports[j].Class + }) + 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..843f200ffe4 --- /dev/null +++ b/src/include/daos/control_types.h @@ -0,0 +1,86 @@ +/** + * (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 + +#if defined(__cplusplus) +extern "C" { +#endif + +/** + * Maximum number of targets per device (matches BIO_XS_CNT_MAX). + */ +#define MAX_TEST_TARGETS_PER_DEVICE 48 + +/** + * Maximum hostname length. + */ +#define DSS_HOSTNAME_MAX_LEN 255 + +/** + * Storage device information. + */ +typedef struct device_list { + 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; + +/** + * 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_result; + int dcri_option_nr; + int dcri_options[4]; +}; + +/** + * DAOS checker query results. + */ +struct daos_check_info { + char *dci_status; + char *dci_phase; + 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 9bb15883b8c..04d25b0c39f 100644 --- a/src/include/daos/tests_lib.h +++ b/src/include/daos/tests_lib.h @@ -1,6 +1,6 @@ /** * (C) Copyright 2015-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 */ @@ -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,31 +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_result; - int dcri_option_nr; - int dcri_options[4]; -}; - -struct daos_check_info { - char *dci_status; - char *dci_phase; - 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 @@ -724,4 +687,21 @@ int */ int dmg_check_set_policy(const char *dmg_config_file, uint32_t flags, const char *policies); +/** + * Initialize the DMG control context. + * + * \param dmg_config_file + * [IN] DMG config file path. + * + * \return Zero on success, negative value if error. + */ +int +dmg_init(const char *dmg_config_file); + +/** + * Finalize the DMG control context. + */ +void +dmg_fini(void); + #endif /* __DAOS_TESTS_LIB_H__ */ diff --git a/src/tests/SConscript b/src/tests/SConscript index be22f9a0bd2..eebf57f477e 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']