| /*****************************************************************************\ | |
| * scrontab.c | |
| ***************************************************************************** | |
| * 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 <ctype.h> | |
| #include <stdio.h> | |
| #include <sys/stat.h> | |
| #include <sys/types.h> | |
| #include <unistd.h> | |
| #include "src/common/bitstring.h" | |
| #include "src/common/cron.h" | |
| #include "src/common/env.h" | |
| #include "src/common/fetch_config.h" | |
| #include "src/common/log.h" | |
| #include "src/common/read_config.h" | |
| #include "src/common/ref.h" | |
| #include "src/common/slurm_opt.h" | |
| #include "src/common/spank.h" | |
| #include "src/common/uid.h" | |
| #include "src/common/xmalloc.h" | |
| #include "src/common/xstring.h" | |
| #include "src/interfaces/cli_filter.h" | |
| #include "src/interfaces/hash.h" | |
| #include "src/interfaces/runtime.h" | |
| #include "scrontab.h" | |
| #define OPT_LONG_AUTOCOMP 0x100 | |
| static void _usage(void); | |
| decl_static_data(default_crontab_txt); | |
| decl_static_data(usage_txt); | |
| static uid_t uid; | |
| static gid_t gid; | |
| static bool edit_only = false; | |
| static bool first_form = false; | |
| static bool list_only = false; | |
| static bool remove_only = false; | |
| /* Name of input file. May be "-". */ | |
| static const char *infile = NULL; | |
| scron_opt_t scopt; | |
| slurm_opt_t opt = { | |
| .scron_opt = &scopt, | |
| .help_func = _usage, | |
| .usage_func = _usage | |
| }; | |
| static void _usage(void) | |
| { | |
| char *txt; | |
| static_ref_to_cstring(txt, usage_txt); | |
| fprintf(stderr, "%s", txt); | |
| xfree(txt); | |
| } | |
| static void _parse_args(int argc, char **argv) | |
| { | |
| log_options_t logopt = LOG_OPTS_STDERR_ONLY; | |
| int c = 0, option_index = 0; | |
| static struct option long_options[] = { | |
| {"autocomplete", required_argument, 0, OPT_LONG_AUTOCOMP}, | |
| {NULL, no_argument, 0, 'e'}, | |
| {NULL, no_argument, 0, 'l'}, | |
| {NULL, no_argument, 0, 'r'}, | |
| {NULL, required_argument, 0, 'u'}, | |
| {NULL, no_argument, 0, 'v'}, | |
| {NULL, 0, 0, 0}}; | |
| log_init(xbasename(argv[0]), logopt, 0, NULL); | |
| uid = getuid(); | |
| gid = getgid(); | |
| opterr = 0; | |
| while ((c = getopt_long(argc, argv, "elru:v", | |
| long_options, &option_index)) != -1) { | |
| switch (c) { | |
| case (int)'e': | |
| if (!isatty(STDIN_FILENO)) | |
| fatal("Standard input is not a TTY"); | |
| edit_only = true; | |
| break; | |
| case (int)'l': | |
| list_only = true; | |
| break; | |
| case (int)'r': | |
| remove_only = true; | |
| break; | |
| case (int)'u': | |
| if (uid_from_string(optarg, &uid)) | |
| fatal("Could not find user %s", optarg); | |
| gid = gid_from_uid(uid); | |
| break; | |
| case (int)'v': | |
| logopt.stderr_level++; | |
| log_alter(logopt, 0, NULL); | |
| break; | |
| case OPT_LONG_AUTOCOMP: | |
| suggest_completion(long_options, optarg); | |
| exit(0); | |
| break; | |
| default: | |
| _usage(); | |
| exit(1); | |
| } | |
| } | |
| if (edit_only || list_only || remove_only) { | |
| if (optind < argc) { | |
| _usage(); | |
| exit(1); | |
| } | |
| return; | |
| } | |
| if ((argc - optind) > 1) { | |
| _usage(); | |
| exit(1); | |
| } | |
| if (optind < argc) { | |
| first_form = true; | |
| infile = argv[optind]; | |
| } else if (isatty(STDIN_FILENO)) { | |
| edit_only = true; | |
| } else { | |
| first_form = true; | |
| infile = "-"; | |
| } | |
| } | |
| static void _update_crontab_with_disabled_lines(char **crontab, | |
| char *disabled_lines, | |
| char *prepend) | |
| { | |
| char **lines; | |
| int line_count = 0; | |
| bitstr_t *disabled; | |
| char *new_crontab = NULL; | |
| if (!*crontab || !disabled_lines || disabled_lines[0] == '\0') | |
| return; | |
| lines = convert_file_to_line_array(*crontab, &line_count); | |
| disabled = bit_alloc(line_count); | |
| bit_unfmt(disabled, disabled_lines); | |
| for (int i = 1; lines[i]; i++) { | |
| if (bit_test(disabled, i)) | |
| xstrfmtcat(new_crontab, "%s%s\n", prepend, lines[i]); | |
| else | |
| xstrfmtcat(new_crontab, "%s\n", lines[i]); | |
| } | |
| xfree(*crontab); | |
| *crontab = new_crontab; | |
| FREE_NULL_BITMAP(disabled); | |
| xfree(lines); | |
| } | |
| static void _reset_options(void) | |
| { | |
| slurm_reset_all_options(&opt, true); | |
| /* cli_filter plugins can change the defaults */ | |
| if (cli_filter_g_setup_defaults(&opt, false)) { | |
| error("cli_filter plugin terminated with error"); | |
| exit(1); | |
| } | |
| opt.job_flags |= CRON_JOB; | |
| } | |
| static char *_job_script_header(void) | |
| { | |
| return xstrdup("#!/bin/sh\n" | |
| "# This job was submitted through scrontab\n"); | |
| } | |
| static char *_read_fd(int fd) | |
| { | |
| char *buf, *ptr; | |
| int buf_size = 4096; | |
| size_t buf_left; | |
| ssize_t script_size = 0, tmp_size; | |
| buf = ptr = xmalloc(buf_size); | |
| buf_left = buf_size; | |
| while ((tmp_size = read(fd, ptr, buf_left)) > 0) { | |
| buf_left -= tmp_size; | |
| script_size += tmp_size; | |
| if (buf_left == 0) { | |
| buf_size += BUFSIZ; | |
| xrealloc(buf, buf_size); | |
| } | |
| ptr = buf + script_size; | |
| buf_left = buf_size - script_size; | |
| } | |
| return buf; | |
| } | |
| static char *_load_script_from_fd(int fd) | |
| { | |
| if (lseek(fd, 0, SEEK_SET) < 0) | |
| fatal("%s: lseek(0): %m", __func__); | |
| return _read_fd(fd); | |
| } | |
| static char *_tmp_path(void) | |
| { | |
| char *tmpdir = getenv("TMPDIR"); | |
| return tmpdir ? tmpdir : "/tmp"; | |
| } | |
| /* | |
| * Replace crontab with an edited version after running an editor. | |
| */ | |
| static void _edit_crontab(char **crontab) | |
| { | |
| int fd, wstatus, err; | |
| pid_t pid; | |
| char *editor, *filename = NULL; | |
| if (!*crontab) | |
| static_ref_to_cstring(*crontab, default_crontab_txt); | |
| xstrfmtcat(filename, "%s/scrontab-XXXXXX", _tmp_path()); | |
| /* protect against weak file permissions in old glibc */ | |
| umask(0077); | |
| fd = mkstemp(filename); | |
| if (fd < 0) | |
| fatal("error creating temp crontab file '%s': %m", filename); | |
| safe_write(fd, *crontab, strlen(*crontab)); | |
| close(fd); | |
| xfree(*crontab); | |
| if (!(editor = getenv("VISUAL")) || (editor[0] == '\0')) | |
| if (!(editor = getenv("EDITOR")) || (editor[0] == '\0')) | |
| editor = "vi"; | |
| if ((pid = fork()) == -1) { | |
| unlink(filename); | |
| xfree(filename); | |
| fatal("cannot fork"); | |
| } | |
| if (!pid) { | |
| /* child */ | |
| char *argv[3]; | |
| argv[0] = editor; | |
| argv[1] = filename; | |
| argv[2] = NULL; | |
| execvp(editor, argv); | |
| exit(127); | |
| } | |
| waitpid(pid, &wstatus, 0); | |
| if (wstatus) { | |
| unlink(filename); | |
| xfree(filename); | |
| fatal("editor returned non-zero exit code"); | |
| } | |
| fd = open(filename, O_RDONLY); | |
| if (fd < 0) { | |
| err = errno; | |
| unlink(filename); | |
| fatal("error reopening temp crontab file '%s': %s", | |
| filename, strerror(err)); | |
| } | |
| *crontab = _load_script_from_fd(fd); | |
| close(fd); | |
| unlink(filename); | |
| xfree(filename); | |
| return; | |
| rwfail: | |
| err = errno; | |
| close(fd); | |
| unlink(filename); | |
| fatal("error writing to temp crontab file '%s': %s", | |
| filename, strerror(err)); | |
| } | |
| static job_desc_msg_t *_entry_to_job(cron_entry_t *entry, char *script) | |
| { | |
| job_desc_msg_t *job = slurm_opt_create_job_desc(&opt, true); | |
| /* | |
| * Nothing is consumed before this point, so the caller still owns the | |
| * entry and the script when the description cannot be built. | |
| */ | |
| if (!job) | |
| return NULL; | |
| /* slurm_opt_create_job_desc() leaves the environment to its callers */ | |
| if (!job->environment) | |
| job->environment = env_array_create(); | |
| /* What the job's batch script inherits from the runtime */ | |
| if (runtime_g_cli_apply_env(opt.runtime_idx, opt.het_job_inx, NO_VAL, | |
| RUNTIME_CTXT_SCRONTAB, true, | |
| &job->environment)) { | |
| error("Unable to export the runtime's environment"); | |
| slurm_free_job_desc_msg(job); | |
| return NULL; | |
| } | |
| /* | |
| * slurm_opt_create_job_desc() leaves the standard streams to its | |
| * callers, since each of them names the options differently. | |
| */ | |
| job->std_err = xstrdup(opt.efname); | |
| job->std_in = xstrdup(opt.ifname); | |
| job->std_out = xstrdup(opt.ofname); | |
| job->crontab_entry = entry; | |
| /* finish building the batch script */ | |
| xstrfmtcat(script, "# crontab time request was: '%s'\n%s\n", | |
| entry->cronspec, entry->command); | |
| job->script = script; | |
| set_prio_process_env(); | |
| env_array_overwrite(&job->environment, "SLURM_PRIO_PROCESS", | |
| getenv("SLURM_PRIO_PROCESS")); | |
| if (xstrcasecmp(opt.export_env, "NIL")) | |
| env_array_overwrite(&job->environment, "SLURM_GET_USER_ENV", | |
| "1"); | |
| job->env_size = envcount(job->environment); | |
| if (!job->name) { | |
| char *pos; | |
| job->name = xstrdup(entry->command); | |
| if ((pos = xstrstr(job->name, " "))) | |
| *pos = '\0'; | |
| } | |
| if (!job->work_dir) { | |
| if (!(job->work_dir = uid_to_dir(uid))) | |
| fatal("uid_to_dir(%u) failed", uid); | |
| } | |
| return job; | |
| } | |
| static int _list_find_key(void *x, void *key) | |
| { | |
| config_key_pair_t *key_pair = x; | |
| char *str = key; | |
| if (!xstrcmp(key_pair->name, str)) | |
| return 1; | |
| return 0; | |
| } | |
| static int _foreach_env_var_expand(void *x, void *arg) | |
| { | |
| char *key = NULL; | |
| config_key_pair_t *key_pair = x; | |
| cron_entry_t *entry = arg; | |
| xstrfmtcat(key, "$%s", key_pair->name); | |
| xstrsubstitute(entry->command, key, key_pair->value); | |
| xfree(key); | |
| return SLURM_SUCCESS; | |
| } | |
| static void _expand_variables(cron_entry_t *entry, list_t *env_vars) | |
| { | |
| if (!env_vars || !list_count(env_vars)) | |
| return; | |
| list_for_each(env_vars, _foreach_env_var_expand, entry); | |
| } | |
| static void _edit_and_update_crontab(char *crontab) | |
| { | |
| char **lines; | |
| char *badline = NULL; | |
| int lineno, line_start; | |
| char *line; | |
| list_t *jobs, *env_vars; | |
| int line_count; | |
| bool setup_next_entry = true; | |
| char *script; | |
| crontab_update_response_msg_t *response; | |
| edit: | |
| if (edit_only && crontab) { | |
| slurm_hash_t before = { .type = HASH_PLUGIN_K12 }; | |
| slurm_hash_t after = { .type = HASH_PLUGIN_K12 }; | |
| int before_len, after_len; | |
| before_len = hash_g_compute(crontab, strlen(crontab), NULL, 0, | |
| &before); | |
| _edit_crontab(&crontab); | |
| after_len = hash_g_compute(crontab, strlen(crontab), NULL, 0, | |
| &after); | |
| if ((before_len == after_len) && | |
| (!memcmp(before.hash, after.hash, before_len))) { | |
| info("No modification made"); | |
| xfree(crontab); | |
| return; | |
| } | |
| } else if (edit_only) { | |
| _edit_crontab(&crontab); | |
| } | |
| jobs = list_create((ListDelF) slurm_free_job_desc_msg); | |
| env_vars = list_create(destroy_config_key_pair); | |
| lines = convert_file_to_line_array(xstrdup(crontab), &line_count); | |
| lineno = 0; | |
| line_start = -1; | |
| setup_next_entry = true; | |
| while ((line = lines[lineno])) { | |
| char *pos = line; | |
| cron_entry_t *entry; | |
| job_desc_msg_t *job = NULL; | |
| if (setup_next_entry) { | |
| _reset_options(); | |
| script = _job_script_header(); | |
| setup_next_entry = false; | |
| } | |
| /* advance to the first non-whitespace character */ | |
| while (*pos == ' ' || *pos == '\t') | |
| pos++; | |
| if (*pos == '\0' || *pos == '\n') { | |
| /* line was only whitespace */ | |
| lineno++; | |
| continue; | |
| } else if (!xstrncmp(pos, "#SCRON", 6)) { | |
| xstrfmtcat(script, "%s\n", line); | |
| if (line_start == -1) | |
| line_start = lineno; | |
| if (parse_scron_line(line + 6, lineno)) { | |
| badline = xstrdup_printf("%d", lineno); | |
| break; | |
| } | |
| lineno++; | |
| continue; | |
| } else if (*pos == '#') { | |
| /* boring comment line */ | |
| lineno++; | |
| continue; | |
| } else { | |
| /* check env setting. */ | |
| char *name = NULL, *value = NULL; | |
| if (load_env(pos, &name, &value)) { | |
| config_key_pair_t *key_pair = | |
| xmalloc(sizeof(*key_pair)); | |
| xassert(name); | |
| xassert(value); | |
| key_pair->name = name; | |
| key_pair->value = value; | |
| list_delete_all(env_vars, _list_find_key, name); | |
| list_append(env_vars, key_pair); | |
| lineno++; | |
| continue; | |
| } | |
| } | |
| if (!(entry = cronspec_to_bitstring(pos))) { | |
| badline = xstrdup_printf("%d", lineno); | |
| break; | |
| } | |
| _expand_variables(entry, env_vars); | |
| if (cli_filter_g_pre_submit(&opt, 0)) { | |
| free_cron_entry(entry); | |
| badline = xstrdup_printf("%d-%d", line_start, lineno); | |
| printf("cli_filter plugin terminated with error\n"); | |
| break; | |
| } | |
| /* | |
| * track lines associated with this job submission, starting | |
| * starting at the first SCRON directive and completing here | |
| */ | |
| if (line_start != -1) | |
| entry->line_start = line_start; | |
| else | |
| entry->line_start = lineno; | |
| line_start = -1; | |
| entry->line_end = lineno; | |
| if (!(job = _entry_to_job(entry, script))) { | |
| badline = xstrdup_printf("%d-%d", entry->line_start, | |
| lineno); | |
| free_cron_entry(entry); | |
| xfree(script); | |
| break; | |
| } | |
| list_append(jobs, job); | |
| script = NULL; | |
| setup_next_entry = true; | |
| lineno++; | |
| } | |
| xfree(*(lines + 1)); | |
| xfree(lines); | |
| xfree(script); | |
| FREE_NULL_LIST(env_vars); | |
| if (badline) { | |
| if (first_form) { | |
| printf("There are errors in your crontab.\n"); | |
| xfree(badline); | |
| xfree(crontab); | |
| FREE_NULL_LIST(jobs); | |
| exit(1); | |
| } | |
| char c = '\0'; | |
| FREE_NULL_LIST(jobs); | |
| while (tolower(c) != 'y' && tolower(c) != 'n') { | |
| printf("There are errors in your crontab.\n" | |
| "The failed line(s) is commented out with #BAD:\n" | |
| "Do you want to retry the edit? (y/n) "); | |
| c = (char) getchar(); | |
| } | |
| if (c == 'n') | |
| exit(0); | |
| _update_crontab_with_disabled_lines(&crontab, badline, | |
| "#BAD: "); | |
| xfree(badline); | |
| goto edit; | |
| } | |
| response = slurm_update_crontab(uid, gid, crontab, jobs); | |
| if (response->return_code) { | |
| if (first_form) { | |
| printf("There was an issue with the job submission on lines %s\n" | |
| "The error code return was: %s\n" | |
| "The error message was: %s\n", | |
| response->failed_lines, | |
| slurm_strerror(response->return_code), | |
| response->err_msg); | |
| slurm_free_crontab_update_response_msg(response); | |
| xfree(crontab); | |
| FREE_NULL_LIST(jobs); | |
| exit(1); | |
| } | |
| char c = '\0'; | |
| FREE_NULL_LIST(jobs); | |
| while (tolower(c) != 'y' && tolower(c) != 'n') { | |
| printf("There was an issue with the job submission on lines %s\n" | |
| "The error code return was: %s\n" | |
| "The error message was: %s\n" | |
| "The failed lines are commented out with #BAD:\n" | |
| "Do you want to retry the edit? (y/n) ", | |
| response->failed_lines, | |
| slurm_strerror(response->return_code), | |
| response->err_msg); | |
| c = (char) getchar(); | |
| } | |
| if (c == 'n') | |
| exit(0); | |
| _update_crontab_with_disabled_lines(&crontab, | |
| response->failed_lines, | |
| "#BAD: "); | |
| slurm_free_crontab_update_response_msg(response); | |
| goto edit; | |
| } | |
| if (response->job_submit_user_msg) | |
| print_multi_line_string(response->job_submit_user_msg, -1, | |
| LOG_LEVEL_INFO); | |
| for (int i = 0; i < response->jobids_count; i++) | |
| cli_filter_g_post_submit(0, response->jobids[i], NO_VAL); | |
| slurm_free_crontab_update_response_msg(response); | |
| xfree(crontab); | |
| FREE_NULL_LIST(jobs); | |
| } | |
| static void _handle_first_form(char **new_crontab) | |
| { | |
| int input_desc; | |
| if (!infile) | |
| fatal("invalid input file"); | |
| if (!xstrcmp(infile, "-")) { | |
| input_desc = STDIN_FILENO; | |
| *new_crontab = _read_fd(input_desc); | |
| } else { | |
| if ((input_desc = open(infile, O_RDONLY)) < 0) | |
| fatal("failed to open %s", infile); | |
| *new_crontab = _read_fd(input_desc); | |
| if (close(input_desc)) | |
| fatal("failed to close fd %d", input_desc); | |
| } | |
| } | |
| extern int main(int argc, char **argv) | |
| { | |
| int rc; | |
| char *crontab = NULL, *disabled_lines = NULL; | |
| slurm_init(NULL); | |
| _parse_args(argc, argv); | |
| if (cli_filter_init() != SLURM_SUCCESS) | |
| fatal("failed to initialize cli_filter plugin"); | |
| if (!xstrcasestr(slurm_conf.scron_params, "enable")) | |
| fatal("scrontab is disabled on this cluster"); | |
| if (first_form) | |
| _handle_first_form(&crontab); | |
| if (remove_only) { | |
| if ((rc = slurm_remove_crontab(uid, gid))) | |
| fatal("slurm_remove_crontab failed: %s", | |
| slurm_strerror(rc)); | |
| exit(0); | |
| } | |
| if (edit_only || list_only) { | |
| if ((rc = slurm_request_crontab(uid, &crontab, | |
| &disabled_lines)) && | |
| (rc != ESLURM_JOB_SCRIPT_MISSING)) { | |
| fatal("slurm_request_crontab failed: %s", | |
| slurm_strerror(rc)); | |
| } | |
| _update_crontab_with_disabled_lines(&crontab, disabled_lines, | |
| "#DISABLED: "); | |
| xfree(disabled_lines); | |
| } | |
| if (list_only) { | |
| if (!crontab) { | |
| printf("no crontab for %s\n", uid_to_string(uid)); | |
| exit(1); | |
| } | |
| printf("%s", crontab); | |
| xfree(crontab); | |
| exit(0); | |
| } | |
| /* needed otherwise slurm_option_table_create() always returns NULL */ | |
| if (spank_init_allocator()) | |
| fatal("failed to initialize plugin stack"); | |
| /* Resolve the runtime first so its options are in the option table */ | |
| if (runtime_g_init(NULL, RUNTIME_CTXT_SCRONTAB, NULL)) | |
| fatal("failed to initialize runtime plugin"); | |
| _edit_and_update_crontab(crontab); | |
| cli_filter_fini(); | |
| runtime_g_fini(); | |
| spank_fini(NULL); | |
| return 0; | |
| } |