blob: b707c787595179e6e912a8745ae375e1c0300c3f [file] [log] [blame]
/*****************************************************************************\
* 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/interfaces/cli_filter.h"
#include "src/interfaces/hash.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/ref.h"
#include "src/common/read_config.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 "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 = xmalloc(sizeof(*job));
slurm_init_job_desc_msg(job);
fill_job_desc_from_opts(job);
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;
job->environment = env_array_create();
set_prio_process_env();
env_array_overwrite(&job->environment, "SLURM_PRIO_PROCESS",
getenv("SLURM_PRIO_PROCESS"));
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;
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;
list_append(jobs, _entry_to_job(entry, script));
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");
_edit_and_update_crontab(crontab);
cli_filter_fini();
spank_fini(NULL);
return 0;
}