| /*****************************************************************************\ |
| * commands.c - Slurm scrun commands handler |
| ***************************************************************************** |
| * Copyright (C) SchedMD LLC. |
| * |
| * This file is part of Slurm, a resource management program. |
| * For details, see <https://slurm.schedmd.com/>. |
| * Please also read the included file: DISCLAIMER. |
| * |
| * Slurm is free software; you can redistribute it and/or modify it under |
| * the terms of the GNU General Public License as published by the Free |
| * Software Foundation; either version 2 of the License, or (at your option) |
| * any later version. |
| * |
| * In addition, as a special exception, the copyright holders give permission |
| * to link the code of portions of this program with the OpenSSL library under |
| * certain conditions as described in each individual source file, and |
| * distribute linked combinations including the two. You must obey the GNU |
| * General Public License in all respects for all of the code used other than |
| * OpenSSL. If you modify file(s) with this exception, you may extend this |
| * exception to your version of the file(s), but you are not obligated to do |
| * so. If you do not wish to do so, delete this exception statement from your |
| * version. If you delete this exception statement from all source files in |
| * the program, then also delete it here. |
| * |
| * Slurm is distributed in the hope that it will be useful, but WITHOUT ANY |
| * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
| * details. |
| * |
| * You should have received a copy of the GNU General Public License along |
| * with Slurm; if not, write to the Free Software Foundation, Inc., |
| * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| \*****************************************************************************/ |
| |
| #include "config.h" |
| |
| #include <signal.h> |
| #include <unistd.h> |
| |
| #include "src/common/daemonize.h" |
| #include "src/common/fd.h" |
| #include "src/common/log.h" |
| #include "src/common/proc_args.h" |
| #include "src/common/read_config.h" |
| #include "src/common/xassert.h" |
| #include "src/common/xmalloc.h" |
| #include "src/common/xstring.h" |
| #include "src/interfaces/serializer.h" |
| |
| #include "src/scrun/scrun.h" |
| |
| static data_for_each_cmd_t _foreach_load_annotation(const char *key, |
| data_t *data, void *arg) |
| { |
| if (data_convert_type(data, DATA_TYPE_STRING) != DATA_TYPE_STRING) { |
| return DATA_FOR_EACH_FAIL; |
| } |
| |
| add_key_pair(state.annotations, key, "%s", data_get_string(data)); |
| |
| return DATA_FOR_EACH_CONT; |
| } |
| |
| static void _load_config() |
| { |
| int rc; |
| data_t *term, *ver, *rp, *annot, *config = NULL; |
| buf_t *buf; |
| |
| xassert(!state.config_file); |
| xstrfmtcat(state.config_file, "%s/config.json", state.bundle); |
| |
| if (!(buf = create_mmap_buf(state.config_file))) |
| fatal("unable to load %s: %m", state.config_file); |
| |
| if ((rc = serialize_g_string_to_data(&config, get_buf_data(buf), |
| size_buf(buf), MIME_TYPE_JSON))) { |
| fatal("unable to parse %s: %s", |
| state.config_file, slurm_strerror(rc)); |
| } |
| |
| FREE_NULL_BUFFER(buf); |
| debug("%s: loaded container config: %s", __func__, state.config_file); |
| xassert(!state.config); |
| state.config = config; |
| |
| /* parse out key fields */ |
| xassert(!state.root_path); |
| rp = data_resolve_dict_path(state.config, "/root/path/"); |
| if (data_get_type(rp) != DATA_TYPE_STRING) |
| fatal("Invalid /root/path"); |
| |
| /* need absolute path for root path */ |
| if (data_get_string(rp)[0] == '/') |
| state.root_path = xstrdup(data_get_string(rp)); |
| else |
| state.root_path = xstrdup_printf("%s/%s", state.bundle, |
| data_get_string(rp)); |
| state.orig_root_path = xstrdup(state.root_path); |
| |
| if ((annot = data_key_get(state.config, "annotations")) && |
| (data_dict_for_each(annot, _foreach_load_annotation, NULL) < 0)) |
| fatal("Invalid /annotations"); |
| |
| ver = data_resolve_dict_path(state.config, "/ociVersion/"); |
| if (data_get_type(ver) != DATA_TYPE_STRING) |
| fatal("Invalid /ociVersion/ type %s", |
| data_get_type_string(ver)); |
| xfree(state.oci_version); |
| state.oci_version = xstrdup(data_get_string(ver)); |
| |
| if ((term = data_resolve_dict_path(state.config, |
| "/process/terminal"))) { |
| if (data_get_type(term) != DATA_TYPE_BOOL) |
| fatal("Invalid /process/terminal type %s", |
| data_get_type_string(term)); |
| state.requested_terminal = data_get_bool(term); |
| } else { |
| state.requested_terminal = false; |
| } |
| |
| } |
| |
| static data_for_each_cmd_t _foreach_env(data_t *data, void *arg) |
| { |
| int *i = arg; |
| static const char *match_env[] = { |
| "SCRUN_", |
| "SLURM_", |
| }; |
| |
| if (data_convert_type(data, DATA_TYPE_STRING) != DATA_TYPE_STRING) |
| fatal("%s: expected string at /process/env[%d] in %s but found type %s", |
| __func__, *i, state.config_file, |
| data_get_type_string(data)); |
| |
| for (int j = 0; match_env[j]; j++) { |
| if (xstrncmp(match_env[j], data_get_string(data), |
| strlen(match_env[j]))) { |
| setenvfs("%s", data_get_string(data)); |
| break; |
| } |
| } |
| |
| (*i)++; |
| |
| return DATA_FOR_EACH_CONT; |
| } |
| |
| static void _load_config_environ() |
| { |
| int i = 0; |
| |
| data_t *denv = data_resolve_dict_path(state.config, "/process/env/"); |
| |
| if (!denv) |
| return; |
| |
| if (data_get_type(denv) != DATA_TYPE_LIST) |
| fatal("%s: expected list at /process/env/ in %s but found type %s", |
| __func__, state.config_file, data_get_type_string(denv)); |
| |
| (void) data_list_for_each(denv, _foreach_env, &i); |
| } |
| |
| extern int command_create(void) |
| { |
| xstrfmtcat(state.spool_dir, "%s/%s/", state.root_dir, state.id); |
| |
| if (mkdirpath(state.spool_dir, S_IRWXU, true)) { |
| if (errno != EEXIST) { |
| fatal("%s: unable to create spool directory %s: %s", |
| __func__, state.spool_dir, slurm_strerror(errno)); |
| } else { |
| debug2("%s: spool directory %s already exists", |
| __func__, state.spool_dir); |
| } |
| } else { |
| debug2("%s: created spool directory %s", |
| __func__, state.spool_dir); |
| } |
| |
| _load_config(); |
| _load_config_environ(); |
| |
| return spawn_anchor(); |
| } |
| |
| extern int command_version(void) |
| { |
| printf("scrun version %s\nspec: %s\n", |
| SLURM_VERSION_STRING, OCI_VERSION); |
| return SLURM_SUCCESS; |
| } |
| |
| extern int command_start(void) |
| { |
| slurm_msg_t req, *resp = NULL; |
| int rc; |
| slurm_step_id_t step = {0}; |
| |
| get_anchor_state(); |
| check_state(); |
| |
| debug("%s: processing %s in state:%s", |
| __func__, state.id, |
| slurm_container_status_to_str(state.status)); |
| |
| slurm_msg_t_init(&req); |
| slurm_msg_set_r_uid(&req, SLURM_AUTH_UID_ANY); |
| req.msg_type = REQUEST_CONTAINER_START; |
| |
| if ((rc = send_rpc(&req, &resp, state.id, NULL))) |
| fatal("%s: send_rpc() failed: %s", |
| __func__, slurm_strerror(rc)); |
| |
| slurm_free_msg_members(&req); |
| |
| if (resp && resp->data && |
| (resp->msg_type == RESPONSE_CONTAINER_START)) { |
| container_started_msg_t *st_msg = resp->data; |
| |
| rc = st_msg->rc; |
| step = st_msg->step; |
| } else { |
| fatal("%s: unexpected RPC=%u response", |
| __func__, resp->msg_type); |
| } |
| |
| if (!rc) { |
| debug("%s: container %s start requested JobId=%u StepId=%u", |
| __func__, state.id, step.job_id, step.step_id); |
| } else if (rc == ESLURM_CAN_NOT_START_IMMEDIATELY) { |
| slurm_free_msg(resp); |
| resp = NULL; |
| } else { |
| error("%s: container %s start JobId=%u StepId=%u failed: %s", |
| __func__, state.id, step.job_id, step.step_id, |
| slurm_strerror(rc)); |
| } |
| |
| #ifdef MEMORY_LEAK_DEBUG |
| slurm_free_msg(resp); |
| #endif /* MEMORY_LEAK_DEBUG */ |
| |
| return rc; |
| } |
| |
| static int _foreach_state_annotation(void *x, void *arg) |
| { |
| config_key_pair_t *key_pair_ptr = x; |
| data_t *annot = arg; |
| |
| data_set_string(data_key_set(annot, key_pair_ptr->name), |
| key_pair_ptr->value); |
| |
| return SLURM_SUCCESS; |
| } |
| |
| extern int command_state(void) |
| { |
| int rc = SLURM_SUCCESS; |
| char *str = NULL, *status_str; |
| container_state_msg_status_t status; |
| data_t *o = data_set_dict(data_new()); |
| |
| debug("%s: processing for %s", __func__, state.id); |
| |
| get_anchor_state(); |
| |
| debug("%s: got container:%s state:%s", |
| __func__, state.id, slurm_container_status_to_str(state.status)); |
| |
| status = state.status; |
| |
| /* |
| * Translate internal status to a OCI compliant status: |
| * https://github.com/opencontainers/runtime-spec/blame/main/runtime.md#L19 |
| */ |
| switch (status) { |
| case CONTAINER_ST_INVALID : |
| case CONTAINER_ST_MAX : |
| fatal("%s: status %d should never happen", __func__, status); |
| case CONTAINER_ST_STARTING : |
| status = CONTAINER_ST_CREATING; |
| break; |
| case CONTAINER_ST_CREATING : |
| case CONTAINER_ST_CREATED : |
| case CONTAINER_ST_RUNNING : |
| /* no need to override */ |
| break; |
| case CONTAINER_ST_STOPPING : |
| case CONTAINER_ST_STOPPED : |
| case CONTAINER_ST_UNKNOWN : |
| status = CONTAINER_ST_STOPPED; |
| break; |
| } |
| if (status >= CONTAINER_ST_STOPPED) |
| status = CONTAINER_ST_STOPPED; |
| |
| /* callers may be case-sensitive */ |
| status_str = xstrdup(slurm_container_status_to_str(status)); |
| xstrtolower(status_str); |
| |
| data_set_string(data_key_set(o, "ociVersion"), state.oci_version); |
| data_set_string(data_key_set(o, "id"), state.id); |
| data_set_string_own(data_key_set(o, "status"), status_str); |
| |
| data_set_int(data_key_set(o, "pid"), state.pid); |
| data_set_string(data_key_set(o, "bundle"), state.bundle); |
| |
| list_for_each_ro(state.annotations, _foreach_state_annotation, |
| data_set_dict(data_key_set(o, "annotations"))); |
| |
| if ((rc = serialize_g_data_to_string(&str, NULL, o, MIME_TYPE_JSON, |
| SER_FLAGS_NONE))) |
| fatal("unable to serialise: %s", slurm_strerror(rc)); |
| |
| printf("%s\n", str); |
| |
| xfree(str); |
| |
| debug("%s: state with anchor status=%s and reported status=%s complete: %s", |
| __func__, slurm_container_status_to_str(status), |
| slurm_container_status_to_str(status), slurm_strerror(rc)); |
| return rc; |
| } |
| |
| extern int command_kill(void) |
| { |
| slurm_msg_t req, *resp = NULL; |
| container_signal_msg_t sig_msg; |
| int rc, signal = state.requested_signal; |
| |
| debug("%s: processing %s", __func__, state.id); |
| |
| get_anchor_state(); |
| |
| if (state.status >= CONTAINER_ST_STOPPED) { |
| debug("%s: container:%s already stopped (state:%s)", |
| __func__, state.id, |
| slurm_container_status_to_str(state.status)); |
| return SLURM_SUCCESS; |
| } |
| |
| debug("%s: got container:%s state:%s", |
| __func__, state.id, slurm_container_status_to_str(state.status)); |
| |
| slurm_msg_t_init(&req); |
| slurm_msg_set_r_uid(&req, SLURM_AUTH_UID_ANY); |
| req.msg_type = REQUEST_CONTAINER_KILL; |
| sig_msg.signal = signal; |
| req.data = &sig_msg; |
| |
| debug("%s: requesting signal %s be sent to %s", |
| __func__, strsignal(signal), state.id); |
| if ((rc = send_rpc(&req, &resp, state.id, NULL))) { |
| if (state.jobid) { |
| debug("%s: unable to connect to anchor to signal %s container %s directly: %s", |
| __func__, strsignal(signal), state.id, |
| slurm_strerror(rc)); |
| |
| rc = slurm_signal_job(state.jobid, signal); |
| if ((rc == SLURM_ERROR) && errno) |
| rc = errno; |
| |
| if (rc == ESLURM_ALREADY_DONE) { |
| info("%s: JobId=%u with container %s already complete", |
| __func__, state.jobid, state.id); |
| rc = SLURM_SUCCESS; |
| } else if (rc) { |
| error("%s: unable to signal %s container %s or signal JobId=%u: %m", |
| __func__, strsignal(signal), state.id, |
| state.jobid); |
| } else { |
| info("%s: JobId=%u running container %s has been sent signal %s", |
| __func__, state.jobid, state.id, |
| strsignal(signal)); |
| rc = SLURM_SUCCESS; |
| } |
| } else { |
| /* assume job has already ran and been purged */ |
| info("%s: container %s assumed already complete", |
| __func__, state.id); |
| rc = SLURM_SUCCESS; |
| } |
| } else if (resp && resp->data) { |
| if (resp->msg_type == RESPONSE_CONTAINER_KILL) { |
| return_code_msg_t *rc_msg = resp->data; |
| |
| if ((rc = rc_msg->return_code)) |
| error("%s: unable to signal container %s: %s", |
| __func__, state.id, |
| slurm_strerror(rc_msg->return_code)); |
| else |
| info("%s: successfully sent signal %s to container %s", |
| __func__, strsignal(signal), state.id); |
| } else { |
| error("%s: unexpected response RPC#%u", |
| __func__, resp->msg_type); |
| } |
| } |
| |
| #ifdef MEMORY_LEAK_DEBUG |
| req.data = NULL; /* on stack */ |
| slurm_free_msg_members(&req); |
| slurm_free_msg(resp); |
| #endif /* MEMORY_LEAK_DEBUG */ |
| |
| debug("%s: kill complete: %s", __func__, slurm_strerror(rc)); |
| return rc; |
| } |
| |
| extern int command_delete(void) |
| { |
| int rc; |
| slurm_msg_t req, *resp = NULL; |
| container_delete_msg_t delete_msg; |
| |
| debug("%s: processing %s", __func__, state.id); |
| |
| get_anchor_state(); |
| |
| if (state.status >= CONTAINER_ST_STOPPED) { |
| /* |
| * containers will auto cleanup and delete themselves so we can |
| * just ignore this request |
| */ |
| debug("container %s already stopped", state.id); |
| return SLURM_SUCCESS; |
| } |
| |
| debug("sending delete RPC %s", state.id); |
| |
| slurm_msg_t_init(&req); |
| slurm_msg_set_r_uid(&req, SLURM_AUTH_UID_ANY); |
| req.msg_type = REQUEST_CONTAINER_DELETE; |
| delete_msg.force = state.force; |
| req.data = &delete_msg; |
| |
| if ((rc = send_rpc(&req, &resp, state.id, NULL))) { |
| int signal = SIGTERM; |
| |
| if (state.jobid) { |
| debug("%s: unable to connect to anchor to delete container %s directly: %s", |
| __func__, state.id, slurm_strerror(rc)); |
| |
| if (slurm_signal_job(state.jobid, signal)) { |
| rc = errno; |
| error("%s: unable to signal %s container %s or signal JobId=%u: %m", |
| __func__, strsignal(signal), state.id, |
| state.jobid); |
| } else { |
| info("%s: JobId=%u running container %s has been sent signal %s", |
| __func__, state.jobid, state.id, |
| strsignal(signal)); |
| rc = SLURM_SUCCESS; |
| } |
| } else { |
| if (state.force) { |
| /* assume job has already ran and been purged */ |
| info("%s: container %s assumed already deleted", |
| __func__, state.id); |
| rc = SLURM_SUCCESS; |
| } else { |
| /* no known job: nothing else we can do here */ |
| error("%s: unable to delete container %s: %s", |
| __func__, state.id, slurm_strerror(rc)); |
| rc = ESLURM_INVALID_JOB_ID; |
| } |
| } |
| } |
| |
| if (!rc && resp && resp->data && |
| (resp->msg_type == RESPONSE_CONTAINER_DELETE)) { |
| return_code_msg_t *rc_msg = resp->data; |
| |
| if (rc_msg->return_code) { |
| error("%s: unable to delete container %s: %s", |
| __func__, state.id, |
| slurm_strerror(rc_msg->return_code)); |
| } else { |
| debug("%s: delete container %s successful", |
| __func__, state.id); |
| } |
| |
| rc = rc_msg->return_code; |
| } |
| |
| #ifdef MEMORY_LEAK_DEBUG |
| req.data = NULL; /* on stack */ |
| slurm_free_msg_members(&req); |
| slurm_free_msg(resp); |
| #endif /* MEMORY_LEAK_DEBUG */ |
| |
| debug("%s: delete complete: %s", __func__, slurm_strerror(rc)); |
| return rc; |
| } |