diff --git a/include/crm/common/iso8601_internal.h b/include/crm/common/iso8601_internal.h index f924d8a076..ca1fd9eaab 100644 --- a/include/crm/common/iso8601_internal.h +++ b/include/crm/common/iso8601_internal.h @@ -1,42 +1,57 @@ /* - * Copyright 2015-2022 the Pacemaker project contributors + * Copyright 2015-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__ISO8601_INTERNAL__H # define PCMK__ISO8601_INTERNAL__H #include #include #include #include typedef struct pcmk__time_us pcmk__time_hr_t; pcmk__time_hr_t *pcmk__time_hr_convert(pcmk__time_hr_t *target, const crm_time_t *dt); void pcmk__time_set_hr_dt(crm_time_t *target, const pcmk__time_hr_t *hr_dt); pcmk__time_hr_t *pcmk__time_hr_now(time_t *epoch); pcmk__time_hr_t *pcmk__time_hr_new(const char *date_time); void pcmk__time_hr_free(pcmk__time_hr_t *hr_dt); char *pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt); char *pcmk__epoch2str(const time_t *source, uint32_t flags); char *pcmk__timespec2str(const struct timespec *ts, uint32_t flags); const char *pcmk__readable_interval(guint interval_ms); crm_time_t *pcmk__copy_timet(time_t source); +// For use with pcmk__add_time_from_xml() +enum pcmk__time_component { + pcmk__time_unknown, + pcmk__time_years, + pcmk__time_months, + pcmk__time_weeks, + pcmk__time_days, + pcmk__time_hours, + pcmk__time_minutes, + pcmk__time_seconds, +}; + +int pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component, + const xmlNode *xml); + struct pcmk__time_us { int years; int months; /* Only for durations */ int days; int seconds; int offset; /* Seconds */ bool duration; int useconds; }; #endif diff --git a/lib/common/iso8601.c b/lib/common/iso8601.c index 25db6cbb01..1113bdb69e 100644 --- a/lib/common/iso8601.c +++ b/lib/common/iso8601.c @@ -1,1974 +1,2055 @@ /* * Copyright 2005-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ /* * References: * https://en.wikipedia.org/wiki/ISO_8601 * http://www.staff.science.uu.nl/~gent0113/calendar/isocalendar.htm */ #include #include #include #include #include #include #include #include /* * Andrew's code was originally written for OSes whose "struct tm" contains: * long tm_gmtoff; :: Seconds east of UTC * const char *tm_zone; :: Timezone abbreviation * Some OSes lack these, instead having: * time_t (or long) timezone; :: "difference between UTC and local standard time" * char *tzname[2] = { "...", "..." }; * I (David Lee) confess to not understanding the details. So my attempted * generalisations for where their use is necessary may be flawed. * * 1. Does "difference between ..." subtract the same or opposite way? * 2. Should it use "altzone" instead of "timezone"? * 3. Should it use tzname[0] or tzname[1]? Interaction with timezone/altzone? */ #if defined(HAVE_STRUCT_TM_TM_GMTOFF) # define GMTOFF(tm) ((tm)->tm_gmtoff) #else /* Note: extern variable; macro argument not actually used. */ # define GMTOFF(tm) (-timezone+daylight) #endif #define HOUR_SECONDS (60 * 60) #define DAY_SECONDS (HOUR_SECONDS * 24) /*! * \internal * \brief Validate a seconds/microseconds tuple * * The microseconds value must be in the correct range, and if both are nonzero * they must have the same sign. * * \param[in] sec Seconds * \param[in] usec Microseconds * * \return true if the seconds/microseconds tuple is valid, or false otherwise */ #define valid_sec_usec(sec, usec) \ ((QB_ABS(usec) < QB_TIME_US_IN_SEC) \ && (((sec) == 0) || ((usec) == 0) || (((sec) < 0) == ((usec) < 0)))) // A date/time or duration struct crm_time_s { int years; // Calendar year (date/time) or number of years (duration) int months; // Number of months (duration only) int days; // Ordinal day of year (date/time) or number of days (duration) int seconds; // Seconds of day (date/time) or number of seconds (duration) int offset; // Seconds offset from UTC (date/time only) bool duration; // True if duration }; static crm_time_t *parse_date(const char *date_str); static crm_time_t * crm_get_utc_time(const crm_time_t *dt) { crm_time_t *utc = NULL; if (dt == NULL) { errno = EINVAL; return NULL; } utc = crm_time_new_undefined(); utc->years = dt->years; utc->days = dt->days; utc->seconds = dt->seconds; utc->offset = 0; if (dt->offset) { crm_time_add_seconds(utc, -dt->offset); } else { /* Durations (which are the only things that can include months, never have a timezone */ utc->months = dt->months; } crm_time_log(LOG_TRACE, "utc-source", dt, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); crm_time_log(LOG_TRACE, "utc-target", utc, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); return utc; } crm_time_t * crm_time_new(const char *date_time) { tzset(); if (date_time == NULL) { return pcmk__copy_timet(time(NULL)); } return parse_date(date_time); } /*! * \brief Allocate memory for an uninitialized time object * * \return Newly allocated time object * \note The caller is responsible for freeing the return value using * crm_time_free(). */ crm_time_t * crm_time_new_undefined(void) { crm_time_t *result = calloc(1, sizeof(crm_time_t)); CRM_ASSERT(result != NULL); return result; } /*! * \brief Check whether a time object has been initialized yet * * \param[in] t Time object to check * * \return TRUE if time object has been initialized, FALSE otherwise */ bool crm_time_is_defined(const crm_time_t *t) { // Any nonzero member indicates something has been done to t return (t != NULL) && (t->years || t->months || t->days || t->seconds || t->offset || t->duration); } void crm_time_free(crm_time_t * dt) { if (dt == NULL) { return; } free(dt); } static int year_days(int year) { int d = 365; if (crm_time_leapyear(year)) { d++; } return d; } /* From http://myweb.ecu.edu/mccartyr/ISOwdALG.txt : * * 5. Find the Jan1Weekday for Y (Monday=1, Sunday=7) * YY = (Y-1) % 100 * C = (Y-1) - YY * G = YY + YY/4 * Jan1Weekday = 1 + (((((C / 100) % 4) x 5) + G) % 7) */ int crm_time_january1_weekday(int year) { int YY = (year - 1) % 100; int C = (year - 1) - YY; int G = YY + YY / 4; int jan1 = 1 + (((((C / 100) % 4) * 5) + G) % 7); crm_trace("YY=%d, C=%d, G=%d", YY, C, G); crm_trace("January 1 %.4d: %d", year, jan1); return jan1; } int crm_time_weeks_in_year(int year) { int weeks = 52; int jan1 = crm_time_january1_weekday(year); /* if jan1 == thursday */ if (jan1 == 4) { weeks++; } else { jan1 = crm_time_january1_weekday(year + 1); /* if dec31 == thursday aka. jan1 of next year is a friday */ if (jan1 == 5) { weeks++; } } return weeks; } // Jan-Dec plus Feb of leap years static int month_days[13] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29 }; /*! * \brief Return number of days in given month of given year * * \param[in] Ordinal month (1-12) * \param[in] Gregorian year * * \return Number of days in given month (0 if given month is invalid) */ int crm_time_days_in_month(int month, int year) { if ((month < 1) || (month > 12)) { return 0; } if ((month == 2) && crm_time_leapyear(year)) { month = 13; } return month_days[month - 1]; } bool crm_time_leapyear(int year) { gboolean is_leap = FALSE; if (year % 4 == 0) { is_leap = TRUE; } if (year % 100 == 0 && year % 400 != 0) { is_leap = FALSE; } return is_leap; } static uint32_t get_ordinal_days(uint32_t y, uint32_t m, uint32_t d) { int lpc; for (lpc = 1; lpc < m; lpc++) { d += crm_time_days_in_month(lpc, y); } return d; } void crm_time_log_alias(int log_level, const char *file, const char *function, int line, const char *prefix, const crm_time_t *date_time, int flags) { char *date_s = crm_time_as_string(date_time, flags); if (log_level == LOG_STDOUT) { printf("%s%s%s\n", (prefix? prefix : ""), (prefix? ": " : ""), date_s); } else { do_crm_log_alias(log_level, file, function, line, "%s%s%s", (prefix? prefix : ""), (prefix? ": " : ""), date_s); } free(date_s); } static void crm_time_get_sec(int sec, uint32_t *h, uint32_t *m, uint32_t *s) { uint32_t hours, minutes, seconds; seconds = QB_ABS(sec); hours = seconds / HOUR_SECONDS; seconds -= HOUR_SECONDS * hours; minutes = seconds / 60; seconds -= 60 * minutes; crm_trace("%d == %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, sec, hours, minutes, seconds); *h = hours; *m = minutes; *s = seconds; } int crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m, uint32_t *s) { crm_time_get_sec(dt->seconds, h, m, s); return TRUE; } int crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m) { uint32_t s; crm_time_get_sec(dt->seconds, h, m, &s); return TRUE; } long long crm_time_get_seconds(const crm_time_t *dt) { int lpc; crm_time_t *utc = NULL; long long in_seconds = 0; if (dt == NULL) { return 0; } utc = crm_get_utc_time(dt); if (utc == NULL) { return 0; } for (lpc = 1; lpc < utc->years; lpc++) { long long dmax = year_days(lpc); in_seconds += DAY_SECONDS * dmax; } /* utc->months is an offset that can only be set for a duration. * By definition, the value is variable depending on the date to * which it is applied. * * Force 30-day months so that something vaguely sane happens * for anyone that tries to use a month in this way. */ if (utc->months > 0) { in_seconds += DAY_SECONDS * 30 * (long long) (utc->months); } if (utc->days > 0) { in_seconds += DAY_SECONDS * (long long) (utc->days - 1); } in_seconds += utc->seconds; crm_time_free(utc); return in_seconds; } #define EPOCH_SECONDS 62135596800ULL /* Calculated using crm_time_get_seconds() */ long long crm_time_get_seconds_since_epoch(const crm_time_t *dt) { return (dt == NULL)? 0 : (crm_time_get_seconds(dt) - EPOCH_SECONDS); } int crm_time_get_gregorian(const crm_time_t *dt, uint32_t *y, uint32_t *m, uint32_t *d) { int months = 0; int days = dt->days; if(dt->years != 0) { for (months = 1; months <= 12 && days > 0; months++) { int mdays = crm_time_days_in_month(months, dt->years); if (mdays >= days) { break; } else { days -= mdays; } } } else if (dt->months) { /* This is a duration including months, don't convert the days field */ months = dt->months; } else { /* This is a duration not including months, still don't convert the days field */ } *y = dt->years; *m = months; *d = days; crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days); return TRUE; } int crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d) { *y = dt->years; *d = dt->days; return TRUE; } int crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w, uint32_t *d) { /* * Monday 29 December 2008 is written "2009-W01-1" * Sunday 3 January 2010 is written "2009-W53-7" */ int year_num = 0; int jan1 = crm_time_january1_weekday(dt->years); int h = -1; CRM_CHECK(dt->days > 0, return FALSE); /* 6. Find the Weekday for Y M D */ h = dt->days + jan1 - 1; *d = 1 + ((h - 1) % 7); /* 7. Find if Y M D falls in YearNumber Y-1, WeekNumber 52 or 53 */ if (dt->days <= (8 - jan1) && jan1 > 4) { crm_trace("year--, jan1=%d", jan1); year_num = dt->years - 1; *w = crm_time_weeks_in_year(year_num); } else { year_num = dt->years; } /* 8. Find if Y M D falls in YearNumber Y+1, WeekNumber 1 */ if (year_num == dt->years) { int dmax = year_days(year_num); int correction = 4 - *d; if ((dmax - dt->days) < correction) { crm_trace("year++, jan1=%d, i=%d vs. %d", jan1, dmax - dt->days, correction); year_num = dt->years + 1; *w = 1; } } /* 9. Find if Y M D falls in YearNumber Y, WeekNumber 1 through 53 */ if (year_num == dt->years) { int j = dt->days + (7 - *d) + (jan1 - 1); *w = j / 7; if (jan1 > 4) { *w -= 1; } } *y = year_num; crm_trace("Converted %.4d-%.3d to %.4" PRIu32 "-W%.2" PRIu32 "-%" PRIu32, dt->years, dt->days, *y, *w, *d); return TRUE; } #define DATE_MAX 128 /*! * \internal * \brief Print "." to a buffer * * \param[in] sec Seconds * \param[in] usec Microseconds (must be of same sign as \p sec and of * absolute value less than \p QB_TIME_US_IN_SEC) * \param[in,out] buf Result buffer * \param[in,out] offset Current offset within \p buf */ static inline void sec_usec_as_string(long long sec, int usec, char *buf, size_t *offset) { *offset += snprintf(buf + *offset, DATE_MAX - *offset, "%s%lld.%06d", ((sec == 0) && (usec < 0))? "-" : "", sec, QB_ABS(usec)); } /*! * \internal * \brief Get a string representation of a duration * * \param[in] dt Time object to interpret as a duration * \param[in] usec Microseconds to add to \p dt * \param[in] show_usec Whether to include microseconds in \p result * \param[out] result Where to store the result string */ static void crm_duration_as_string(const crm_time_t *dt, int usec, bool show_usec, char *result) { size_t offset = 0; CRM_ASSERT(valid_sec_usec(dt->seconds, usec)); if (dt->years) { offset += snprintf(result + offset, DATE_MAX - offset, "%4d year%s ", dt->years, pcmk__plural_s(dt->years)); } if (dt->months) { offset += snprintf(result + offset, DATE_MAX - offset, "%2d month%s ", dt->months, pcmk__plural_s(dt->months)); } if (dt->days) { offset += snprintf(result + offset, DATE_MAX - offset, "%2d day%s ", dt->days, pcmk__plural_s(dt->days)); } // At least print seconds (and optionally usecs) if ((offset == 0) || (dt->seconds != 0) || (show_usec && (usec != 0))) { if (show_usec) { sec_usec_as_string(dt->seconds, usec, result, &offset); } else { offset += snprintf(result + offset, DATE_MAX - offset, "%d", dt->seconds); } offset += snprintf(result + offset, DATE_MAX - offset, " second%s", pcmk__plural_s(dt->seconds)); } // More than one minute, so provide a more readable breakdown into units if (QB_ABS(dt->seconds) >= 60) { uint32_t h = 0; uint32_t m = 0; uint32_t s = 0; uint32_t u = QB_ABS(usec); bool print_sec_component = false; crm_time_get_sec(dt->seconds, &h, &m, &s); print_sec_component = ((s != 0) || (show_usec && (u != 0))); offset += snprintf(result + offset, DATE_MAX - offset, " ("); if (h) { offset += snprintf(result + offset, DATE_MAX - offset, "%" PRIu32 " hour%s%s", h, pcmk__plural_s(h), ((m != 0) || print_sec_component)? " " : ""); } if (m) { offset += snprintf(result + offset, DATE_MAX - offset, "%" PRIu32 " minute%s%s", m, pcmk__plural_s(m), print_sec_component? " " : ""); } if (print_sec_component) { if (show_usec) { sec_usec_as_string(s, u, result, &offset); } else { offset += snprintf(result + offset, DATE_MAX - offset, "%" PRIu32, s); } offset += snprintf(result + offset, DATE_MAX - offset, " second%s", pcmk__plural_s(dt->seconds)); } offset += snprintf(result + offset, DATE_MAX - offset, ")"); } } /*! * \internal * \brief Get a string representation of a time object * * \param[in] dt Time to convert to string * \param[in] usec Microseconds to add to \p dt * \param[in] flags Group of \p crm_time_* string format options * \param[out] result Where to store the result string * * \note \p result must be of size \p DATE_MAX or larger. */ static void time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags, char *result) { crm_time_t *utc = NULL; size_t offset = 0; if (!crm_time_is_defined(dt)) { strcpy(result, ""); return; } CRM_ASSERT(valid_sec_usec(dt->seconds, usec)); /* Simple cases: as duration, seconds, or seconds since epoch. * These never depend on time zone. */ if (pcmk_is_set(flags, crm_time_log_duration)) { crm_duration_as_string(dt, usec, pcmk_is_set(flags, crm_time_usecs), result); return; } if (pcmk_any_flags_set(flags, crm_time_seconds|crm_time_epoch)) { long long seconds = 0; if (pcmk_is_set(flags, crm_time_seconds)) { seconds = crm_time_get_seconds(dt); } else { seconds = crm_time_get_seconds_since_epoch(dt); } if (pcmk_is_set(flags, crm_time_usecs)) { sec_usec_as_string(seconds, usec, result, &offset); } else { snprintf(result, DATE_MAX, "%lld", seconds); } return; } // Convert to UTC if local timezone was not requested if ((dt->offset != 0) && !pcmk_is_set(flags, crm_time_log_with_timezone)) { crm_trace("UTC conversion"); utc = crm_get_utc_time(dt); dt = utc; } // As readable string if (pcmk_is_set(flags, crm_time_log_date)) { if (pcmk_is_set(flags, crm_time_weeks)) { // YYYY-WW-D uint32_t y, w, d; if (crm_time_get_isoweek(dt, &y, &w, &d)) { offset += snprintf(result + offset, DATE_MAX - offset, "%" PRIu32 "-W%.2" PRIu32 "-%" PRIu32, y, w, d); } } else if (pcmk_is_set(flags, crm_time_ordinal)) { // YYYY-DDD uint32_t y, d; if (crm_time_get_ordinal(dt, &y, &d)) { offset += snprintf(result + offset, DATE_MAX - offset, "%" PRIu32 "-%.3" PRIu32, y, d); } } else { // YYYY-MM-DD uint32_t y, m, d; if (crm_time_get_gregorian(dt, &y, &m, &d)) { offset += snprintf(result + offset, DATE_MAX - offset, "%.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d); } } } if (pcmk_is_set(flags, crm_time_log_timeofday)) { uint32_t h = 0, m = 0, s = 0; if (offset > 0) { offset += snprintf(result + offset, DATE_MAX - offset, " "); } if (crm_time_get_timeofday(dt, &h, &m, &s)) { offset += snprintf(result + offset, DATE_MAX - offset, "%.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, h, m, s); if (pcmk_is_set(flags, crm_time_usecs)) { offset += snprintf(result + offset, DATE_MAX - offset, ".%06" PRIu32, QB_ABS(usec)); } } if (pcmk_is_set(flags, crm_time_log_with_timezone) && (dt->offset != 0)) { crm_time_get_sec(dt->offset, &h, &m, &s); offset += snprintf(result + offset, DATE_MAX - offset, " %c%.2" PRIu32 ":%.2" PRIu32, ((dt->offset < 0)? '-' : '+'), h, m); } else { offset += snprintf(result + offset, DATE_MAX - offset, "Z"); } } crm_time_free(utc); } /*! * \brief Get a string representation of a \p crm_time_t object * * \param[in] dt Time to convert to string * \param[in] flags Group of \p crm_time_* string format options * * \note The caller is responsible for freeing the return value using \p free(). */ char * crm_time_as_string(const crm_time_t *dt, int flags) { char result[DATE_MAX] = { '\0', }; char *result_copy = NULL; time_as_string_common(dt, 0, flags, result); pcmk__str_update(&result_copy, result); return result_copy; } /*! * \internal * \brief Determine number of seconds from an hour:minute:second string * * \param[in] time_str Time specification string * \param[out] result Number of seconds equivalent to time_str * * \return TRUE if specification was valid, FALSE (and set errno) otherwise * \note This may return the number of seconds in a day (which is out of bounds * for a time object) if given 24:00:00. */ static bool crm_time_parse_sec(const char *time_str, int *result) { int rc; uint32_t hour = 0; uint32_t minute = 0; uint32_t second = 0; *result = 0; // Must have at least hour, but minutes and seconds are optional rc = sscanf(time_str, "%" SCNu32 ":%" SCNu32 ":%" SCNu32, &hour, &minute, &second); if (rc == 1) { rc = sscanf(time_str, "%2" SCNu32 "%2" SCNu32 "%2" SCNu32, &hour, &minute, &second); } if (rc == 0) { crm_err("%s is not a valid ISO 8601 time specification", time_str); errno = EINVAL; return FALSE; } crm_trace("Got valid time: %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, hour, minute, second); if ((hour == 24) && (minute == 0) && (second == 0)) { // Equivalent to 00:00:00 of next day, return number of seconds in day } else if (hour >= 24) { crm_err("%s is not a valid ISO 8601 time specification " "because %" PRIu32 " is not a valid hour", time_str, hour); errno = EINVAL; return FALSE; } if (minute >= 60) { crm_err("%s is not a valid ISO 8601 time specification " "because %" PRIu32 " is not a valid minute", time_str, minute); errno = EINVAL; return FALSE; } if (second >= 60) { crm_err("%s is not a valid ISO 8601 time specification " "because %" PRIu32 " is not a valid second", time_str, second); errno = EINVAL; return FALSE; } *result = (hour * HOUR_SECONDS) + (minute * 60) + second; return TRUE; } static bool crm_time_parse_offset(const char *offset_str, int *offset) { tzset(); if (offset_str == NULL) { // Use local offset #if defined(HAVE_STRUCT_TM_TM_GMTOFF) time_t now = time(NULL); struct tm *now_tm = localtime(&now); #endif int h_offset = GMTOFF(now_tm) / HOUR_SECONDS; int m_offset = (GMTOFF(now_tm) - (HOUR_SECONDS * h_offset)) / 60; if (h_offset < 0 && m_offset < 0) { m_offset = 0 - m_offset; } *offset = (HOUR_SECONDS * h_offset) + (60 * m_offset); return TRUE; } if (offset_str[0] == 'Z') { // @TODO invalid if anything after? *offset = 0; return TRUE; } *offset = 0; if ((offset_str[0] == '+') || (offset_str[0] == '-') || isdigit((int)offset_str[0])) { gboolean negate = FALSE; if (offset_str[0] == '+') { offset_str++; } else if (offset_str[0] == '-') { negate = TRUE; offset_str++; } if (crm_time_parse_sec(offset_str, offset) == FALSE) { return FALSE; } if (negate) { *offset = 0 - *offset; } } // @TODO else invalid? return TRUE; } /*! * \internal * \brief Parse the time portion of an ISO 8601 date/time string * * \param[in] time_str Time portion of specification (after any 'T') * \param[in,out] a_time Time object to parse into * * \return TRUE if valid time was parsed, FALSE (and set errno) otherwise * \note This may add a day to a_time (if the time is 24:00:00). */ static bool crm_time_parse(const char *time_str, crm_time_t *a_time) { uint32_t h, m, s; char *offset_s = NULL; tzset(); if (time_str) { if (crm_time_parse_sec(time_str, &(a_time->seconds)) == FALSE) { return FALSE; } offset_s = strstr(time_str, "Z"); if (offset_s == NULL) { offset_s = strstr(time_str, " "); if (offset_s) { while (isspace(offset_s[0])) { offset_s++; } } } } if (crm_time_parse_offset(offset_s, &(a_time->offset)) == FALSE) { return FALSE; } crm_time_get_sec(a_time->offset, &h, &m, &s); crm_trace("Got tz: %c%2." PRIu32 ":%.2" PRIu32, (a_time->offset < 0)? '-' : '+', h, m); if (a_time->seconds == DAY_SECONDS) { // 24:00:00 == 00:00:00 of next day a_time->seconds = 0; crm_time_add_days(a_time, 1); } return TRUE; } /* * \internal * \brief Parse a time object from an ISO 8601 date/time specification * * \param[in] date_str ISO 8601 date/time specification (or * \c PCMK__VALUE_EPOCH) * * \return New time object on success, NULL (and set errno) otherwise */ static crm_time_t * parse_date(const char *date_str) { const char *time_s = NULL; crm_time_t *dt = NULL; int year = 0; int month = 0; int week = 0; int day = 0; int rc = 0; if (pcmk__str_empty(date_str)) { crm_err("No ISO 8601 date/time specification given"); goto invalid; } if ((date_str[0] == 'T') || (date_str[2] == ':')) { /* Just a time supplied - Infer current date */ dt = crm_time_new(NULL); if (date_str[0] == 'T') { time_s = date_str + 1; } else { time_s = date_str; } goto parse_time; } dt = crm_time_new_undefined(); if ((strncasecmp(PCMK__VALUE_EPOCH, date_str, 5) == 0) && ((date_str[5] == '\0') || (date_str[5] == '/') || isspace(date_str[5]))) { dt->days = 1; dt->years = 1970; crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday); return dt; } /* YYYY-MM-DD */ rc = sscanf(date_str, "%d-%d-%d", &year, &month, &day); if (rc == 1) { /* YYYYMMDD */ rc = sscanf(date_str, "%4d%2d%2d", &year, &month, &day); } if (rc == 3) { if (month > 12) { crm_err("'%s' is not a valid ISO 8601 date/time specification " "because '%d' is not a valid month", date_str, month); goto invalid; } else if (day > crm_time_days_in_month(month, year)) { crm_err("'%s' is not a valid ISO 8601 date/time specification " "because '%d' is not a valid day of the month", date_str, day); goto invalid; } else { dt->years = year; dt->days = get_ordinal_days(year, month, day); crm_trace("Parsed Gregorian date '%.4d-%.3d' from date string '%s'", year, dt->days, date_str); } goto parse_time; } /* YYYY-DDD */ rc = sscanf(date_str, "%d-%d", &year, &day); if (rc == 2) { if (day > year_days(year)) { crm_err("'%s' is not a valid ISO 8601 date/time specification " "because '%d' is not a valid day of the year (max %d)", date_str, day, year_days(year)); goto invalid; } crm_trace("Parsed ordinal year %d and days %d from date string '%s'", year, day, date_str); dt->days = day; dt->years = year; goto parse_time; } /* YYYY-Www-D */ rc = sscanf(date_str, "%d-W%d-%d", &year, &week, &day); if (rc == 3) { if (week > crm_time_weeks_in_year(year)) { crm_err("'%s' is not a valid ISO 8601 date/time specification " "because '%d' is not a valid week of the year (max %d)", date_str, week, crm_time_weeks_in_year(year)); goto invalid; } else if (day < 1 || day > 7) { crm_err("'%s' is not a valid ISO 8601 date/time specification " "because '%d' is not a valid day of the week", date_str, day); goto invalid; } else { /* * See https://en.wikipedia.org/wiki/ISO_week_date * * Monday 29 December 2008 is written "2009-W01-1" * Sunday 3 January 2010 is written "2009-W53-7" * Saturday 27 September 2008 is written "2008-W37-6" * * If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it is in week 01. * If 1 January is on a Friday, Saturday or Sunday, it is in week 52 or 53 of the previous year. */ int jan1 = crm_time_january1_weekday(year); crm_trace("Got year %d (Jan 1 = %d), week %d, and day %d from date string '%s'", year, jan1, week, day, date_str); dt->years = year; crm_time_add_days(dt, (week - 1) * 7); if (jan1 <= 4) { crm_time_add_days(dt, 1 - jan1); } else { crm_time_add_days(dt, 8 - jan1); } crm_time_add_days(dt, day); } goto parse_time; } crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str); goto invalid; parse_time: if (time_s == NULL) { time_s = date_str + strspn(date_str, "0123456789-W"); if ((time_s[0] == ' ') || (time_s[0] == 'T')) { ++time_s; } else { time_s = NULL; } } if ((time_s != NULL) && (crm_time_parse(time_s, dt) == FALSE)) { goto invalid; } crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday); if (crm_time_check(dt) == FALSE) { crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str); goto invalid; } return dt; invalid: crm_time_free(dt); errno = EINVAL; return NULL; } // Parse an ISO 8601 numeric value and return number of characters consumed // @TODO This cannot handle >INT_MAX int values // @TODO Fractions appear to be not working // @TODO Error out on invalid specifications static int parse_int(const char *str, int field_width, int upper_bound, int *result) { int lpc = 0; int offset = 0; int intermediate = 0; gboolean fraction = FALSE; gboolean negate = FALSE; *result = 0; if (*str == '\0') { return 0; } if (str[offset] == 'T') { offset++; } if (str[offset] == '.' || str[offset] == ',') { fraction = TRUE; field_width = -1; offset++; } else if (str[offset] == '-') { negate = TRUE; offset++; } else if (str[offset] == '+' || str[offset] == ':') { offset++; } for (; (fraction || lpc < field_width) && isdigit((int)str[offset]); lpc++) { if (fraction) { intermediate = (str[offset] - '0') / (10 ^ lpc); } else { *result *= 10; intermediate = str[offset] - '0'; } *result += intermediate; offset++; } if (fraction) { *result = (int)(*result * upper_bound); } else if (upper_bound > 0 && *result > upper_bound) { *result = upper_bound; } if (negate) { *result = 0 - *result; } if (lpc > 0) { crm_trace("Found int: %d. Stopped at str[%d]='%c'", *result, lpc, str[lpc]); return offset; } return 0; } /*! * \brief Parse a time duration from an ISO 8601 duration specification * * \param[in] period_s ISO 8601 duration specification (optionally followed by * whitespace, after which the rest of the string will be * ignored) * * \return New time object on success, NULL (and set errno) otherwise * \note It is the caller's responsibility to return the result using * crm_time_free(). */ crm_time_t * crm_time_parse_duration(const char *period_s) { gboolean is_time = FALSE; crm_time_t *diff = NULL; if (pcmk__str_empty(period_s)) { crm_err("No ISO 8601 time duration given"); goto invalid; } if (period_s[0] != 'P') { crm_err("'%s' is not a valid ISO 8601 time duration " "because it does not start with a 'P'", period_s); goto invalid; } if ((period_s[1] == '\0') || isspace(period_s[1])) { crm_err("'%s' is not a valid ISO 8601 time duration " "because nothing follows 'P'", period_s); goto invalid; } diff = crm_time_new_undefined(); diff->duration = TRUE; for (const char *current = period_s + 1; current[0] && (current[0] != '/') && !isspace(current[0]); ++current) { int an_int = 0, rc; if (current[0] == 'T') { /* A 'T' separates year/month/day from hour/minute/seconds. We don't * require it strictly, but just use it to differentiate month from * minutes. */ is_time = TRUE; continue; } // An integer must be next rc = parse_int(current, 10, 0, &an_int); if (rc == 0) { crm_err("'%s' is not a valid ISO 8601 time duration " "because no integer at '%s'", period_s, current); goto invalid; } current += rc; // A time unit must be next (we're not strict about the order) switch (current[0]) { case 'Y': diff->years = an_int; break; case 'M': if (is_time) { /* Minutes */ diff->seconds += an_int * 60; } else { diff->months = an_int; } break; case 'W': diff->days += an_int * 7; break; case 'D': diff->days += an_int; break; case 'H': diff->seconds += an_int * HOUR_SECONDS; break; case 'S': diff->seconds += an_int; break; case '\0': crm_err("'%s' is not a valid ISO 8601 time duration " "because no units after %d", period_s, an_int); goto invalid; default: crm_err("'%s' is not a valid ISO 8601 time duration " "because '%c' is not a valid time unit", period_s, current[0]); goto invalid; } } if (!crm_time_is_defined(diff)) { crm_err("'%s' is not a valid ISO 8601 time duration " "because no amounts and units given", period_s); goto invalid; } return diff; invalid: crm_time_free(diff); errno = EINVAL; return NULL; } /*! * \brief Parse a time period from an ISO 8601 interval specification * * \param[in] period_str ISO 8601 interval specification (start/end, * start/duration, or duration/end) * * \return New time period object on success, NULL (and set errno) otherwise * \note The caller is responsible for freeing the result using * crm_time_free_period(). */ crm_time_period_t * crm_time_parse_period(const char *period_str) { const char *original = period_str; crm_time_period_t *period = NULL; if (pcmk__str_empty(period_str)) { crm_err("No ISO 8601 time period given"); goto invalid; } tzset(); period = calloc(1, sizeof(crm_time_period_t)); CRM_ASSERT(period != NULL); if (period_str[0] == 'P') { period->diff = crm_time_parse_duration(period_str); if (period->diff == NULL) { goto error; } } else { period->start = parse_date(period_str); if (period->start == NULL) { goto error; } } period_str = strstr(original, "/"); if (period_str) { ++period_str; if (period_str[0] == 'P') { if (period->diff != NULL) { crm_err("'%s' is not a valid ISO 8601 time period " "because it has two durations", original); goto invalid; } period->diff = crm_time_parse_duration(period_str); if (period->diff == NULL) { goto error; } } else { period->end = parse_date(period_str); if (period->end == NULL) { goto error; } } } else if (period->diff != NULL) { // Only duration given, assume start is now period->start = crm_time_new(NULL); } else { // Only start given crm_err("'%s' is not a valid ISO 8601 time period " "because it has no duration or ending time", original); goto invalid; } if (period->start == NULL) { period->start = crm_time_subtract(period->end, period->diff); } else if (period->end == NULL) { period->end = crm_time_add(period->start, period->diff); } if (crm_time_check(period->start) == FALSE) { crm_err("'%s' is not a valid ISO 8601 time period " "because the start is invalid", period_str); goto invalid; } if (crm_time_check(period->end) == FALSE) { crm_err("'%s' is not a valid ISO 8601 time period " "because the end is invalid", period_str); goto invalid; } return period; invalid: errno = EINVAL; error: crm_time_free_period(period); return NULL; } /*! * \brief Free a dynamically allocated time period object * * \param[in,out] period Time period to free */ void crm_time_free_period(crm_time_period_t *period) { if (period) { crm_time_free(period->start); crm_time_free(period->end); crm_time_free(period->diff); free(period); } } void crm_time_set(crm_time_t *target, const crm_time_t *source) { crm_trace("target=%p, source=%p", target, source); CRM_CHECK(target != NULL && source != NULL, return); target->years = source->years; target->days = source->days; target->months = source->months; /* Only for durations */ target->seconds = source->seconds; target->offset = source->offset; crm_time_log(LOG_TRACE, "source", source, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); crm_time_log(LOG_TRACE, "target", target, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); } static void ha_set_tm_time(crm_time_t *target, const struct tm *source) { int h_offset = 0; int m_offset = 0; /* Ensure target is fully initialized */ target->years = 0; target->months = 0; target->days = 0; target->seconds = 0; target->offset = 0; target->duration = FALSE; if (source->tm_year > 0) { /* years since 1900 */ target->years = 1900 + source->tm_year; } if (source->tm_yday >= 0) { /* days since January 1 [0-365] */ target->days = 1 + source->tm_yday; } if (source->tm_hour >= 0) { target->seconds += HOUR_SECONDS * source->tm_hour; } if (source->tm_min >= 0) { target->seconds += 60 * source->tm_min; } if (source->tm_sec >= 0) { target->seconds += source->tm_sec; } /* tm_gmtoff == offset from UTC in seconds */ h_offset = GMTOFF(source) / HOUR_SECONDS; m_offset = (GMTOFF(source) - (HOUR_SECONDS * h_offset)) / 60; crm_trace("Time offset is %lds (%.2d:%.2d)", GMTOFF(source), h_offset, m_offset); target->offset += HOUR_SECONDS * h_offset; target->offset += 60 * m_offset; } void crm_time_set_timet(crm_time_t *target, const time_t *source) { ha_set_tm_time(target, localtime(source)); } crm_time_t * pcmk_copy_time(const crm_time_t *source) { crm_time_t *target = crm_time_new_undefined(); crm_time_set(target, source); return target; } /*! * \internal * \brief Convert a \p time_t time to a \p crm_time_t time * * \param[in] source Time to convert * * \return A \p crm_time_t object representing \p source */ crm_time_t * pcmk__copy_timet(time_t source) { crm_time_t *target = crm_time_new_undefined(); crm_time_set_timet(target, &source); return target; } crm_time_t * crm_time_add(const crm_time_t *dt, const crm_time_t *value) { crm_time_t *utc = NULL; crm_time_t *answer = NULL; if ((dt == NULL) || (value == NULL)) { errno = EINVAL; return NULL; } answer = pcmk_copy_time(dt); utc = crm_get_utc_time(value); if (utc == NULL) { crm_time_free(answer); return NULL; } answer->years += utc->years; crm_time_add_months(answer, utc->months); crm_time_add_days(answer, utc->days); crm_time_add_seconds(answer, utc->seconds); crm_time_free(utc); return answer; } +/*! + * \internal + * \brief Add the value of an XML attribute to a time object + * + * \param[in,out] t Time object to add to + * \param[in] component Component of \p t to add to + * \param[in] xml XML with value to add + * + * \return Standard Pacemaker return code + */ +int +pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component, + const xmlNode *xml) +{ + long long value; + const char *attr = NULL; + void (*add)(crm_time_t *, int) = NULL; + + if (t == NULL) { + return EINVAL; + } + + switch (component) { + case pcmk__time_years: + attr = PCMK_XA_YEARS; + add = crm_time_add_years; + break; + + case pcmk__time_months: + attr = PCMK_XA_MONTHS; + add = crm_time_add_months; + break; + + case pcmk__time_weeks: + attr = PCMK_XA_WEEKS; + add = crm_time_add_weeks; + break; + + case pcmk__time_days: + attr = PCMK_XA_DAYS; + add = crm_time_add_days; + break; + + case pcmk__time_hours: + attr = PCMK_XA_HOURS; + add = crm_time_add_hours; + break; + + case pcmk__time_minutes: + attr = PCMK_XA_MINUTES; + add = crm_time_add_minutes; + break; + + case pcmk__time_seconds: + attr = PCMK_XA_SECONDS; + add = crm_time_add_seconds; + break; + + default: + return EINVAL; + } + + if (xml == NULL) { + return pcmk_rc_ok; + } + + if (pcmk__scan_ll(crm_element_value(xml, attr), &value, + 0LL) != pcmk_rc_ok) { + return pcmk_rc_unpack_error; + } + + if ((value < INT_MIN) || (value > INT_MAX)) { + return ERANGE; + } + + if (value != 0LL) { + add(t, (int) value); + } + return pcmk_rc_ok; +} + crm_time_t * crm_time_calculate_duration(const crm_time_t *dt, const crm_time_t *value) { crm_time_t *utc = NULL; crm_time_t *answer = NULL; if ((dt == NULL) || (value == NULL)) { errno = EINVAL; return NULL; } utc = crm_get_utc_time(value); if (utc == NULL) { return NULL; } answer = crm_get_utc_time(dt); if (answer == NULL) { crm_time_free(utc); return NULL; } answer->duration = TRUE; answer->years -= utc->years; if(utc->months != 0) { crm_time_add_months(answer, -utc->months); } crm_time_add_days(answer, -utc->days); crm_time_add_seconds(answer, -utc->seconds); crm_time_free(utc); return answer; } crm_time_t * crm_time_subtract(const crm_time_t *dt, const crm_time_t *value) { crm_time_t *utc = NULL; crm_time_t *answer = NULL; if ((dt == NULL) || (value == NULL)) { errno = EINVAL; return NULL; } utc = crm_get_utc_time(value); if (utc == NULL) { return NULL; } answer = pcmk_copy_time(dt); answer->years -= utc->years; if(utc->months != 0) { crm_time_add_months(answer, -utc->months); } crm_time_add_days(answer, -utc->days); crm_time_add_seconds(answer, -utc->seconds); crm_time_free(utc); return answer; } /*! * \brief Check whether a time object represents a sensible date/time * * \param[in] dt Date/time object to check * * \return \c true if years, days, and seconds are sensible, \c false otherwise */ bool crm_time_check(const crm_time_t *dt) { return (dt != NULL) && (dt->days > 0) && (dt->days <= year_days(dt->years)) && (dt->seconds >= 0) && (dt->seconds < DAY_SECONDS); } #define do_cmp_field(l, r, field) \ if(rc == 0) { \ if(l->field > r->field) { \ crm_trace("%s: %d > %d", \ #field, l->field, r->field); \ rc = 1; \ } else if(l->field < r->field) { \ crm_trace("%s: %d < %d", \ #field, l->field, r->field); \ rc = -1; \ } \ } int crm_time_compare(const crm_time_t *a, const crm_time_t *b) { int rc = 0; crm_time_t *t1 = crm_get_utc_time(a); crm_time_t *t2 = crm_get_utc_time(b); if ((t1 == NULL) && (t2 == NULL)) { rc = 0; } else if (t1 == NULL) { rc = -1; } else if (t2 == NULL) { rc = 1; } else { do_cmp_field(t1, t2, years); do_cmp_field(t1, t2, days); do_cmp_field(t1, t2, seconds); } crm_time_free(t1); crm_time_free(t2); return rc; } /*! * \brief Add a given number of seconds to a date/time or duration * * \param[in,out] a_time Date/time or duration to add seconds to * \param[in] extra Number of seconds to add */ void crm_time_add_seconds(crm_time_t *a_time, int extra) { int days = 0; crm_trace("Adding %d seconds to %d (max=%d)", extra, a_time->seconds, DAY_SECONDS); a_time->seconds += extra; days = a_time->seconds / DAY_SECONDS; a_time->seconds %= DAY_SECONDS; // Don't have negative seconds if (a_time->seconds < 0) { a_time->seconds += DAY_SECONDS; --days; } crm_time_add_days(a_time, days); } void crm_time_add_days(crm_time_t * a_time, int extra) { int lower_bound = 1; int ydays = crm_time_leapyear(a_time->years) ? 366 : 365; crm_trace("Adding %d days to %.4d-%.3d", extra, a_time->years, a_time->days); a_time->days += extra; while (a_time->days > ydays) { a_time->years++; a_time->days -= ydays; ydays = crm_time_leapyear(a_time->years) ? 366 : 365; } if(a_time->duration) { lower_bound = 0; } while (a_time->days < lower_bound) { a_time->years--; a_time->days += crm_time_leapyear(a_time->years) ? 366 : 365; } } void crm_time_add_months(crm_time_t * a_time, int extra) { int lpc; uint32_t y, m, d, dmax; crm_time_get_gregorian(a_time, &y, &m, &d); crm_trace("Adding %d months to %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, extra, y, m, d); if (extra > 0) { for (lpc = extra; lpc > 0; lpc--) { m++; if (m == 13) { m = 1; y++; } } } else { for (lpc = -extra; lpc > 0; lpc--) { m--; if (m == 0) { m = 12; y--; } } } dmax = crm_time_days_in_month(m, y); if (dmax < d) { /* Preserve day-of-month unless the month doesn't have enough days */ d = dmax; } crm_trace("Calculated %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d); a_time->years = y; a_time->days = get_ordinal_days(y, m, d); crm_time_get_gregorian(a_time, &y, &m, &d); crm_trace("Got %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d); } void crm_time_add_minutes(crm_time_t * a_time, int extra) { crm_time_add_seconds(a_time, extra * 60); } void crm_time_add_hours(crm_time_t * a_time, int extra) { crm_time_add_seconds(a_time, extra * HOUR_SECONDS); } void crm_time_add_weeks(crm_time_t * a_time, int extra) { crm_time_add_days(a_time, extra * 7); } void crm_time_add_years(crm_time_t * a_time, int extra) { a_time->years += extra; } static void ha_get_tm_time(struct tm *target, const crm_time_t *source) { *target = (struct tm) { .tm_year = source->years - 1900, .tm_mday = source->days, .tm_sec = source->seconds % 60, .tm_min = ( source->seconds / 60 ) % 60, .tm_hour = source->seconds / HOUR_SECONDS, .tm_isdst = -1, /* don't adjust */ #if defined(HAVE_STRUCT_TM_TM_GMTOFF) .tm_gmtoff = source->offset #endif }; mktime(target); } /* The high-resolution variant of time object was added to meet an immediate * need, and is kept internal API. * * @TODO The long-term goal is to come up with a clean, unified design for a * time type (or types) that meets all the various needs, to replace * crm_time_t, pcmk__time_hr_t, and struct timespec (in lrmd_cmd_t). * Using glib's GDateTime is a possibility (if we are willing to require * glib >= 2.26). */ pcmk__time_hr_t * pcmk__time_hr_convert(pcmk__time_hr_t *target, const crm_time_t *dt) { pcmk__time_hr_t *hr_dt = NULL; if (dt) { hr_dt = target?target:calloc(1, sizeof(pcmk__time_hr_t)); CRM_ASSERT(hr_dt != NULL); *hr_dt = (pcmk__time_hr_t) { .years = dt->years, .months = dt->months, .days = dt->days, .seconds = dt->seconds, .offset = dt->offset, .duration = dt->duration }; } return hr_dt; } void pcmk__time_set_hr_dt(crm_time_t *target, const pcmk__time_hr_t *hr_dt) { CRM_ASSERT((hr_dt) && (target)); *target = (crm_time_t) { .years = hr_dt->years, .months = hr_dt->months, .days = hr_dt->days, .seconds = hr_dt->seconds, .offset = hr_dt->offset, .duration = hr_dt->duration }; } /*! * \internal * \brief Return the current time as a high-resolution time * * \param[out] epoch If not NULL, this will be set to seconds since epoch * * \return Newly allocated high-resolution time set to the current time */ pcmk__time_hr_t * pcmk__time_hr_now(time_t *epoch) { struct timespec tv; crm_time_t dt; pcmk__time_hr_t *hr; qb_util_timespec_from_epoch_get(&tv); if (epoch != NULL) { *epoch = tv.tv_sec; } crm_time_set_timet(&dt, &(tv.tv_sec)); hr = pcmk__time_hr_convert(NULL, &dt); if (hr != NULL) { hr->useconds = tv.tv_nsec / QB_TIME_NS_IN_USEC; } return hr; } pcmk__time_hr_t * pcmk__time_hr_new(const char *date_time) { pcmk__time_hr_t *hr_dt = NULL; if (date_time == NULL) { hr_dt = pcmk__time_hr_now(NULL); } else { crm_time_t *dt; dt = parse_date(date_time); hr_dt = pcmk__time_hr_convert(NULL, dt); crm_time_free(dt); } return hr_dt; } void pcmk__time_hr_free(pcmk__time_hr_t * hr_dt) { free(hr_dt); } char * pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt) { const char *mark_s; int max = 128, scanned_pos = 0, printed_pos = 0, fmt_pos = 0, date_len = 0, nano_digits = 0; char nano_s[10], date_s[max+1], nanofmt_s[5] = "%", *tmp_fmt_s; struct tm tm; crm_time_t dt; if (!format) { return NULL; } pcmk__time_set_hr_dt(&dt, hr_dt); ha_get_tm_time(&tm, &dt); sprintf(nano_s, "%06d000", hr_dt->useconds); while ((format[scanned_pos]) != '\0') { mark_s = strchr(&format[scanned_pos], '%'); if (mark_s) { int fmt_len = 1; fmt_pos = mark_s - format; while ((format[fmt_pos+fmt_len] != '\0') && (format[fmt_pos+fmt_len] >= '0') && (format[fmt_pos+fmt_len] <= '9')) { fmt_len++; } scanned_pos = fmt_pos + fmt_len + 1; if (format[fmt_pos+fmt_len] == 'N') { nano_digits = atoi(&format[fmt_pos+1]); nano_digits = (nano_digits > 6)?6:nano_digits; nano_digits = (nano_digits < 0)?0:nano_digits; sprintf(&nanofmt_s[1], ".%ds", nano_digits); } else { if (format[scanned_pos] != '\0') { continue; } fmt_pos = scanned_pos; /* print till end */ } } else { scanned_pos = strlen(format); fmt_pos = scanned_pos; /* print till end */ } tmp_fmt_s = strndup(&format[printed_pos], fmt_pos - printed_pos); #ifdef HAVE_FORMAT_NONLITERAL #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif date_len += strftime(&date_s[date_len], max-date_len, tmp_fmt_s, &tm); #ifdef HAVE_FORMAT_NONLITERAL #pragma GCC diagnostic pop #endif printed_pos = scanned_pos; free(tmp_fmt_s); if (nano_digits) { #ifdef HAVE_FORMAT_NONLITERAL #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif date_len += snprintf(&date_s[date_len], max-date_len, nanofmt_s, nano_s); #ifdef HAVE_FORMAT_NONLITERAL #pragma GCC diagnostic pop #endif nano_digits = 0; } } return (date_len == 0)?NULL:strdup(date_s); } /*! * \internal * \brief Return a human-friendly string corresponding to an epoch time value * * \param[in] source Pointer to epoch time value (or \p NULL for current time) * \param[in] flags Group of \p crm_time_* flags controlling display format * (0 to use \p ctime() with newline removed) * * \return String representation of \p source on success (may be empty depending * on \p flags; guaranteed not to be \p NULL) * * \note The caller is responsible for freeing the return value using \p free(). */ char * pcmk__epoch2str(const time_t *source, uint32_t flags) { time_t epoch_time = (source == NULL)? time(NULL) : *source; char *result = NULL; if (flags == 0) { const char *buf = pcmk__trim(ctime(&epoch_time)); if (buf != NULL) { result = strdup(buf); CRM_ASSERT(result != NULL); } } else { crm_time_t dt; crm_time_set_timet(&dt, &epoch_time); result = crm_time_as_string(&dt, flags); } return result; } /*! * \internal * \brief Return a human-friendly string corresponding to seconds-and- * nanoseconds value * * Time is shown with microsecond resolution if \p crm_time_usecs is in \p * flags. * * \param[in] ts Time in seconds and nanoseconds (or \p NULL for current * time) * \param[in] flags Group of \p crm_time_* flags controlling display format * * \return String representation of \p ts on success (may be empty depending on * \p flags; guaranteed not to be \p NULL) * * \note The caller is responsible for freeing the return value using \p free(). */ char * pcmk__timespec2str(const struct timespec *ts, uint32_t flags) { struct timespec tmp_ts; crm_time_t dt; char result[DATE_MAX] = { 0 }; char *result_copy = NULL; if (ts == NULL) { qb_util_timespec_from_epoch_get(&tmp_ts); ts = &tmp_ts; } crm_time_set_timet(&dt, &ts->tv_sec); time_as_string_common(&dt, ts->tv_nsec / QB_TIME_NS_IN_USEC, flags, result); pcmk__str_update(&result_copy, result); return result_copy; } /*! * \internal * \brief Given a millisecond interval, return a log-friendly string * * \param[in] interval_ms Interval in milliseconds * * \return Readable version of \p interval_ms * * \note The return value is a pointer to static memory that will be * overwritten by later calls to this function. */ const char * pcmk__readable_interval(guint interval_ms) { #define MS_IN_S (1000) #define MS_IN_M (MS_IN_S * 60) #define MS_IN_H (MS_IN_M * 60) #define MS_IN_D (MS_IN_H * 24) #define MAXSTR sizeof("..d..h..m..s...ms") static char str[MAXSTR]; int offset = 0; str[0] = '\0'; if (interval_ms > MS_IN_D) { offset += snprintf(str + offset, MAXSTR - offset, "%ud", interval_ms / MS_IN_D); interval_ms -= (interval_ms / MS_IN_D) * MS_IN_D; } if (interval_ms > MS_IN_H) { offset += snprintf(str + offset, MAXSTR - offset, "%uh", interval_ms / MS_IN_H); interval_ms -= (interval_ms / MS_IN_H) * MS_IN_H; } if (interval_ms > MS_IN_M) { offset += snprintf(str + offset, MAXSTR - offset, "%um", interval_ms / MS_IN_M); interval_ms -= (interval_ms / MS_IN_M) * MS_IN_M; } // Ns, N.NNNs, or NNNms if (interval_ms > MS_IN_S) { offset += snprintf(str + offset, MAXSTR - offset, "%u", interval_ms / MS_IN_S); interval_ms -= (interval_ms / MS_IN_S) * MS_IN_S; if (interval_ms > 0) { offset += snprintf(str + offset, MAXSTR - offset, ".%03u", interval_ms); } (void) snprintf(str + offset, MAXSTR - offset, "s"); } else if (interval_ms > 0) { (void) snprintf(str + offset, MAXSTR - offset, "%ums", interval_ms); } else if (str[0] == '\0') { strcpy(str, "0s"); } return str; } diff --git a/lib/common/tests/iso8601/Makefile.am b/lib/common/tests/iso8601/Makefile.am index 5187aecb64..39145ef6af 100644 --- a/lib/common/tests/iso8601/Makefile.am +++ b/lib/common/tests/iso8601/Makefile.am @@ -1,16 +1,17 @@ # -# Copyright 2020-2022 the Pacemaker project contributors +# Copyright 2020-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # include $(top_srcdir)/mk/tap.mk include $(top_srcdir)/mk/unittest.mk # Add "_test" to the end of all test program names to simplify .gitignore. -check_PROGRAMS = pcmk__readable_interval_test +check_PROGRAMS = pcmk__add_time_from_xml_test \ + pcmk__readable_interval_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c b/lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c new file mode 100644 index 0000000000..d554912230 --- /dev/null +++ b/lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c @@ -0,0 +1,242 @@ +/* + * Copyright 2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include // xmlNode + +#include + +#include +#include +#include + +#define ALL_VALID "" + +#define YEARS_INVALID "" + +#define YEARS_TOO_BIG "" + +#define YEARS_TOO_SMALL "" + +static void +null_time_invalid(void **state) +{ + xmlNode *xml = string2xml(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(NULL, pcmk__time_years, xml), + EINVAL); + free_xml(xml); +} + +static void +null_xml_ok(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = pcmk_copy_time(t); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, NULL), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); +} + +static void +invalid_component(void **state) +{ + xmlNode *xml = string2xml(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(NULL, pcmk__time_unknown, xml), + EINVAL); + free_xml(xml); +} + +static void +missing_attr(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = pcmk_copy_time(t); + xmlNode *xml = string2xml(YEARS_INVALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_months, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +invalid_attr(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = pcmk_copy_time(t); + xmlNode *xml = string2xml(YEARS_INVALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml), + pcmk_rc_unpack_error); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +out_of_range_attr(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = pcmk_copy_time(t); + xmlNode *xml = NULL; + + xml = string2xml(YEARS_TOO_BIG); + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml), ERANGE); + assert_int_equal(crm_time_compare(t, reference), 0); + free_xml(xml); + + xml = string2xml(YEARS_TOO_SMALL); + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml), ERANGE); + assert_int_equal(crm_time_compare(t, reference), 0); + free_xml(xml); + + crm_time_free(t); + crm_time_free(reference); +} + +static void +add_years(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2025-01-01 15:00:00"); + xmlNode *xml = string2xml(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +add_months(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2024-03-01 15:00:00"); + xmlNode *xml = string2xml(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_months, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +add_weeks(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2024-01-22 15:00:00"); + xmlNode *xml = string2xml(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_weeks, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +add_days(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2023-12-31 15:00:00"); + xmlNode *xml = string2xml(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_days, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +add_hours(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2024-01-01 16:00:00"); + xmlNode *xml = string2xml(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_hours, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +add_minutes(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2024-01-01 15:01:00"); + xmlNode *xml = string2xml(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_minutes, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +add_seconds(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2024-01-01 15:00:01"); + xmlNode *xml = string2xml(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_seconds, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_time_invalid), + cmocka_unit_test(null_xml_ok), + cmocka_unit_test(invalid_component), + cmocka_unit_test(missing_attr), + cmocka_unit_test(invalid_attr), + cmocka_unit_test(out_of_range_attr), + cmocka_unit_test(add_years), + cmocka_unit_test(add_months), + cmocka_unit_test(add_weeks), + cmocka_unit_test(add_days), + cmocka_unit_test(add_hours), + cmocka_unit_test(add_minutes), + cmocka_unit_test(add_seconds)); diff --git a/lib/pengine/rules.c b/lib/pengine/rules.c index c2716c8df8..08eeb95d19 100644 --- a/lib/pengine/rules.c +++ b/lib/pengine/rules.c @@ -1,1114 +1,1101 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include CRM_TRACE_INIT_DATA(pe_rules); /*! * \brief Evaluate any rules contained by given XML element * * \param[in,out] xml XML element to check for rules * \param[in] node_hash Node attributes to use to evaluate expressions * \param[in] now Time to use when evaluating expressions * \param[out] next_change If not NULL, set to when evaluation will change * * \return TRUE if no rules, or any of rules present is in effect, else FALSE */ gboolean pe_evaluate_rules(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now, crm_time_t *next_change) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .now = now, .match_data = NULL, .rsc_data = NULL, .op_data = NULL }; return pe_eval_rules(ruleset, &rule_data, next_change); } gboolean pe_test_rule(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, crm_time_t *next_change, pe_match_data_t *match_data) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .now = now, .match_data = match_data, .rsc_data = NULL, .op_data = NULL }; return pe_eval_expr(rule, &rule_data, next_change); } /*! * \brief Evaluate one rule subelement (pass/fail) * * A rule element may contain another rule, a node attribute expression, or a * date expression. Given any one of those, evaluate it and return whether it * passed. * * \param[in,out] expr Rule subelement XML * \param[in] node_hash Node attributes to use when evaluating expression * \param[in] role Ignored (deprecated) * \param[in] now Time to use when evaluating expression * \param[out] next_change If not NULL, set to when evaluation will change * \param[in] match_data If not NULL, resource back-references and params * * \return TRUE if expression is in effect under given conditions, else FALSE */ gboolean pe_test_expression(xmlNode *expr, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, crm_time_t *next_change, pe_match_data_t *match_data) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .now = now, .match_data = match_data, .rsc_data = NULL, .op_data = NULL }; return pe_eval_subexpr(expr, &rule_data, next_change); } -static void -update_field(crm_time_t *t, const xmlNode *xml, const char *attr, - void (*time_fn)(crm_time_t *, int)) -{ - long long value; - - if ((pcmk__scan_ll(crm_element_value(xml, attr), &value, 0LL) == pcmk_rc_ok) - && (value != 0LL) && (value >= INT_MIN) && (value <= INT_MAX)) { - time_fn(t, (int) value); - } -} - static crm_time_t * parse_xml_duration(const crm_time_t *start, const xmlNode *duration_spec) { crm_time_t *end = pcmk_copy_time(start); - update_field(end, duration_spec, PCMK_XA_YEARS, crm_time_add_years); - update_field(end, duration_spec, PCMK_XA_MONTHS, crm_time_add_months); - update_field(end, duration_spec, PCMK_XA_WEEKS, crm_time_add_weeks); - update_field(end, duration_spec, PCMK_XA_DAYS, crm_time_add_days); - update_field(end, duration_spec, PCMK_XA_HOURS, crm_time_add_hours); - update_field(end, duration_spec, PCMK_XA_MINUTES, crm_time_add_minutes); - update_field(end, duration_spec, PCMK_XA_SECONDS, crm_time_add_seconds); - + pcmk__add_time_from_xml(end, pcmk__time_years, duration_spec); + pcmk__add_time_from_xml(end, pcmk__time_months, duration_spec); + pcmk__add_time_from_xml(end, pcmk__time_weeks, duration_spec); + pcmk__add_time_from_xml(end, pcmk__time_days, duration_spec); + pcmk__add_time_from_xml(end, pcmk__time_hours, duration_spec); + pcmk__add_time_from_xml(end, pcmk__time_minutes, duration_spec); + pcmk__add_time_from_xml(end, pcmk__time_seconds, duration_spec); return end; } // Set next_change to t if t is earlier static void crm_time_set_if_earlier(crm_time_t *next_change, crm_time_t *t) { if ((next_change != NULL) && (t != NULL)) { if (!crm_time_is_defined(next_change) || (crm_time_compare(t, next_change) < 0)) { crm_time_set(next_change, t); } } } // Information about a block of nvpair elements typedef struct sorted_set_s { int score; // This block's score for sorting const char *name; // This block's ID const char *special_name; // ID that should sort first xmlNode *attr_set; // This block gboolean overwrite; // Whether existing values will be overwritten } sorted_set_t; static gint sort_pairs(gconstpointer a, gconstpointer b) { const sorted_set_t *pair_a = a; const sorted_set_t *pair_b = b; if (a == NULL && b == NULL) { return 0; } else if (a == NULL) { return 1; } else if (b == NULL) { return -1; } if (pcmk__str_eq(pair_a->name, pair_a->special_name, pcmk__str_casei)) { return -1; } else if (pcmk__str_eq(pair_b->name, pair_a->special_name, pcmk__str_casei)) { return 1; } /* If we're overwriting values, we want lowest score first, so the highest * score is processed last; if we're not overwriting values, we want highest * score first, so nothing else overwrites it. */ if (pair_a->score < pair_b->score) { return pair_a->overwrite? -1 : 1; } else if (pair_a->score > pair_b->score) { return pair_a->overwrite? 1 : -1; } return 0; } static void populate_hash(xmlNode * nvpair_list, GHashTable * hash, gboolean overwrite, xmlNode * top) { const char *name = NULL; const char *value = NULL; const char *old_value = NULL; xmlNode *list = nvpair_list; xmlNode *an_attr = NULL; if (pcmk__xe_is(list->children, PCMK__XE_ATTRIBUTES)) { list = list->children; } for (an_attr = pcmk__xe_first_child(list); an_attr != NULL; an_attr = pcmk__xe_next(an_attr)) { if (pcmk__xe_is(an_attr, PCMK_XE_NVPAIR)) { xmlNode *ref_nvpair = expand_idref(an_attr, top); name = crm_element_value(an_attr, PCMK_XA_NAME); if ((name == NULL) && (ref_nvpair != NULL)) { name = crm_element_value(ref_nvpair, PCMK_XA_NAME); } value = crm_element_value(an_attr, PCMK_XA_VALUE); if ((value == NULL) && (ref_nvpair != NULL)) { value = crm_element_value(ref_nvpair, PCMK_XA_VALUE); } if (name == NULL || value == NULL) { continue; } old_value = g_hash_table_lookup(hash, name); if (pcmk__str_eq(value, "#default", pcmk__str_casei)) { if (old_value) { crm_trace("Letting %s default (removing explicit value \"%s\")", name, value); g_hash_table_remove(hash, name); } continue; } else if (old_value == NULL) { crm_trace("Setting %s=\"%s\"", name, value); pcmk__insert_dup(hash, name, value); } else if (overwrite) { crm_trace("Setting %s=\"%s\" (overwriting old value \"%s\")", name, value, old_value); pcmk__insert_dup(hash, name, value); } } } } typedef struct unpack_data_s { gboolean overwrite; void *hash; crm_time_t *next_change; const pe_rule_eval_data_t *rule_data; xmlNode *top; } unpack_data_t; static void unpack_attr_set(gpointer data, gpointer user_data) { sorted_set_t *pair = data; unpack_data_t *unpack_data = user_data; if (!pe_eval_rules(pair->attr_set, unpack_data->rule_data, unpack_data->next_change)) { return; } crm_trace("Adding attributes from %s (score %d) %s overwrite", pair->name, pair->score, (unpack_data->overwrite? "with" : "without")); populate_hash(pair->attr_set, unpack_data->hash, unpack_data->overwrite, unpack_data->top); } /*! * \internal * \brief Create a sorted list of nvpair blocks * * \param[in,out] top XML document root (used to expand id-ref's) * \param[in] xml_obj XML element containing blocks of nvpair elements * \param[in] set_name If not NULL, only get blocks of this element * \param[in] always_first If not NULL, sort block with this ID as first * * \return List of sorted_set_t entries for nvpair blocks */ static GList * make_pairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, const char *always_first, gboolean overwrite) { GList *unsorted = NULL; if (xml_obj == NULL) { return NULL; } for (xmlNode *attr_set = pcmk__xe_first_child(xml_obj); attr_set != NULL; attr_set = pcmk__xe_next(attr_set)) { if ((set_name == NULL) || pcmk__xe_is(attr_set, set_name)) { const char *score = NULL; sorted_set_t *pair = NULL; xmlNode *expanded_attr_set = expand_idref(attr_set, top); if (expanded_attr_set == NULL) { continue; // Not possible with schema validation enabled } pair = calloc(1, sizeof(sorted_set_t)); pair->name = pcmk__xe_id(expanded_attr_set); pair->special_name = always_first; pair->attr_set = expanded_attr_set; pair->overwrite = overwrite; score = crm_element_value(expanded_attr_set, PCMK_XA_SCORE); pair->score = char2score(score); unsorted = g_list_prepend(unsorted, pair); } } return g_list_sort(unsorted, sort_pairs); } /*! * \brief Extract nvpair blocks contained by an XML element into a hash table * * \param[in,out] top XML document root (used to expand id-ref's) * \param[in] xml_obj XML element containing blocks of nvpair elements * \param[in] set_name If not NULL, only use blocks of this element * \param[in] rule_data Matching parameters to use when unpacking * \param[out] hash Where to store extracted name/value pairs * \param[in] always_first If not NULL, process block with this ID first * \param[in] overwrite Whether to replace existing values with same name * \param[out] next_change If not NULL, set to when evaluation will change */ void pe_eval_nvpairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, const pe_rule_eval_data_t *rule_data, GHashTable *hash, const char *always_first, gboolean overwrite, crm_time_t *next_change) { GList *pairs = make_pairs(top, xml_obj, set_name, always_first, overwrite); if (pairs) { unpack_data_t data = { .hash = hash, .overwrite = overwrite, .next_change = next_change, .top = top, .rule_data = rule_data }; g_list_foreach(pairs, unpack_attr_set, &data); g_list_free_full(pairs, free); } } /*! * \brief Extract nvpair blocks contained by an XML element into a hash table * * \param[in,out] top XML document root (used to expand id-ref's) * \param[in] xml_obj XML element containing blocks of nvpair elements * \param[in] set_name Element name to identify nvpair blocks * \param[in] node_hash Node attributes to use when evaluating rules * \param[out] hash Where to store extracted name/value pairs * \param[in] always_first If not NULL, process block with this ID first * \param[in] overwrite Whether to replace existing values with same name * \param[in] now Time to use when evaluating rules * \param[out] next_change If not NULL, set to when evaluation will change */ void pe_unpack_nvpairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, GHashTable *node_hash, GHashTable *hash, const char *always_first, gboolean overwrite, crm_time_t *now, crm_time_t *next_change) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .now = now, .match_data = NULL, .rsc_data = NULL, .op_data = NULL }; pe_eval_nvpairs(top, xml_obj, set_name, &rule_data, hash, always_first, overwrite, next_change); } /*! * \brief Expand any regular expression submatches (%0-%9) in a string * * \param[in] string String possibly containing submatch variables * \param[in] match_data If not NULL, regular expression matches * * \return Newly allocated string identical to \p string with submatches * expanded, or NULL if there were no matches */ char * pe_expand_re_matches(const char *string, const pe_re_match_data_t *match_data) { size_t len = 0; int i; const char *p, *last_match_index; char *p_dst, *result = NULL; if (pcmk__str_empty(string) || !match_data) { return NULL; } p = last_match_index = string; while (*p) { if (*p == '%' && *(p + 1) && isdigit(*(p + 1))) { i = *(p + 1) - '0'; if (match_data->nregs >= i && match_data->pmatch[i].rm_so != -1 && match_data->pmatch[i].rm_eo > match_data->pmatch[i].rm_so) { len += p - last_match_index + (match_data->pmatch[i].rm_eo - match_data->pmatch[i].rm_so); last_match_index = p + 2; } p++; } p++; } len += p - last_match_index + 1; /* FIXME: Excessive? */ if (len - 1 <= 0) { return NULL; } p_dst = result = calloc(1, len); p = string; while (*p) { if (*p == '%' && *(p + 1) && isdigit(*(p + 1))) { i = *(p + 1) - '0'; if (match_data->nregs >= i && match_data->pmatch[i].rm_so != -1 && match_data->pmatch[i].rm_eo > match_data->pmatch[i].rm_so) { /* rm_eo can be equal to rm_so, but then there is nothing to do */ int match_len = match_data->pmatch[i].rm_eo - match_data->pmatch[i].rm_so; memcpy(p_dst, match_data->string + match_data->pmatch[i].rm_so, match_len); p_dst += match_len; } p++; } else { *(p_dst) = *(p); p_dst++; } p++; } return result; } /*! * \brief Evaluate rules * * \param[in,out] ruleset XML possibly containing rule sub-elements * \param[in] rule_data * \param[out] next_change If not NULL, set to when evaluation will change * * \return TRUE if there are no rules or */ gboolean pe_eval_rules(xmlNode *ruleset, const pe_rule_eval_data_t *rule_data, crm_time_t *next_change) { // If there are no rules, pass by default gboolean ruleset_default = TRUE; for (xmlNode *rule = first_named_child(ruleset, PCMK_XE_RULE); rule != NULL; rule = crm_next_same_xml(rule)) { ruleset_default = FALSE; if (pe_eval_expr(rule, rule_data, next_change)) { /* Only the deprecated PCMK__XE_LIFETIME element of location * constraints may contain more than one rule at the top level -- * the schema limits a block of nvpairs to a single top-level rule. * So, this effectively means that a lifetime is active if any rule * it contains is active. */ return TRUE; } } return ruleset_default; } /*! * \brief Evaluate all of a rule's expressions * * \param[in,out] rule XML containing a rule definition or its id-ref * \param[in] rule_data Matching parameters to check against rule * \param[out] next_change If not NULL, set to when evaluation will change * * \return TRUE if \p rule_data passes \p rule, otherwise FALSE */ gboolean pe_eval_expr(xmlNode *rule, const pe_rule_eval_data_t *rule_data, crm_time_t *next_change) { xmlNode *expr = NULL; gboolean test = TRUE; gboolean empty = TRUE; gboolean passed = TRUE; gboolean do_and = TRUE; const char *value = NULL; rule = expand_idref(rule, NULL); if (rule == NULL) { return FALSE; // Not possible with schema validation enabled } value = crm_element_value(rule, PCMK_XA_BOOLEAN_OP); if (pcmk__str_eq(value, PCMK_VALUE_OR, pcmk__str_casei)) { do_and = FALSE; passed = FALSE; } else if (!pcmk__str_eq(value, PCMK_VALUE_AND, pcmk__str_null_matches|pcmk__str_casei)) { pcmk__config_warn("Rule %s has invalid " PCMK_XA_BOOLEAN_OP " value '%s', using default ('" PCMK_VALUE_AND "')", pcmk__xe_id(rule), value); } crm_trace("Testing rule %s", pcmk__xe_id(rule)); for (expr = pcmk__xe_first_child(rule); expr != NULL; expr = pcmk__xe_next(expr)) { test = pe_eval_subexpr(expr, rule_data, next_change); empty = FALSE; if (test && do_and == FALSE) { crm_trace("Expression %s/%s passed", pcmk__xe_id(rule), pcmk__xe_id(expr)); return TRUE; } else if (test == FALSE && do_and) { crm_trace("Expression %s/%s failed", pcmk__xe_id(rule), pcmk__xe_id(expr)); return FALSE; } } if (empty) { pcmk__config_err("Ignoring rule %s because it contains no expressions", pcmk__xe_id(rule)); } crm_trace("Rule %s %s", pcmk__xe_id(rule), passed ? "passed" : "failed"); return passed; } /*! * \brief Evaluate a single rule expression, including any subexpressions * * \param[in,out] expr XML containing a rule expression * \param[in] rule_data Matching parameters to check against expression * \param[out] next_change If not NULL, set to when evaluation will change * * \return TRUE if \p rule_data passes \p expr, otherwise FALSE */ gboolean pe_eval_subexpr(xmlNode *expr, const pe_rule_eval_data_t *rule_data, crm_time_t *next_change) { gboolean accept = FALSE; const char *uname = NULL; switch (pcmk__expression_type(expr)) { case pcmk__subexpr_rule: accept = pe_eval_expr(expr, rule_data, next_change); break; case pcmk__subexpr_attribute: case pcmk__subexpr_location: /* these expressions can never succeed if there is * no node to compare with */ if (rule_data->node_hash != NULL) { accept = pe__eval_attr_expr(expr, rule_data); } break; case pcmk__subexpr_datetime: switch (pe__eval_date_expr(expr, rule_data->now, next_change)) { case pcmk_rc_within_range: case pcmk_rc_ok: accept = TRUE; break; default: accept = FALSE; break; } break; case pcmk__subexpr_resource: accept = pe__eval_rsc_expr(expr, rule_data); break; case pcmk__subexpr_operation: accept = pe__eval_op_expr(expr, rule_data); break; default: CRM_CHECK(FALSE /* bad type */ , return FALSE); accept = FALSE; } if (rule_data->node_hash) { uname = g_hash_table_lookup(rule_data->node_hash, CRM_ATTR_UNAME); } crm_trace("Expression %s %s on %s", pcmk__xe_id(expr), (accept? "passed" : "failed"), pcmk__s(uname, "all nodes")); return accept; } /*! * \internal * \brief Compare two values in a rule's node attribute expression * * \param[in] l_val Value on left-hand side of comparison * \param[in] r_val Value on right-hand side of comparison * \param[in] type How to interpret the values (allowed values: * \c PCMK_VALUE_STRING, \c PCMK_VALUE_INTEGER, * \c PCMK_VALUE_NUMBER, \c PCMK_VALUE_VERSION, \c NULL) * \param[in] op Type of comparison * * \return -1 if (l_val < r_val), * 0 if (l_val == r_val), * 1 if (l_val > r_val) */ static int compare_attr_expr_vals(const char *l_val, const char *r_val, const char *type, const char *op) { int cmp = 0; if (l_val != NULL && r_val != NULL) { if (type == NULL) { if (pcmk__strcase_any_of(op, PCMK_VALUE_LT, PCMK_VALUE_LTE, PCMK_VALUE_GT, PCMK_VALUE_GTE, NULL)) { if (pcmk__char_in_any_str('.', l_val, r_val, NULL)) { type = PCMK_VALUE_NUMBER; } else { type = PCMK_VALUE_INTEGER; } } else { type = PCMK_VALUE_STRING; } crm_trace("Defaulting to %s based comparison for '%s' op", type, op); } if (pcmk__str_eq(type, PCMK_VALUE_STRING, pcmk__str_casei)) { cmp = strcasecmp(l_val, r_val); } else if (pcmk__str_eq(type, PCMK_VALUE_INTEGER, pcmk__str_casei)) { long long l_val_num; int rc1 = pcmk__scan_ll(l_val, &l_val_num, 0LL); long long r_val_num; int rc2 = pcmk__scan_ll(r_val, &r_val_num, 0LL); if ((rc1 == pcmk_rc_ok) && (rc2 == pcmk_rc_ok)) { if (l_val_num < r_val_num) { cmp = -1; } else if (l_val_num > r_val_num) { cmp = 1; } else { cmp = 0; } } else { crm_debug("Integer parse error. Comparing %s and %s as strings", l_val, r_val); cmp = compare_attr_expr_vals(l_val, r_val, PCMK_VALUE_STRING, op); } } else if (pcmk__str_eq(type, PCMK_VALUE_NUMBER, pcmk__str_casei)) { double l_val_num; double r_val_num; int rc1 = pcmk__scan_double(l_val, &l_val_num, NULL, NULL); int rc2 = pcmk__scan_double(r_val, &r_val_num, NULL, NULL); if (rc1 == pcmk_rc_ok && rc2 == pcmk_rc_ok) { if (l_val_num < r_val_num) { cmp = -1; } else if (l_val_num > r_val_num) { cmp = 1; } else { cmp = 0; } } else { crm_debug("Floating-point parse error. Comparing %s and %s as " "strings", l_val, r_val); cmp = compare_attr_expr_vals(l_val, r_val, PCMK_VALUE_STRING, op); } } else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_casei)) { cmp = compare_version(l_val, r_val); } } else if (l_val == NULL && r_val == NULL) { cmp = 0; } else if (r_val == NULL) { cmp = 1; } else { // l_val == NULL && r_val != NULL cmp = -1; } return cmp; } /*! * \internal * \brief Check whether an attribute expression evaluates to \c true * * \param[in] l_val Value on left-hand side of comparison * \param[in] r_val Value on right-hand side of comparison * \param[in] type How to interpret the values (allowed values: * \c PCMK_VALUE_STRING, \c PCMK_VALUE_INTEGER, * \c PCMK_VALUE_NUMBER, \c PCMK_VALUE_VERSION, \c NULL) * \param[in] op Type of comparison. * * \return \c true if expression evaluates to \c true, \c false * otherwise */ static bool accept_attr_expr(const char *l_val, const char *r_val, const char *type, const char *op) { int cmp; if (pcmk__str_eq(op, PCMK_VALUE_DEFINED, pcmk__str_casei)) { return (l_val != NULL); } else if (pcmk__str_eq(op, PCMK_VALUE_NOT_DEFINED, pcmk__str_casei)) { return (l_val == NULL); } cmp = compare_attr_expr_vals(l_val, r_val, type, op); if (pcmk__str_eq(op, PCMK_VALUE_EQ, pcmk__str_casei)) { return (cmp == 0); } else if (pcmk__str_eq(op, PCMK_VALUE_NE, pcmk__str_casei)) { return (cmp != 0); } else if (l_val == NULL || r_val == NULL) { // The comparison is meaningless from this point on return false; } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) { return (cmp < 0); } else if (pcmk__str_eq(op, PCMK_VALUE_LTE, pcmk__str_casei)) { return (cmp <= 0); } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) { return (cmp > 0); } else if (pcmk__str_eq(op, PCMK_VALUE_GTE, pcmk__str_casei)) { return (cmp >= 0); } return false; // Should never reach this point } /*! * \internal * \brief Get correct value according to \c PCMK_XA_VALUE_SOURCE * * \param[in] expr_id Rule expression ID (for logging only) * \param[in] value value given in rule expression * \param[in] value_source \c PCMK_XA_VALUE_SOURCE given in rule expressions * \param[in] match_data If not NULL, resource back-references and params */ static const char * expand_value_source(const char *expr_id, const char *value, const char *value_source, const pe_match_data_t *match_data) { GHashTable *table = NULL; if (pcmk__str_empty(value)) { return NULL; // value_source is irrelevant } else if (pcmk__str_eq(value_source, PCMK_VALUE_PARAM, pcmk__str_casei)) { table = match_data->params; } else if (pcmk__str_eq(value_source, PCMK_VALUE_META, pcmk__str_casei)) { table = match_data->meta; } else { // literal if (!pcmk__str_eq(value_source, PCMK_VALUE_LITERAL, pcmk__str_null_matches|pcmk__str_casei)) { pcmk__config_warn("Expression %s has invalid " PCMK_XA_VALUE_SOURCE " value '%s', using default " "('" PCMK_VALUE_LITERAL "')", pcmk__s(expr_id, "without ID"), value_source); } return value; } if (table == NULL) { return NULL; } return (const char *) g_hash_table_lookup(table, value); } /*! * \internal * \brief Evaluate a node attribute expression based on #uname, #id, #kind, * or a generic node attribute * * \param[in] expr XML of rule expression * \param[in] rule_data The match_data and node_hash members are used * * \return TRUE if rule_data satisfies the expression, FALSE otherwise */ gboolean pe__eval_attr_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) { gboolean attr_allocated = FALSE; const char *h_val = NULL; const char *id = pcmk__xe_id(expr); const char *attr = crm_element_value(expr, PCMK_XA_ATTRIBUTE); const char *op = crm_element_value(expr, PCMK_XA_OPERATION); const char *type = crm_element_value(expr, PCMK_XA_TYPE); const char *value = crm_element_value(expr, PCMK_XA_VALUE); const char *value_source = crm_element_value(expr, PCMK_XA_VALUE_SOURCE); if (attr == NULL) { pcmk__config_err("Expression %s invalid: " PCMK_XA_ATTRIBUTE " not specified", pcmk__s(id, "without ID")); return FALSE; } else if (op == NULL) { pcmk__config_err("Expression %s invalid: " PCMK_XA_OPERATION " not specified", pcmk__s(id, "without ID")); return FALSE; } if (rule_data->match_data != NULL) { // Expand any regular expression submatches (%0-%9) in attribute name if (rule_data->match_data->re != NULL) { char *resolved_attr = pe_expand_re_matches(attr, rule_data->match_data->re); if (resolved_attr != NULL) { attr = (const char *) resolved_attr; attr_allocated = TRUE; } } // Get value appropriate to PCMK_XA_VALUE_SOURCE value = expand_value_source(id, value, value_source, rule_data->match_data); } if (rule_data->node_hash != NULL) { h_val = (const char *)g_hash_table_lookup(rule_data->node_hash, attr); } if (attr_allocated) { free((char *)attr); attr = NULL; } return accept_attr_expr(h_val, value, type, op); } /*! * \internal * \brief Evaluate a date_expression * * \param[in] expr XML of rule expression * \param[in] now Time to use for evaluation * \param[out] next_change If not NULL, set to when evaluation will change * * \return Standard Pacemaker return code */ int pe__eval_date_expr(const xmlNode *expr, const crm_time_t *now, crm_time_t *next_change) { crm_time_t *start = NULL; crm_time_t *end = NULL; const char *value = NULL; const char *op = crm_element_value(expr, PCMK_XA_OPERATION); xmlNode *duration_spec = NULL; xmlNode *date_spec = NULL; // "undetermined" will also be returned for parsing errors int rc = pcmk_rc_undetermined; crm_trace("Testing expression: %s", pcmk__xe_id(expr)); duration_spec = first_named_child(expr, PCMK_XE_DURATION); date_spec = first_named_child(expr, PCMK_XE_DATE_SPEC); value = crm_element_value(expr, PCMK_XA_START); if (value != NULL) { start = crm_time_new(value); } value = crm_element_value(expr, PCMK_XA_END); if (value != NULL) { end = crm_time_new(value); } if (start != NULL && end == NULL && duration_spec != NULL) { end = parse_xml_duration(start, duration_spec); } if (pcmk__str_eq(op, "in_range", pcmk__str_null_matches | pcmk__str_casei)) { if ((start == NULL) && (end == NULL)) { // in_range requires at least one of start or end } else if ((start != NULL) && (crm_time_compare(now, start) < 0)) { rc = pcmk_rc_before_range; crm_time_set_if_earlier(next_change, start); } else if ((end != NULL) && (crm_time_compare(now, end) > 0)) { rc = pcmk_rc_after_range; } else { rc = pcmk_rc_within_range; if (end && next_change) { // Evaluation doesn't change until second after end crm_time_add_seconds(end, 1); crm_time_set_if_earlier(next_change, end); } } } else if (pcmk__str_eq(op, PCMK_VALUE_DATE_SPEC, pcmk__str_casei)) { rc = pcmk__evaluate_date_spec(date_spec, now); // @TODO set next_change appropriately } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) { if (start == NULL) { // gt requires start } else if (crm_time_compare(now, start) > 0) { rc = pcmk_rc_within_range; } else { rc = pcmk_rc_before_range; // Evaluation doesn't change until second after start crm_time_add_seconds(start, 1); crm_time_set_if_earlier(next_change, start); } } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) { if (end == NULL) { // lt requires end } else if (crm_time_compare(now, end) < 0) { rc = pcmk_rc_within_range; crm_time_set_if_earlier(next_change, end); } else { rc = pcmk_rc_after_range; } } crm_time_free(start); crm_time_free(end); return rc; } gboolean pe__eval_op_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) { const char *name = crm_element_value(expr, PCMK_XA_NAME); const char *interval_s = crm_element_value(expr, PCMK_META_INTERVAL); guint interval_ms = 0U; crm_trace("Testing op_defaults expression: %s", pcmk__xe_id(expr)); if (rule_data->op_data == NULL) { crm_trace("No operations data provided"); return FALSE; } if (pcmk_parse_interval_spec(interval_s, &interval_ms) != pcmk_rc_ok) { crm_trace("Could not parse interval: %s", interval_s); return FALSE; } if ((interval_s != NULL) && (interval_ms != rule_data->op_data->interval)) { crm_trace("Interval doesn't match: %d != %d", interval_ms, rule_data->op_data->interval); return FALSE; } if (!pcmk__str_eq(name, rule_data->op_data->op_name, pcmk__str_none)) { crm_trace("Name doesn't match: %s != %s", name, rule_data->op_data->op_name); return FALSE; } return TRUE; } gboolean pe__eval_rsc_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) { const char *class = crm_element_value(expr, PCMK_XA_CLASS); const char *provider = crm_element_value(expr, PCMK_XA_PROVIDER); const char *type = crm_element_value(expr, PCMK_XA_TYPE); crm_trace("Testing rsc_defaults expression: %s", pcmk__xe_id(expr)); if (rule_data->rsc_data == NULL) { crm_trace("No resource data provided"); return FALSE; } if (class != NULL && !pcmk__str_eq(class, rule_data->rsc_data->standard, pcmk__str_none)) { crm_trace("Class doesn't match: %s != %s", class, rule_data->rsc_data->standard); return FALSE; } if ((provider == NULL && rule_data->rsc_data->provider != NULL) || (provider != NULL && rule_data->rsc_data->provider == NULL) || !pcmk__str_eq(provider, rule_data->rsc_data->provider, pcmk__str_none)) { crm_trace("Provider doesn't match: %s != %s", provider, rule_data->rsc_data->provider); return FALSE; } if (type != NULL && !pcmk__str_eq(type, rule_data->rsc_data->agent, pcmk__str_none)) { crm_trace("Agent doesn't match: %s != %s", type, rule_data->rsc_data->agent); return FALSE; } return TRUE; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include gboolean test_ruleset(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now) { return pe_evaluate_rules(ruleset, node_hash, now, NULL); } gboolean test_rule(xmlNode * rule, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now) { return pe_test_rule(rule, node_hash, role, now, NULL, NULL); } gboolean pe_test_rule_re(xmlNode * rule, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now, pe_re_match_data_t * re_match_data) { pe_match_data_t match_data = { .re = re_match_data, .params = NULL, .meta = NULL, }; return pe_test_rule(rule, node_hash, role, now, NULL, &match_data); } gboolean pe_test_rule_full(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, pe_match_data_t *match_data) { return pe_test_rule(rule, node_hash, role, now, NULL, match_data); } gboolean test_expression(xmlNode * expr, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now) { return pe_test_expression(expr, node_hash, role, now, NULL, NULL); } gboolean pe_test_expression_re(xmlNode * expr, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now, pe_re_match_data_t * re_match_data) { pe_match_data_t match_data = { .re = re_match_data, .params = NULL, .meta = NULL, }; return pe_test_expression(expr, node_hash, role, now, NULL, &match_data); } gboolean pe_test_expression_full(xmlNode *expr, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, pe_match_data_t *match_data) { return pe_test_expression(expr, node_hash, role, now, NULL, match_data); } void unpack_instance_attributes(xmlNode *top, xmlNode *xml_obj, const char *set_name, GHashTable *node_hash, GHashTable *hash, const char *always_first, gboolean overwrite, crm_time_t *now) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .now = now, .match_data = NULL, .rsc_data = NULL, .op_data = NULL }; pe_eval_nvpairs(top, xml_obj, set_name, &rule_data, hash, always_first, overwrite, NULL); } enum expression_type find_expression_type(xmlNode *expr) { return pcmk__expression_type(expr); } // LCOV_EXCL_STOP // End deprecated API