blob: a48eeb9a460d08c0cf40a6fd661351c08d3186b3 [file] [edit]
/*****************************************************************************\
* Copyright (C) SchedMD LLC.
*****************************************************************************
* spank_plugin.c - Generic SPANK plugin for testing.
*
* Configuration is via environment variables:
*
* FAILURE INJECTION
* SPANK_FAIL_TEST_FUNC -- SPANK callback name to fail in
* (e.g. slurm_spank_init, slurm_spank_user_init).
* SPANK_FAIL_TEST_CTXT -- SPANK context in which to fail (local, remote,
* allocator, slurmd, job_script).
* SPANK_FAIL_TEST_MODE -- optional. Affects only slurm_spank_init failures:
* "node" -> ESPANK_NODE_FAILURE (default)
* "job" -> ESPANK_JOB_FAILURE
* If FUNC and CTXT are both unset the plugin is inert on this path.
* The targeted callback returns -ESPANK_ERROR; others return
* ESPANK_SUCCESS. Combine with SPANK_HOOK_CREATE_FILE to record which
* callbacks ran and what they returned.
*
* HOOK MARKER FILES
* SPANK_HOOK_CREATE_FILE -- boolean (any non-empty value enables). Each hook
* that runs appends one line to SPANK_TMP_DIR/
* {hookname}_{context}_log to signal it executed:
* job_id=<id> rc=<rc>
* where rc is what the hook returned. The context is
* part of the name because the same hook may run as
* different users (e.g. root in slurmstepd and the
* job user in srun), so they can't share a file.
* SPANK_TMP_DIR is baked in at compile time. Nothing
* is printed to the user's stderr, where it could
* interleave with output of other processes.
*
* JOB INFO LOGGING
* SPANK_HOOK_LOG_JOB_INFO -- boolean (any non-empty value enables). Logs what
* spank_get_item() and slurm_load_job() report for each
* task, appending to SPANK_TMP_DIR/spank_job_info.log.
* All lines share one layout so a single regex parses:
* caller=<func> source=<source> \
* self_job_id=<id> self_step_id=<id> \
* job_id=<id> array_job_id=<id> array_task_id=<id> \
* [rc=<rc>]
* Inert if unset.
*
* EXTERN STEP HOLD
* SPANK_TMP_DIR/hold_extern -- if this file exists, slurm_spank_init() in
* remote context of the extern step creates
* SPANK_TMP_DIR/blocked.<job_id> and then waits, for
* at most 300 seconds, for SPANK_TMP_DIR/
* release.<job_id> to exist. slurmstepd runs it after
* its socket is open and before the task plugins set
* up the step, so it holds that window open until
* the test releases it. Every other step and context
* returns at once. This one is triggered by a file
* and not by an environment variable because the
* extern step is not created by the submitter.
*
* All capabilities are independent: any combination may be active at
* the same time. In remote context, env vars are read via spank_getenv()
* because the process environment is wiped; in other contexts, getenv() is
* used on the client process environment.
\*****************************************************************************/
#include <fcntl.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <slurm/slurm.h>
#include <slurm/spank.h>
#ifndef PLUGIN_NAME
#define PLUGIN_NAME spank_plugin
#endif
#ifndef SPANK_TMP_DIR
#error "SPANK_TMP_DIR must be defined at compile time (passed via -DSPANK_TMP_DIR=...)"
#endif
SPANK_PLUGIN(PLUGIN_NAME, 1);
#define MAX_PATH 4096
#define MAX_LINE 1024
#define HOLD_EXTERN_MAX_WAIT 300
int slurm_spank_init_failure_mode = ESPANK_NODE_FAILURE;
static char *current_context(void)
{
switch (spank_context()) {
case S_CTX_ERROR:
return "error";
case S_CTX_LOCAL:
return "local";
case S_CTX_REMOTE:
return "remote";
case S_CTX_ALLOCATOR:
return "allocator";
case S_CTX_SLURMD:
return "slurmd";
case S_CTX_JOB_SCRIPT:
return "job_script";
default:
return "unknown";
}
}
/*
* Read env var by name. In remote context the process env is wiped, so
* spank_getenv() is the only path that sees what the submitter sent over.
* In other contexts fall back to getenv() on the client process env.
* Returns 0 on success (out is NUL-terminated and non-empty), -1 otherwise.
*/
static int _get_env(spank_t sp, const char *name, char *out, size_t outsz)
{
if (spank_context() == S_CTX_REMOTE) {
if (spank_getenv(sp, name, out, outsz - 1) == ESPANK_SUCCESS &&
out[0] != '\0')
return 0;
return -1;
}
const char *v = getenv(name);
if (v && v[0]) {
strncpy(out, v, outsz - 1);
out[outsz - 1] = '\0';
return 0;
}
return -1;
}
/*
* Append one line with a single write(2) on an O_APPEND fd: lines are shorter
* than PIPE_BUF, so concurrent tasks can't interleave them, while stdio buffers
* would be lost when the task exec's right after slurm_spank_task_init().
*/
static void _log_line(const char *path, const char *fmt, ...)
{
char line[MAX_LINE];
int len, fd;
va_list ap;
va_start(ap, fmt);
len = vsnprintf(line, sizeof(line), fmt, ap);
va_end(ap);
if (len < 0)
return;
if (len >= (int) sizeof(line))
len = sizeof(line) - 1;
if ((fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0666)) < 0) {
slurm_error("%s: unable to open %s: %m", plugin_name, path);
return;
}
if (write(fd, line, len) != len)
slurm_error("%s: unable to write to %s: %m", plugin_name, path);
close(fd);
}
/*
* Append to SPANK_TMP_DIR/{hookname}_{context}_log as a marker that this hook
* executed, recording the job and the rc the hook returns.
* SPANK_TMP_DIR is baked in at compile time by spank_plugin via spank_tmp.
*/
static void _write_hook_marker(spank_t sp, const char *func, int rc)
{
char path[MAX_PATH + 64];
uint32_t jobid = 0;
spank_get_item(sp, S_JOB_ID, &jobid);
snprintf(path, sizeof(path), SPANK_TMP_DIR "/%s_%s_log", func,
current_context());
_log_line(path, "job_id=%u rc=%d\n", jobid, rc);
}
static void _log_job_info(spank_t sp, const char *path, const char *caller)
{
/*
* slurm_load_job() can only be called from slurm_spank_task_init,
* once slurmstepd dropped its auth setuid lock.
*/
if (strcmp(caller, "slurm_spank_task_init") != 0)
return;
uint32_t step_id = NO_VAL, job_id = NO_VAL;
uint32_t array_job_id = NO_VAL, array_task_id = NO_VAL;
job_info_msg_t *job_info = NULL;
uint32_t i;
int rc;
if (!(rc = spank_get_item(sp, S_JOB_STEPID, &step_id)) &&
!(rc = spank_get_item(sp, S_JOB_ID, &job_id)) &&
!(rc = spank_get_item(sp, S_JOB_ARRAY_ID, &array_job_id)))
rc = spank_get_item(sp, S_JOB_ARRAY_TASK_ID, &array_task_id);
if (rc) {
_log_line(path,
"caller=%s source=spank_get_item_error self_job_id=%u self_step_id=%u job_id=%u array_job_id=%u array_task_id=%u rc=%d\n",
caller, job_id, step_id, job_id, array_job_id,
array_task_id, rc);
return;
}
_log_line(path,
"caller=%s source=spank_get_item self_job_id=%u self_step_id=%u job_id=%u array_job_id=%u array_task_id=%u\n",
caller, job_id, step_id, job_id, array_job_id, array_task_id);
/*
* This RPC deadlocks the task unless slurmstepd dropped its auth setuid
* lock before the SPANK stack (task.c:spank_user_task).
*/
#if SLURM_VERSION_NUMBER >= SLURM_VERSION_NUM(26, 5, 0)
slurm_step_id_t load_step_id = SLURM_STEP_ID_INITIALIZER;
load_step_id.job_id = job_id;
rc = slurm_load_job(&job_info, load_step_id, SHOW_DETAIL);
#else /* Ticket 13506 (!3137): Change API to accept slurm_step_id_t */
rc = slurm_load_job(&job_info, job_id, SHOW_DETAIL);
#endif
if (rc) {
_log_line(path,
"caller=%s source=load_job_error self_job_id=%u self_step_id=%u job_id=%u array_job_id=%u array_task_id=%u rc=%d\n",
caller, job_id, step_id, job_id, array_job_id,
array_task_id, rc);
return;
}
for (i = 0; i < job_info->record_count; i++) {
slurm_job_info_t *job = job_info->job_array + i;
_log_line(path,
"caller=%s source=load_job self_job_id=%u self_step_id=%u job_id=%u array_job_id=%u array_task_id=%u\n",
caller, job_id, step_id,
#if SLURM_VERSION_NUMBER >= SLURM_VERSION_NUM(25, 11, 0)
job->step_id.job_id,
#else
job->job_id,
#endif
job->array_job_id, job->array_task_id);
}
slurm_free_job_info_msg(job_info);
}
/*
* Hold the extern step in slurm_spank_init() while SPANK_TMP_DIR/hold_extern
* exists, until SPANK_TMP_DIR/release.<job_id> is created.
*/
static void _hold_extern_step(spank_t sp)
{
char path[MAX_PATH + 64];
uint32_t job_id = 0, step_id = 0;
int fd;
if ((spank_context() != S_CTX_REMOTE) ||
access(SPANK_TMP_DIR "/hold_extern", F_OK) ||
(spank_get_item(sp, S_JOB_STEPID, &step_id) != ESPANK_SUCCESS) ||
(step_id != SLURM_EXTERN_CONT) ||
(spank_get_item(sp, S_JOB_ID, &job_id) != ESPANK_SUCCESS))
return;
snprintf(path, sizeof(path), SPANK_TMP_DIR "/blocked.%u", job_id);
if ((fd = open(path, (O_CREAT | O_WRONLY), 0644)) >= 0)
close(fd);
snprintf(path, sizeof(path), SPANK_TMP_DIR "/release.%u", job_id);
for (int i = 0; (i < HOLD_EXTERN_MAX_WAIT) && access(path, F_OK); i++)
sleep(1);
}
/*
* Failure injection: check whether this callback is the targeted one.
* Returns -ESPANK_ERROR when targeted, ESPANK_SUCCESS otherwise.
*/
static int _fail_if_targeted(spank_t sp, const char *func)
{
char target_func[256] = {0};
char target_ctxt[256] = {0};
char mode[32] = {0};
/* Both must be set to arm the failure injection path. */
if (_get_env(sp, "SPANK_FAIL_TEST_FUNC", target_func,
sizeof(target_func)) ||
_get_env(sp, "SPANK_FAIL_TEST_CTXT", target_ctxt,
sizeof(target_ctxt)))
return ESPANK_SUCCESS;
if (strcmp(target_func, func) || strcmp(target_ctxt, current_context()))
return ESPANK_SUCCESS;
/* This is the targeted callback. */
if (_get_env(sp, "SPANK_FAIL_TEST_MODE", mode, sizeof(mode)) == 0) {
if (!strcasecmp(mode, "job"))
slurm_spank_init_failure_mode = ESPANK_JOB_FAILURE;
else if (!strcasecmp(mode, "node"))
slurm_spank_init_failure_mode = ESPANK_NODE_FAILURE;
}
return -ESPANK_ERROR;
}
#define ENTRYPOINT(FUNC) \
extern int FUNC(spank_t sp, int ac, char **av) \
{ \
/* Get the desired rc */ \
int _rc = _fail_if_targeted(sp, __func__); \
\
/* Create the log file with the desired rc if configured */ \
char _enabled[4] = {0}; \
if (_get_env(sp, "SPANK_HOOK_CREATE_FILE", \
_enabled, sizeof(_enabled)) == 0) \
_write_hook_marker(sp, __func__, _rc); \
\
/* Log job info in the log file if configured */ \
if (spank_context() == S_CTX_REMOTE) { \
char _ji[4] = {0}; \
if (_get_env(sp, "SPANK_HOOK_LOG_JOB_INFO", _ji, \
sizeof(_ji)) == 0) \
_log_job_info(sp, \
SPANK_TMP_DIR \
"/spank_job_info.log", \
__func__); \
} \
\
/* Hold the extern step in init if configured */ \
if (!strcmp(__func__, "slurm_spank_init")) \
_hold_extern_step(sp); \
\
/* Return failure if configured */ \
return _rc; \
}
ENTRYPOINT(slurm_spank_init)
ENTRYPOINT(slurm_spank_job_prolog)
ENTRYPOINT(slurm_spank_init_post_opt)
ENTRYPOINT(slurm_spank_local_user_init)
ENTRYPOINT(slurm_spank_user_init)
ENTRYPOINT(slurm_spank_task_init_privileged)
ENTRYPOINT(slurm_spank_task_init)
ENTRYPOINT(slurm_spank_task_post_fork)
ENTRYPOINT(slurm_spank_task_exit)
ENTRYPOINT(slurm_spank_job_epilog)
ENTRYPOINT(slurm_spank_slurmd_exit)
ENTRYPOINT(slurm_spank_exit)