|  | /*****************************************************************************\ | 
|  | *  cron.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 <unistd.h> | 
|  |  | 
|  | #include "src/common/bitstring.h" | 
|  | #include "src/common/cron.h" | 
|  | #include "src/common/log.h" | 
|  | #include "src/common/pack.h" | 
|  | #include "src/common/read_config.h" | 
|  | #include "src/common/slurm_time.h" | 
|  | #include "src/common/uid.h" | 
|  | #include "src/common/xmalloc.h" | 
|  | #include "src/common/xstring.h" | 
|  |  | 
|  | #define LEAP_YEAR(y) ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) | 
|  |  | 
|  | extern cron_entry_t *new_cron_entry(void) | 
|  | { | 
|  | cron_entry_t *entry = xmalloc(sizeof(*entry)); | 
|  |  | 
|  | entry->minute = bit_alloc(61); | 
|  | entry->hour = bit_alloc(25); | 
|  | entry->day_of_month = bit_alloc(32); | 
|  | entry->month = bit_alloc(13); | 
|  | entry->day_of_week = bit_alloc(8); | 
|  |  | 
|  | return entry; | 
|  | } | 
|  |  | 
|  | extern void free_cron_entry(void *in) | 
|  | { | 
|  | cron_entry_t *entry = (cron_entry_t *) in; | 
|  |  | 
|  | if (!entry) | 
|  | return; | 
|  |  | 
|  | xfree(entry->minute); | 
|  | xfree(entry->hour); | 
|  | xfree(entry->day_of_month); | 
|  | xfree(entry->month); | 
|  | xfree(entry->day_of_week); | 
|  | xfree(entry->cronspec); | 
|  | xfree(entry->command); | 
|  | xfree(entry); | 
|  | } | 
|  |  | 
|  | extern bool valid_cron_entry(cron_entry_t *e) | 
|  | { | 
|  | int first_day_of_month; | 
|  |  | 
|  | /* basic structure check */ | 
|  | if ((bit_size(e->minute) != 61) || | 
|  | (bit_size(e->hour) != 25) || | 
|  | (bit_size(e->day_of_month) != 32) || | 
|  | (bit_size(e->month) != 13) || | 
|  | (bit_size(e->day_of_week) != 8)) | 
|  | return false; | 
|  |  | 
|  | /* | 
|  | * Clear top or lower bits (may have been set for wildcard processing). | 
|  | */ | 
|  | bit_clear(e->minute, 60); | 
|  | bit_clear(e->hour, 24); | 
|  | bit_clear(e->day_of_month, 0); | 
|  | bit_clear(e->month, 0); | 
|  | bit_clear(e->day_of_week, 7); | 
|  |  | 
|  | /* | 
|  | * Missing some e. Need at least one bit set in each field or | 
|  | * the wildcard flag, otherwise calc_next_cron_start() will break. | 
|  | */ | 
|  | first_day_of_month = bit_ffs(e->day_of_month); | 
|  | if ((!(e->flags & CRON_WILD_MINUTE) && (bit_ffs(e->minute) == -1)) || | 
|  | (!(e->flags & CRON_WILD_HOUR) && (bit_ffs(e->hour) == -1)) || | 
|  | (!(e->flags & CRON_WILD_DOM) && (first_day_of_month == -1)) || | 
|  | (!(e->flags & CRON_WILD_MONTH) && (bit_ffs(e->month) == -1)) || | 
|  | (!(e->flags & CRON_WILD_DOW) && (bit_ffs(e->day_of_week) == -1))) | 
|  | return false; | 
|  |  | 
|  | /* | 
|  | * Make sure the crontab isn't requesting a non-existent | 
|  | * combination of month and day. | 
|  | * | 
|  | * Note: we do allow you to schedule something to only run | 
|  | * on leap days, as crazy as that may seem. | 
|  | */ | 
|  | if (e->flags & CRON_WILD_DOM) { | 
|  | ; | 
|  | } else if (first_day_of_month == 31) { | 
|  | if (!bit_test(e->month, 1) && !bit_test(e->month, 3) && | 
|  | !bit_test(e->month, 5) && !bit_test(e->month, 7) && | 
|  | !bit_test(e->month, 8) && !bit_test(e->month, 10) && | 
|  | !bit_test(e->month, 12)) | 
|  | return false; | 
|  | } else if (first_day_of_month == 30) { | 
|  | /* Make sure the only month available isn't February. */ | 
|  | if ((bit_fls(e->month) == 2) && (bit_ffs(e->month) == 2)) | 
|  | return false; | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | extern char *cronspec_from_cron_entry(cron_entry_t *entry) | 
|  | { | 
|  | char *cronspec = NULL; | 
|  | char *fmt; | 
|  |  | 
|  | if (entry->flags & CRON_WILD_MINUTE) { | 
|  | xstrcat(cronspec, "* "); | 
|  | } else { | 
|  | fmt = bit_fmt_full(entry->minute); | 
|  | xstrfmtcat(cronspec, "%s ", fmt); | 
|  | xfree(fmt); | 
|  | } | 
|  |  | 
|  | if (entry->flags & CRON_WILD_HOUR) { | 
|  | xstrcat(cronspec, "* "); | 
|  | } else { | 
|  | fmt = bit_fmt_full(entry->hour); | 
|  | xstrfmtcat(cronspec, "%s ", fmt); | 
|  | xfree(fmt); | 
|  | } | 
|  |  | 
|  | if (entry->flags & CRON_WILD_DOM) { | 
|  | xstrcat(cronspec, "* "); | 
|  | } else { | 
|  | fmt = bit_fmt_full(entry->day_of_month); | 
|  | xstrfmtcat(cronspec, "%s ", fmt); | 
|  | xfree(fmt); | 
|  | } | 
|  |  | 
|  | if (entry->flags & CRON_WILD_MONTH) { | 
|  | xstrcat(cronspec, "* "); | 
|  | } else { | 
|  | fmt = bit_fmt_full(entry->month); | 
|  | xstrfmtcat(cronspec, "%s ", fmt); | 
|  | xfree(fmt); | 
|  | } | 
|  |  | 
|  | if (entry->flags & CRON_WILD_DOW) { | 
|  | xstrcat(cronspec, "*"); | 
|  | } else { | 
|  | fmt = bit_fmt_full(entry->day_of_week); | 
|  | xstrfmtcat(cronspec, "%s", fmt); | 
|  | xfree(fmt); | 
|  | } | 
|  |  | 
|  | return cronspec; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Determine how many months there are between now and the next month this job | 
|  | * could run in. | 
|  | * | 
|  | * One important note: struct tm has jan == 0, but the crontab | 
|  | * format and our bitstring have jan == 1. | 
|  | */ | 
|  | static int _next_month(cron_entry_t *entry, struct tm *tm) | 
|  | { | 
|  | int months_to_advance = 0; | 
|  |  | 
|  | /* tm_mon should be 0-11 */ | 
|  | xassert(tm->tm_mon >= 0); | 
|  | xassert(tm->tm_mon <= 11); | 
|  |  | 
|  | /* month is current valid, nice and easy, no major adjustments needed */ | 
|  | if (entry->flags & CRON_WILD_MONTH || | 
|  | bit_test(entry->month, tm->tm_mon + 1)) | 
|  | return 0; | 
|  |  | 
|  | /* Start testing from now to get the closest month */ | 
|  | for (int i = tm->tm_mon; i < 12; i++) { | 
|  | if (bit_test(entry->month, i + 1)) | 
|  | goto found; | 
|  | months_to_advance++; | 
|  | } | 
|  |  | 
|  | /* Loop around to beginning of the year if needed */ | 
|  | for (int i = 0; i < tm->tm_mon; i++) { | 
|  | if (bit_test(entry->month, i + 1)) | 
|  | goto found; | 
|  | months_to_advance++; | 
|  | } | 
|  |  | 
|  | fatal("Could not find a valid month, this should be impossible"); | 
|  |  | 
|  | found: | 
|  | /* | 
|  | * Next usable month is not this month. Reset other timing to midnight | 
|  | * on the first of the next valid month. | 
|  | */ | 
|  | tm->tm_mon += months_to_advance; | 
|  | tm->tm_hour = 0; | 
|  | tm->tm_min = 0; | 
|  | tm->tm_mday = 1; | 
|  | slurm_mktime(tm); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Determine how many days there are between now and the next day of the week | 
|  | * this job could run on. | 
|  | * | 
|  | * Intended for use when day of week is specified in the cron entry. | 
|  | */ | 
|  | static int _next_day_of_week(cron_entry_t *entry, struct tm *tm) | 
|  | { | 
|  | int days_to_advance = 0; | 
|  |  | 
|  | /* tm_wday should be 0-6 */ | 
|  | xassert(tm->tm_wday >= 0); | 
|  | xassert(tm->tm_wday <= 6); | 
|  |  | 
|  | /* Start testing from now to get the closest day */ | 
|  | for (int i = tm->tm_wday; i < 7; i++) { | 
|  | if (bit_test(entry->day_of_week, i)) | 
|  | return days_to_advance; | 
|  | days_to_advance++; | 
|  | } | 
|  |  | 
|  | /* Loop around to beginning of the week if needed */ | 
|  | for (int i = 0; i < tm->tm_wday; i++) { | 
|  | if (bit_test(entry->day_of_week, i)) | 
|  | return days_to_advance; | 
|  | days_to_advance++; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | /* Return number of days in a given tm->tm_mon */ | 
|  | static int _days_in_month(struct tm *tm) | 
|  | { | 
|  | /* Default to maximum days in month (highest likelihood) */ | 
|  | int days_in_month = 31; | 
|  |  | 
|  | /* tm_mon should be 0-11 */ | 
|  | xassert(tm->tm_mon >= 0); | 
|  | xassert(tm->tm_mon <= 11); | 
|  |  | 
|  | switch (tm->tm_mon) { | 
|  | case 1: | 
|  | days_in_month = LEAP_YEAR(tm->tm_year) ? 29 : 28; | 
|  | break; | 
|  | case 3: | 
|  | case 5: | 
|  | case 8: | 
|  | case 10: | 
|  | days_in_month = 30; | 
|  | break; | 
|  | } | 
|  |  | 
|  | return days_in_month; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Determine how many days there are between now and the next day of the month | 
|  | * this job could run on. | 
|  | * | 
|  | * Intended for use when day of month is specified in the cron entry. | 
|  | */ | 
|  | static int _next_day_of_month(cron_entry_t *entry, struct tm *tm) | 
|  | { | 
|  | int days_to_advance = 0; | 
|  | int days_in_month; | 
|  |  | 
|  | /* tm_mday should be 1-31 */ | 
|  | xassert(tm->tm_mday >= 1); | 
|  | xassert(tm->tm_mday <= 31); | 
|  |  | 
|  | days_in_month = _days_in_month(tm); | 
|  |  | 
|  | /* | 
|  | * Advance days within tm month till cron entry day found (and return | 
|  | * count) or reach days in tm month. | 
|  | */ | 
|  | for (int i = tm->tm_mday; i <= days_in_month; i++) { | 
|  | if (bit_test(entry->day_of_month, i)) | 
|  | return days_to_advance; | 
|  | days_to_advance++; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Continue advancing days within next month till tm_mday found and return | 
|  | * aggregated count of advanced days. | 
|  | */ | 
|  | for (int i = 1; i < tm->tm_mday; i++) { | 
|  | if (bit_test(entry->day_of_month, i)) | 
|  | return days_to_advance; | 
|  | days_to_advance++; | 
|  | } | 
|  |  | 
|  | return days_to_advance; | 
|  | } | 
|  |  | 
|  | extern time_t calc_next_cron_start(cron_entry_t *entry, time_t next) | 
|  | { | 
|  | struct tm tm; | 
|  | time_t now = time(NULL); | 
|  | int validated_month, days_to_add; | 
|  |  | 
|  | /* | 
|  | * Avoid running twice in the same minute. | 
|  | */ | 
|  | if (next && next > now + 60) { | 
|  | now = next; | 
|  | localtime_r(&now, &tm); | 
|  | tm.tm_sec = 0; | 
|  | } else { | 
|  | localtime_r(&now, &tm); | 
|  | tm.tm_sec = 0; | 
|  | tm.tm_min++; | 
|  | } | 
|  |  | 
|  | month: | 
|  | _next_month(entry, &tm); | 
|  |  | 
|  | validated_month = tm.tm_mon; | 
|  |  | 
|  | days_to_add = 0; | 
|  | if ((entry->flags & CRON_WILD_DOM) && (entry->flags & CRON_WILD_DOW)) { | 
|  | /* Wildcard for both is the easy path out. */ | 
|  | ; | 
|  | } else if (entry->flags & CRON_WILD_DOM) { | 
|  | /* | 
|  | * Only pay attention to the day of week. | 
|  | */ | 
|  | days_to_add = _next_day_of_week(entry, &tm); | 
|  | } else if (entry->flags & CRON_WILD_DOW) { | 
|  | /* | 
|  | * Only attention to the day of month. | 
|  | */ | 
|  | days_to_add = _next_day_of_month(entry, &tm); | 
|  | } else { | 
|  | /* | 
|  | * When both are specified, the defacto behavior is to | 
|  | * treat them as OR'd rather than AND'd, as trying to | 
|  | * resolve both simultaneously would result in the job | 
|  | * very rarely running. | 
|  | * So find the soonest time between them and use that. | 
|  | */ | 
|  | int dom_next = _next_day_of_month(entry, &tm); | 
|  | int dow_next = _next_day_of_week(entry, &tm); | 
|  |  | 
|  | days_to_add = MIN(dom_next, dow_next); | 
|  | } | 
|  | if (days_to_add) { | 
|  | tm.tm_mday += days_to_add; | 
|  | tm.tm_hour = 0; | 
|  | tm.tm_min = 0; | 
|  | slurm_mktime(&tm); | 
|  |  | 
|  | /* month slipped back, need to re-validate */ | 
|  | if (validated_month != tm.tm_mon) | 
|  | goto month; | 
|  | } | 
|  |  | 
|  | hour: | 
|  | if (!(entry->flags & CRON_WILD_HOUR) && | 
|  | !bit_test(entry->hour, tm.tm_hour)) { | 
|  | /* must be in future, reset minutes */ | 
|  | tm.tm_min = 0; | 
|  |  | 
|  | while (tm.tm_hour < 24) { | 
|  | if (bit_test(entry->hour, tm.tm_hour)) | 
|  | break; | 
|  | tm.tm_hour++; | 
|  | } | 
|  | if (tm.tm_hour == 24) { | 
|  | /* | 
|  | * tm_hour set to 24 rolls the day and possibly | 
|  | * the month at well. revalidate month + day. | 
|  | */ | 
|  | slurm_mktime(&tm); | 
|  | goto month; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (!(entry->flags & CRON_WILD_MINUTE) && | 
|  | !bit_test(entry->minute, tm.tm_min)) { | 
|  | while (tm.tm_min < 60) { | 
|  | if (bit_test(entry->minute, tm.tm_min)) | 
|  | break; | 
|  | tm.tm_min++; | 
|  | } | 
|  | if (tm.tm_min == 60 && tm.tm_hour == 23) { | 
|  | /* | 
|  | * this will roll into the next day, | 
|  | * which may also be a new month | 
|  | */ | 
|  | slurm_mktime(&tm); | 
|  | goto month; | 
|  | } else if (tm.tm_min == 60) { | 
|  | /* | 
|  | * next hour, but fortunately still | 
|  | * in the same day | 
|  | */ | 
|  | tm.tm_min = 0; | 
|  | tm.tm_hour++; | 
|  | goto hour; | 
|  | } | 
|  | } | 
|  |  | 
|  | return slurm_mktime(&tm); | 
|  | } | 
|  |  | 
|  | extern void pack_cron_entry(void *in, uint16_t protocol_version, | 
|  | buf_t *buffer) | 
|  | { | 
|  | uint8_t set = (in ? 1 : 0); | 
|  | cron_entry_t *entry = (cron_entry_t *) in; | 
|  |  | 
|  | pack8(set, buffer); | 
|  |  | 
|  | if (!set) | 
|  | return; | 
|  |  | 
|  | if (protocol_version >= SLURM_MIN_PROTOCOL_VERSION) { | 
|  | pack32(entry->flags, buffer); | 
|  | pack_bit_str_hex(entry->minute, buffer); | 
|  | pack_bit_str_hex(entry->hour, buffer); | 
|  | pack_bit_str_hex(entry->day_of_month, buffer); | 
|  | pack_bit_str_hex(entry->month, buffer); | 
|  | pack_bit_str_hex(entry->day_of_week, buffer); | 
|  | packstr(entry->cronspec, buffer); | 
|  | /* command is not packed, only in struct for parsing */ | 
|  | pack32(entry->line_start, buffer); | 
|  | pack32(entry->line_end, buffer); | 
|  | } | 
|  | } | 
|  |  | 
|  | extern int unpack_cron_entry(void **entry_ptr, uint16_t protocol_version, | 
|  | buf_t *buffer) | 
|  | { | 
|  | uint8_t set; | 
|  | cron_entry_t *entry = NULL; | 
|  |  | 
|  | xassert(entry_ptr); | 
|  |  | 
|  | safe_unpack8(&set, buffer); | 
|  |  | 
|  | if (!set) | 
|  | return SLURM_SUCCESS; | 
|  |  | 
|  | entry = xmalloc(sizeof(*entry)); | 
|  | *entry_ptr = entry; | 
|  |  | 
|  | if (protocol_version >= SLURM_MIN_PROTOCOL_VERSION) { | 
|  | safe_unpack32(&entry->flags, buffer); | 
|  | unpack_bit_str_hex(&entry->minute, buffer); | 
|  | unpack_bit_str_hex(&entry->hour, buffer); | 
|  | unpack_bit_str_hex(&entry->day_of_month, buffer); | 
|  | unpack_bit_str_hex(&entry->month, buffer); | 
|  | unpack_bit_str_hex(&entry->day_of_week, buffer); | 
|  | safe_unpackstr(&entry->cronspec, buffer); | 
|  | /* command is not packed, only in struct for parsing */ | 
|  | safe_unpack32(&entry->line_start, buffer); | 
|  | safe_unpack32(&entry->line_end, buffer); | 
|  | } else { | 
|  | goto unpack_error; | 
|  | } | 
|  |  | 
|  | return SLURM_SUCCESS; | 
|  |  | 
|  | unpack_error: | 
|  | *entry_ptr = NULL; | 
|  | free_cron_entry(entry); | 
|  | return SLURM_ERROR; | 
|  | } |