Page Menu
Home
ClusterLabs Projects
Search
Configure Global Search
Log In
Files
F1842202
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
52 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/common/rules.c b/lib/common/rules.c
index 9a2546fd00..021ca505cd 100644
--- a/lib/common/rules.c
+++ b/lib/common/rules.c
@@ -1,1476 +1,1467 @@
/*
* 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 <crm_internal.h>
#include <stdio.h> // NULL, size_t
#include <stdbool.h> // bool
#include <ctype.h> // isdigit()
#include <regex.h> // regmatch_t
#include <stdint.h> // uint32_t
#include <inttypes.h> // PRIu32
#include <glib.h> // gboolean, FALSE
#include <libxml/tree.h> // xmlNode
#include <crm/common/scheduler.h>
#include <crm/common/iso8601_internal.h>
#include <crm/common/nvpair_internal.h>
#include <crm/common/scheduler_internal.h>
#include "crmcommon_private.h"
/*!
* \internal
* \brief Get the condition type corresponding to given condition XML
*
* \param[in] condition Rule condition XML
*
* \return Condition type corresponding to \p condition
*/
enum expression_type
pcmk__condition_type(const xmlNode *condition)
{
const char *name = NULL;
// Expression types based on element name
if (pcmk__xe_is(condition, PCMK_XE_DATE_EXPRESSION)) {
return pcmk__condition_datetime;
} else if (pcmk__xe_is(condition, PCMK_XE_RSC_EXPRESSION)) {
return pcmk__condition_resource;
} else if (pcmk__xe_is(condition, PCMK_XE_OP_EXPRESSION)) {
return pcmk__condition_operation;
} else if (pcmk__xe_is(condition, PCMK_XE_RULE)) {
return pcmk__condition_rule;
} else if (!pcmk__xe_is(condition, PCMK_XE_EXPRESSION)) {
return pcmk__condition_unknown;
}
// Expression types based on node attribute name
name = crm_element_value(condition, PCMK_XA_ATTRIBUTE);
if (pcmk__str_any_of(name, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID,
NULL)) {
return pcmk__condition_location;
}
return pcmk__condition_attribute;
}
/*!
* \internal
* \brief Get parent XML element's ID for logging purposes
*
* \param[in] xml XML of a subelement
*
* \return ID of \p xml's parent for logging purposes (guaranteed non-NULL)
*/
static const char *
loggable_parent_id(const xmlNode *xml)
{
// Default if called without parent (likely for unit testing)
const char *parent_id = "implied";
if ((xml != NULL) && (xml->parent != NULL)) {
parent_id = pcmk__xe_id(xml->parent);
if (parent_id == NULL) { // Not possible with schema validation enabled
parent_id = "without ID";
}
}
return parent_id;
}
/*!
* \internal
* \brief Get the moon phase corresponding to a given date/time
*
* \param[in] now Date/time to get moon phase for
*
* \return Phase of the moon corresponding to \p now, where 0 is the new moon
* and 7 is the full moon
* \deprecated This feature has been deprecated since 2.1.6.
*/
static int
phase_of_the_moon(const crm_time_t *now)
{
/* As per the nethack rules:
* - A moon period is 29.53058 days ~= 30
* - A year is 365.2422 days
* - Number of days moon phase advances on first day of year compared to
* preceding year is (365.2422 - 12 * 29.53058) ~= 11
* - Number of years until same phases fall on the same days of the month
* is 18.6 ~= 19
* - Moon phase on first day of year (epact) ~= (11 * (year%19) + 29) % 30
* (29 as initial condition)
* - Current phase in days = first day phase + days elapsed in year
* - 6 moons ~= 177 days ~= 8 reported phases * 22 (+ 11/22 for rounding)
*/
uint32_t epact, diy, goldn;
uint32_t y;
crm_time_get_ordinal(now, &y, &diy);
goldn = (y % 19) + 1;
epact = (11 * goldn + 18) % 30;
if (((epact == 25) && (goldn > 11)) || (epact == 24)) {
epact++;
}
return (((((diy + epact) * 6) + 11) % 177) / 22) & 7;
}
/*!
* \internal
* \brief Check an integer value against a range from a date specification
*
* \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to check
* \param[in] id XML ID of parent date expression for logging purposes
* \param[in] attr Name of XML attribute with range to check against
* \param[in] value Value to compare against range
*
* \return Standard Pacemaker return code (specifically, pcmk_rc_before_range,
* pcmk_rc_after_range, or pcmk_rc_ok to indicate that result is either
* within range or undetermined)
* \note We return pcmk_rc_ok for an undetermined result so we can continue
* checking the next range attribute.
*/
static int
check_range(const xmlNode *date_spec, const char *id, const char *attr,
uint32_t value)
{
int rc = pcmk_rc_ok;
const char *range = crm_element_value(date_spec, attr);
long long low, high;
if (range == NULL) {
// Attribute not present
} else if (pcmk__parse_ll_range(range, &low, &high) != pcmk_rc_ok) {
// Invalid range
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "
"as not passing because '%s' is not a valid range "
"for " PCMK_XE_DATE_SPEC " attribute %s",
id, range, attr);
rc = pcmk_rc_unpack_error;
} else if ((low != -1) && (value < low)) {
rc = pcmk_rc_before_range;
} else if ((high != -1) && (value > high)) {
rc = pcmk_rc_after_range;
}
crm_trace(PCMK_XE_DATE_EXPRESSION " %s: " PCMK_XE_DATE_SPEC
" %s='%s' for %" PRIu32 ": %s",
id, attr, pcmk__s(range, ""), value, pcmk_rc_str(rc));
return rc;
}
/*!
* \internal
* \brief Evaluate a date specification for a given date/time
*
* \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to evaluate
* \param[in] now Time to check
*
* \return Standard Pacemaker return code (specifically, EINVAL for NULL
* arguments, pcmk_rc_unpack_error if the specification XML is invalid,
* \c pcmk_rc_ok if \p now is within the specification's ranges, or
* \c pcmk_rc_before_range or \c pcmk_rc_after_range as appropriate)
*/
int
pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now)
{
const char *id = NULL;
const char *parent_id = loggable_parent_id(date_spec);
// Range attributes that can be specified for a PCMK_XE_DATE_SPEC element
struct range {
const char *attr;
uint32_t value;
} ranges[] = {
{ PCMK_XA_YEARS, 0U },
{ PCMK_XA_MONTHS, 0U },
{ PCMK_XA_MONTHDAYS, 0U },
{ PCMK_XA_HOURS, 0U },
{ PCMK_XA_MINUTES, 0U },
{ PCMK_XA_SECONDS, 0U },
{ PCMK_XA_YEARDAYS, 0U },
{ PCMK_XA_WEEKYEARS, 0U },
{ PCMK_XA_WEEKS, 0U },
{ PCMK_XA_WEEKDAYS, 0U },
{ PCMK__XA_MOON, 0U },
};
if ((date_spec == NULL) || (now == NULL)) {
return EINVAL;
}
// Get specification ID (for logging)
id = pcmk__xe_id(date_spec);
if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because " PCMK_XE_DATE_SPEC
" subelement has no " PCMK_XA_ID, parent_id);
return pcmk_rc_unpack_error;
}
// Year, month, day
crm_time_get_gregorian(now, &(ranges[0].value), &(ranges[1].value),
&(ranges[2].value));
// Hour, minute, second
crm_time_get_timeofday(now, &(ranges[3].value), &(ranges[4].value),
&(ranges[5].value));
// Year (redundant) and day of year
crm_time_get_ordinal(now, &(ranges[0].value), &(ranges[6].value));
// Week year, week of week year, day of week
crm_time_get_isoweek(now, &(ranges[7].value), &(ranges[8].value),
&(ranges[9].value));
// Moon phase (deprecated)
ranges[10].value = phase_of_the_moon(now);
if (crm_element_value(date_spec, PCMK__XA_MOON) != NULL) {
pcmk__config_warn("Support for '" PCMK__XA_MOON "' in "
PCMK_XE_DATE_SPEC " elements (such as %s) is "
"deprecated and will be removed in a future release "
"of Pacemaker", id);
}
for (int i = 0; i < PCMK__NELEM(ranges); ++i) {
int rc = check_range(date_spec, parent_id, ranges[i].attr,
ranges[i].value);
if (rc != pcmk_rc_ok) {
return rc;
}
}
// All specified ranges passed, or none were given (also considered a pass)
return pcmk_rc_ok;
}
#define ADD_COMPONENT(component) do { \
int rc = pcmk__add_time_from_xml(*end, component, duration); \
if (rc != pcmk_rc_ok) { \
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s " \
"as not passing because " PCMK_XE_DURATION \
" %s attribute %s is invalid", \
parent_id, id, \
pcmk__time_component_attr(component)); \
crm_time_free(*end); \
*end = NULL; \
return rc; \
} \
} while (0)
/*!
* \internal
* \brief Given a duration and a start time, calculate the end time
*
* \param[in] duration XML of PCMK_XE_DURATION element
* \param[in] start Start time
* \param[out] end Where to store end time (\p *end must be NULL
* initially)
*
* \return Standard Pacemaker return code
* \note The caller is responsible for freeing \p *end using crm_time_free().
*/
int
pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start,
crm_time_t **end)
{
const char *id = NULL;
const char *parent_id = loggable_parent_id(duration);
if ((start == NULL) || (duration == NULL)
|| (end == NULL) || (*end != NULL)) {
return EINVAL;
}
// Get duration ID (for logging)
id = pcmk__xe_id(duration);
if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "
"as not passing because " PCMK_XE_DURATION
" subelement has no " PCMK_XA_ID, parent_id);
return pcmk_rc_unpack_error;
}
*end = pcmk_copy_time(start);
ADD_COMPONENT(pcmk__time_years);
ADD_COMPONENT(pcmk__time_months);
ADD_COMPONENT(pcmk__time_weeks);
ADD_COMPONENT(pcmk__time_days);
ADD_COMPONENT(pcmk__time_hours);
ADD_COMPONENT(pcmk__time_minutes);
ADD_COMPONENT(pcmk__time_seconds);
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Evaluate a range check for a given date/time
*
* \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element
* \param[in] id Expression ID for logging purposes
* \param[in] now Date/time to compare
* \param[in,out] next_change If not NULL, set this to when the evaluation
* will change, if known and earlier than the
* original value
*
* \return Standard Pacemaker return code
*/
static int
evaluate_in_range(const xmlNode *date_expression, const char *id,
const crm_time_t *now, crm_time_t *next_change)
{
crm_time_t *start = NULL;
crm_time_t *end = NULL;
if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START,
&start) != pcmk_rc_ok) {
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because " PCMK_XA_START " is invalid", id);
return pcmk_rc_unpack_error;
}
if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END,
&end) != pcmk_rc_ok) {
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because " PCMK_XA_END " is invalid", id);
crm_time_free(start);
return pcmk_rc_unpack_error;
}
if ((start == NULL) && (end == NULL)) {
// Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because " PCMK_VALUE_IN_RANGE
" requires at least one of " PCMK_XA_START " or "
PCMK_XA_END, id);
return pcmk_rc_unpack_error;
}
if (end == NULL) {
xmlNode *duration = pcmk__xe_first_child(date_expression,
PCMK_XE_DURATION, NULL, NULL);
if (duration != NULL) {
int rc = pcmk__unpack_duration(duration, start, &end);
if (rc != pcmk_rc_ok) {
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION
" %s as not passing because duration "
"is invalid", id);
crm_time_free(start);
return rc;
}
}
}
if ((start != NULL) && (crm_time_compare(now, start) < 0)) {
pcmk__set_time_if_earlier(next_change, start);
crm_time_free(start);
crm_time_free(end);
return pcmk_rc_before_range;
}
if (end != NULL) {
if (crm_time_compare(now, end) > 0) {
crm_time_free(start);
crm_time_free(end);
return pcmk_rc_after_range;
}
// Evaluation doesn't change until second after end
if (next_change != NULL) {
crm_time_add_seconds(end, 1);
pcmk__set_time_if_earlier(next_change, end);
}
}
crm_time_free(start);
crm_time_free(end);
return pcmk_rc_within_range;
}
/*!
* \internal
* \brief Evaluate a greater-than check for a given date/time
*
* \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element
* \param[in] id Expression ID for logging purposes
* \param[in] now Date/time to compare
* \param[in,out] next_change If not NULL, set this to when the evaluation
* will change, if known and earlier than the
* original value
*
* \return Standard Pacemaker return code
*/
static int
evaluate_gt(const xmlNode *date_expression, const char *id,
const crm_time_t *now, crm_time_t *next_change)
{
crm_time_t *start = NULL;
if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START,
&start) != pcmk_rc_ok) {
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because " PCMK_XA_START " is invalid",
id);
return pcmk_rc_unpack_error;
}
if (start == NULL) { // Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because " PCMK_VALUE_GT " requires "
PCMK_XA_START, id);
return pcmk_rc_unpack_error;
}
if (crm_time_compare(now, start) > 0) {
crm_time_free(start);
return pcmk_rc_within_range;
}
// Evaluation doesn't change until second after start time
crm_time_add_seconds(start, 1);
pcmk__set_time_if_earlier(next_change, start);
crm_time_free(start);
return pcmk_rc_before_range;
}
/*!
* \internal
* \brief Evaluate a less-than check for a given date/time
*
* \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element
* \param[in] id Expression ID for logging purposes
* \param[in] now Date/time to compare
* \param[in,out] next_change If not NULL, set this to when the evaluation
* will change, if known and earlier than the
* original value
*
* \return Standard Pacemaker return code
*/
static int
evaluate_lt(const xmlNode *date_expression, const char *id,
const crm_time_t *now, crm_time_t *next_change)
{
crm_time_t *end = NULL;
if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END,
&end) != pcmk_rc_ok) {
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because " PCMK_XA_END " is invalid", id);
return pcmk_rc_unpack_error;
}
if (end == NULL) { // Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because " PCMK_VALUE_GT " requires "
PCMK_XA_END, id);
return pcmk_rc_unpack_error;
}
if (crm_time_compare(now, end) < 0) {
pcmk__set_time_if_earlier(next_change, end);
crm_time_free(end);
return pcmk_rc_within_range;
}
crm_time_free(end);
return pcmk_rc_after_range;
}
/*!
* \internal
* \brief Evaluate a rule's date expression for a given date/time
*
* \param[in] date_expression XML of a PCMK_XE_DATE_EXPRESSION element
* \param[in] now Time to use for evaluation
* \param[in,out] next_change If not NULL, set this to when the evaluation
* will change, if known and earlier than the
* original value
*
* \return Standard Pacemaker return code (unlike most other evaluation
* functions, this can return either pcmk_rc_ok or pcmk_rc_within_range
* on success)
*/
int
pcmk__evaluate_date_expression(const xmlNode *date_expression,
const crm_time_t *now, crm_time_t *next_change)
{
const char *id = NULL;
const char *op = NULL;
int rc = pcmk_rc_ok;
if ((date_expression == NULL) || (now == NULL)) {
return EINVAL;
}
// Get expression ID (for logging)
id = pcmk__xe_id(date_expression);
if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " without "
PCMK_XA_ID " as not passing");
return pcmk_rc_unpack_error;
}
op = crm_element_value(date_expression, PCMK_XA_OPERATION);
if (pcmk__str_eq(op, PCMK_VALUE_IN_RANGE,
pcmk__str_null_matches|pcmk__str_casei)) {
rc = evaluate_in_range(date_expression, id, now, next_change);
} else if (pcmk__str_eq(op, PCMK_VALUE_DATE_SPEC, pcmk__str_casei)) {
xmlNode *date_spec = pcmk__xe_first_child(date_expression,
PCMK_XE_DATE_SPEC, NULL,
NULL);
if (date_spec == NULL) { // Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "
"as not passing because " PCMK_VALUE_DATE_SPEC
" operations require a " PCMK_XE_DATE_SPEC
" subelement", id);
return pcmk_rc_unpack_error;
}
// @TODO set next_change appropriately
rc = pcmk__evaluate_date_spec(date_spec, now);
} else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) {
rc = evaluate_gt(date_expression, id, now, next_change);
} else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) {
rc = evaluate_lt(date_expression, id, now, next_change);
} else { // Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION
" %s as not passing because '%s' is not a valid "
PCMK_XE_OPERATION, id, op);
return pcmk_rc_unpack_error;
}
crm_trace(PCMK_XE_DATE_EXPRESSION " %s (%s): %s (%d)",
id, op, pcmk_rc_str(rc), rc);
return rc;
}
/*!
* \internal
* \brief Go through submatches in a string, either counting how many bytes
* would be needed for the expansion, or performing the expansion,
* as requested
*
* \param[in] string String possibly containing submatch variables
* \param[in] match String that matched the regular expression
* \param[in] submatches Regular expression submatches (as set by regexec())
* \param[in] nmatches Number of entries in \p submatches[]
* \param[out] expansion If not NULL, expand string here (must be
* pre-allocated to appropriate size)
* \param[out] nbytes If not NULL, set to size needed for expansion
*
* \return true if any expansion is needed, otherwise false
*/
static bool
process_submatches(const char *string, const char *match,
const regmatch_t submatches[], int nmatches,
char *expansion, size_t *nbytes)
{
bool expanded = false;
const char *src = string;
if (nbytes != NULL) {
*nbytes = 1; // Include space for terminator
}
while (*src != '\0') {
int submatch = 0;
size_t match_len = 0;
if ((src[0] != '%') || !isdigit(src[1])) {
/* src does not point to the first character of a %N sequence,
* so expand this character as-is
*/
if (expansion != NULL) {
*expansion++ = *src;
}
if (nbytes != NULL) {
++(*nbytes);
}
++src;
continue;
}
submatch = src[1] - '0';
src += 2; // Skip over %N sequence in source string
expanded = true; // Expansion will be different from source
// Omit sequence from expansion unless it has a non-empty match
if ((nmatches <= submatch) // Not enough submatches
|| (submatches[submatch].rm_so < 0) // Pattern did not match
|| (submatches[submatch].rm_eo
<= submatches[submatch].rm_so)) { // Match was empty
continue;
}
match_len = submatches[submatch].rm_eo - submatches[submatch].rm_so;
if (nbytes != NULL) {
*nbytes += match_len;
}
if (expansion != NULL) {
memcpy(expansion, match + submatches[submatch].rm_so,
match_len);
expansion += match_len;
}
}
return expanded;
}
/*!
* \internal
* \brief Expand any regular expression submatches (%0-%9) in a string
*
* \param[in] string String possibly containing submatch variables
* \param[in] match String that matched the regular expression
* \param[in] submatches Regular expression submatches (as set by regexec())
* \param[in] nmatches Number of entries in \p submatches[]
*
* \return Newly allocated string identical to \p string with submatches
* expanded on success, or NULL if no expansions were needed
* \note The caller is responsible for freeing the result with free()
*/
char *
pcmk__replace_submatches(const char *string, const char *match,
const regmatch_t submatches[], int nmatches)
{
size_t nbytes = 0;
char *result = NULL;
if (pcmk__str_empty(string) || pcmk__str_empty(match)) {
return NULL; // Nothing to expand
}
// Calculate how much space will be needed for expanded string
if (!process_submatches(string, match, submatches, nmatches, NULL,
&nbytes)) {
return NULL; // No expansions needed
}
// Allocate enough space for expanded string
result = pcmk__assert_alloc(nbytes, sizeof(char));
// Expand submatches
(void) process_submatches(string, match, submatches, nmatches, result,
NULL);
return result;
}
/*!
* \internal
* \brief Parse a comparison type from a string
*
* \param[in] op String with comparison type (valid values are
* \c PCMK_VALUE_DEFINED, \c PCMK_VALUE_NOT_DEFINED,
* \c PCMK_VALUE_EQ, \c PCMK_VALUE_NE,
* \c PCMK_VALUE_LT, \c PCMK_VALUE_LTE,
* \c PCMK_VALUE_GT, or \c PCMK_VALUE_GTE)
*
* \return Comparison type corresponding to \p op
*/
enum pcmk__comparison
pcmk__parse_comparison(const char *op)
{
if (pcmk__str_eq(op, PCMK_VALUE_DEFINED, pcmk__str_casei)) {
return pcmk__comparison_defined;
} else if (pcmk__str_eq(op, PCMK_VALUE_NOT_DEFINED, pcmk__str_casei)) {
return pcmk__comparison_undefined;
} else if (pcmk__str_eq(op, PCMK_VALUE_EQ, pcmk__str_casei)) {
return pcmk__comparison_eq;
} else if (pcmk__str_eq(op, PCMK_VALUE_NE, pcmk__str_casei)) {
return pcmk__comparison_ne;
} else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) {
return pcmk__comparison_lt;
} else if (pcmk__str_eq(op, PCMK_VALUE_LTE, pcmk__str_casei)) {
return pcmk__comparison_lte;
} else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) {
return pcmk__comparison_gt;
} else if (pcmk__str_eq(op, PCMK_VALUE_GTE, pcmk__str_casei)) {
return pcmk__comparison_gte;
}
return pcmk__comparison_unknown;
}
/*!
* \internal
* \brief Parse a value type from a string
*
* \param[in] type String with value type (valid values are NULL,
* \c PCMK_VALUE_STRING, \c PCMK_VALUE_INTEGER,
* \c PCMK_VALUE_NUMBER, and \c PCMK_VALUE_VERSION)
* \param[in] op Operation type (used only to select default)
* \param[in] value1 First value being compared (used only to select default)
* \param[in] value2 Second value being compared (used only to select default)
*/
enum pcmk__type
pcmk__parse_type(const char *type, enum pcmk__comparison op,
const char *value1, const char *value2)
{
if (type == NULL) {
switch (op) {
case pcmk__comparison_lt:
case pcmk__comparison_lte:
case pcmk__comparison_gt:
case pcmk__comparison_gte:
if (((value1 != NULL) && (strchr(value1, '.') != NULL))
|| ((value2 != NULL) && (strchr(value2, '.') != NULL))) {
return pcmk__type_number;
}
return pcmk__type_integer;
default:
return pcmk__type_string;
}
}
if (pcmk__str_eq(type, PCMK_VALUE_STRING, pcmk__str_casei)) {
return pcmk__type_string;
} else if (pcmk__str_eq(type, PCMK_VALUE_INTEGER, pcmk__str_casei)) {
return pcmk__type_integer;
} else if (pcmk__str_eq(type, PCMK_VALUE_NUMBER, pcmk__str_casei)) {
return pcmk__type_number;
} else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_casei)) {
return pcmk__type_version;
}
return pcmk__type_unknown;
}
/*!
* \internal
* \brief Compare two strings according to a given type
*
* \param[in] value1 String with first value to compare
* \param[in] value2 String with second value to compare
* \param[in] type How to interpret the values
*
* \return Standard comparison result (a negative integer if \p value1 is
* lesser, 0 if the values are equal, and a positive integer if
* \p value1 is greater)
*/
int
pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type)
{
// NULL compares as less than non-NULL
if (value2 == NULL) {
return (value1 == NULL)? 0 : 1;
}
if (value1 == NULL) {
return -1;
}
switch (type) {
case pcmk__type_string:
return strcasecmp(value1, value2);
case pcmk__type_integer:
{
long long integer1;
long long integer2;
if ((pcmk__scan_ll(value1, &integer1, 0LL) != pcmk_rc_ok)
|| (pcmk__scan_ll(value2, &integer2, 0LL) != pcmk_rc_ok)) {
crm_warn("Comparing '%s' and '%s' as strings because "
"invalid as integers", value1, value2);
return strcasecmp(value1, value2);
}
return (integer1 < integer2)? -1 : (integer1 > integer2)? 1 : 0;
}
break;
case pcmk__type_number:
{
double num1;
double num2;
if ((pcmk__scan_double(value1, &num1, NULL, NULL) != pcmk_rc_ok)
|| (pcmk__scan_double(value2, &num2, NULL,
NULL) != pcmk_rc_ok)) {
crm_warn("Comparing '%s' and '%s' as strings because invalid as "
"numbers", value1, value2);
return strcasecmp(value1, value2);
}
return (num1 < num2)? -1 : (num1 > num2)? 1 : 0;
}
break;
case pcmk__type_version:
return compare_version(value1, value2);
default: // Invalid type
return 0;
}
}
/*!
* \internal
* \brief Parse a reference value source from a string
*
* \param[in] source String indicating reference value source
*
* \return Reference value source corresponding to \p source
*/
enum pcmk__reference_source
pcmk__parse_source(const char *source)
{
if (pcmk__str_eq(source, PCMK_VALUE_LITERAL,
pcmk__str_casei|pcmk__str_null_matches)) {
return pcmk__source_literal;
} else if (pcmk__str_eq(source, PCMK_VALUE_PARAM, pcmk__str_casei)) {
return pcmk__source_instance_attrs;
} else if (pcmk__str_eq(source, PCMK_VALUE_META, pcmk__str_casei)) {
return pcmk__source_meta_attrs;
} else {
return pcmk__source_unknown;
}
}
/*!
* \internal
* \brief Parse a boolean operator from a string
*
* \param[in] combine String indicating boolean operator
*
* \return Enumeration value corresponding to \p combine
*/
enum pcmk__combine
pcmk__parse_combine(const char *combine)
{
if (pcmk__str_eq(combine, PCMK_VALUE_AND,
pcmk__str_null_matches|pcmk__str_casei)) {
return pcmk__combine_and;
} else if (pcmk__str_eq(combine, PCMK_VALUE_OR, pcmk__str_casei)) {
return pcmk__combine_or;
} else {
return pcmk__combine_unknown;
}
}
/*!
* \internal
* \brief Get the result of a node attribute comparison for rule evaluation
*
* \param[in] actual Actual node attribute value
* \param[in] reference Node attribute value from rule (ignored for
* \p comparison of \c pcmk__comparison_defined or
* \c pcmk__comparison_undefined)
* \param[in] type How to interpret the values
* \param[in] comparison How to compare the values
*
* \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok if the
* comparison passes, and some other value if it does not)
*/
static int
evaluate_attr_comparison(const char *actual, const char *reference,
enum pcmk__type type, enum pcmk__comparison comparison)
{
int cmp = 0;
switch (comparison) {
case pcmk__comparison_defined:
return (actual != NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
case pcmk__comparison_undefined:
return (actual == NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
default:
break;
}
cmp = pcmk__cmp_by_type(actual, reference, type);
switch (comparison) {
case pcmk__comparison_eq:
return (cmp == 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
case pcmk__comparison_ne:
return (cmp != 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
default:
break;
}
if ((actual == NULL) || (reference == NULL)) {
return pcmk_rc_op_unsatisfied; // Comparison would be meaningless
}
switch (comparison) {
case pcmk__comparison_lt:
return (cmp < 0)? pcmk_rc_ok : pcmk_rc_after_range;
case pcmk__comparison_lte:
return (cmp <= 0)? pcmk_rc_ok : pcmk_rc_after_range;
case pcmk__comparison_gt:
return (cmp > 0)? pcmk_rc_ok : pcmk_rc_before_range;
case pcmk__comparison_gte:
return (cmp >= 0)? pcmk_rc_ok : pcmk_rc_before_range;
default: // Not possible with schema validation enabled
return pcmk_rc_op_unsatisfied;
}
}
/*!
* \internal
* \brief Get a reference value from a configured source
*
* \param[in] value Value given in rule expression
* \param[in] source Reference value source
* \param[in] rule_input Values used to evaluate rule criteria
*/
static const char *
value_from_source(const char *value, enum pcmk__reference_source source,
const pcmk_rule_input_t *rule_input)
{
GHashTable *table = NULL;
- if (pcmk__str_empty(value)) {
- /* @COMPAT When we can break backward compatibility, drop this block so
- * empty strings are treated as such (there should never be an empty
- * string as an instance attribute or meta-attribute name, so those will
- * get NULL anyway, but it could matter for literal comparisons)
- */
- return NULL;
- }
-
switch (source) {
case pcmk__source_literal:
return value;
case pcmk__source_instance_attrs:
table = rule_input->rsc_params;
break;
case pcmk__source_meta_attrs:
table = rule_input->rsc_meta;
break;
default:
return NULL; // Not possible
}
if (table == NULL) {
return NULL;
}
return (const char *) g_hash_table_lookup(table, value);
}
/*!
* \internal
* \brief Evaluate a node attribute rule expression
*
* \param[in] expression XML of a rule's PCMK_XE_EXPRESSION subelement
* \param[in] rule_input Values used to evaluate rule criteria
*
* \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
* passes, some other value if it does not)
*/
int
pcmk__evaluate_attr_expression(const xmlNode *expression,
const pcmk_rule_input_t *rule_input)
{
const char *id = NULL;
const char *op = NULL;
const char *attr = NULL;
const char *type_s = NULL;
const char *value = NULL;
const char *actual = NULL;
const char *source_s = NULL;
const char *reference = NULL;
char *expanded_attr = NULL;
int rc = pcmk_rc_ok;
enum pcmk__type type = pcmk__type_unknown;
enum pcmk__reference_source source = pcmk__source_unknown;
enum pcmk__comparison comparison = pcmk__comparison_unknown;
if ((expression == NULL) || (rule_input == NULL)) {
return EINVAL;
}
// Get expression ID (for logging)
id = pcmk__xe_id(expression);
if (pcmk__str_empty(id)) {
pcmk__config_err("Treating " PCMK_XE_EXPRESSION " without " PCMK_XA_ID
" as not passing");
return pcmk_rc_unpack_error;
}
/* Get name of node attribute to compare (expanding any %0-%9 to
* regular expression submatches)
*/
attr = crm_element_value(expression, PCMK_XA_ATTRIBUTE);
if (attr == NULL) {
pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
"because " PCMK_XA_ATTRIBUTE " was not specified", id);
return pcmk_rc_unpack_error;
}
expanded_attr = pcmk__replace_submatches(attr, rule_input->rsc_id,
rule_input->rsc_id_submatches,
rule_input->rsc_id_nmatches);
if (expanded_attr != NULL) {
attr = expanded_attr;
}
// Get and validate operation
op = crm_element_value(expression, PCMK_XA_OPERATION);
comparison = pcmk__parse_comparison(op);
if (comparison == pcmk__comparison_unknown) {
// Not possible with schema validation enabled
if (op == NULL) {
pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
"passing because it has no " PCMK_XA_OPERATION,
id);
} else {
pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
"passing because '%s' is not a valid "
PCMK_XA_OPERATION, id, op);
}
rc = pcmk_rc_unpack_error;
goto done;
}
// How reference value is obtained (literal, resource meta-attribute, etc.)
source_s = crm_element_value(expression, PCMK_XA_VALUE_SOURCE);
source = pcmk__parse_source(source_s);
if (source == pcmk__source_unknown) {
// Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
"because '%s' is not a valid " PCMK_XA_VALUE_SOURCE,
id, source_s);
rc = pcmk_rc_unpack_error;
goto done;
}
// Get and validate reference value
value = crm_element_value(expression, PCMK_XA_VALUE);
switch (comparison) {
case pcmk__comparison_defined:
case pcmk__comparison_undefined:
if (value != NULL) {
pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
"passing because " PCMK_XA_VALUE " is not "
"allowed when " PCMK_XA_OPERATION " is %s",
id, op);
rc = pcmk_rc_unpack_error;
goto done;
}
break;
default:
if (value == NULL) {
pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
"passing because " PCMK_XA_VALUE " is "
"required when " PCMK_XA_OPERATION " is %s",
id, op);
rc = pcmk_rc_unpack_error;
goto done;
}
+ reference = value_from_source(value, source, rule_input);
break;
}
- reference = value_from_source(value, source, rule_input);
// Get actual value of node attribute
if (rule_input->node_attrs != NULL) {
actual = g_hash_table_lookup(rule_input->node_attrs, attr);
}
// Get and validate value type (after expanding reference value)
type_s = crm_element_value(expression, PCMK_XA_TYPE);
type = pcmk__parse_type(type_s, comparison, actual, reference);
if (type == pcmk__type_unknown) {
// Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
"because '%s' is not a valid type", id, type_s);
rc = pcmk_rc_unpack_error;
goto done;
}
rc = evaluate_attr_comparison(actual, reference, type, comparison);
switch (comparison) {
case pcmk__comparison_defined:
case pcmk__comparison_undefined:
crm_trace(PCMK_XE_EXPRESSION " %s result: %s (for attribute %s %s)",
id, pcmk_rc_str(rc), attr, op);
break;
default:
crm_trace(PCMK_XE_EXPRESSION " %s result: "
"%s (attribute %s %s '%s' via %s source as %s type)",
id, pcmk_rc_str(rc), attr, op, pcmk__s(reference, ""),
pcmk__s(source_s, "default"), pcmk__s(type_s, "default"));
break;
}
done:
free(expanded_attr);
return rc;
}
/*!
* \internal
* \brief Evaluate a resource rule expression
*
* \param[in] rsc_expression XML of rule's \c PCMK_XE_RSC_EXPRESSION subelement
* \param[in] rule_input Values used to evaluate rule criteria
*
* \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
* passes, some other value if it does not)
*/
int
pcmk__evaluate_rsc_expression(const xmlNode *rsc_expression,
const pcmk_rule_input_t *rule_input)
{
const char *id = NULL;
const char *standard = NULL;
const char *provider = NULL;
const char *type = NULL;
if ((rsc_expression == NULL) || (rule_input == NULL)) {
return EINVAL;
}
// Validate XML ID
id = pcmk__xe_id(rsc_expression);
if (pcmk__str_empty(id)) {
// Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_RSC_EXPRESSION " without "
PCMK_XA_ID " as not passing");
return pcmk_rc_unpack_error;
}
// Compare resource standard
standard = crm_element_value(rsc_expression, PCMK_XA_CLASS);
if ((standard != NULL)
&& !pcmk__str_eq(standard, rule_input->rsc_standard, pcmk__str_none)) {
crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because "
"actual standard '%s' doesn't match '%s'",
id, pcmk__s(rule_input->rsc_standard, ""), standard);
return pcmk_rc_op_unsatisfied;
}
// Compare resource provider
provider = crm_element_value(rsc_expression, PCMK_XA_PROVIDER);
if ((provider != NULL)
&& !pcmk__str_eq(provider, rule_input->rsc_provider, pcmk__str_none)) {
crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because "
"actual provider '%s' doesn't match '%s'",
id, pcmk__s(rule_input->rsc_provider, ""), provider);
return pcmk_rc_op_unsatisfied;
}
// Compare resource agent type
type = crm_element_value(rsc_expression, PCMK_XA_TYPE);
if ((type != NULL)
&& !pcmk__str_eq(type, rule_input->rsc_agent, pcmk__str_none)) {
crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because "
"actual agent '%s' doesn't match '%s'",
id, pcmk__s(rule_input->rsc_agent, ""), type);
return pcmk_rc_op_unsatisfied;
}
crm_trace(PCMK_XE_RSC_EXPRESSION " %s is satisfied by %s%s%s:%s",
id, pcmk__s(standard, ""),
((provider == NULL)? "" : ":"), pcmk__s(provider, ""),
pcmk__s(type, ""));
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Evaluate an operation rule expression
*
* \param[in] op_expression XML of a rule's \c PCMK_XE_OP_EXPRESSION subelement
* \param[in] rule_input Values used to evaluate rule criteria
*
* \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
* is satisfied, some other value if it is not)
*/
int
pcmk__evaluate_op_expression(const xmlNode *op_expression,
const pcmk_rule_input_t *rule_input)
{
const char *id = NULL;
const char *name = NULL;
const char *interval_s = NULL;
guint interval_ms = 0U;
if ((op_expression == NULL) || (rule_input == NULL)) {
return EINVAL;
}
// Get operation expression ID (for logging)
id = pcmk__xe_id(op_expression);
if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " without "
PCMK_XA_ID " as not passing");
return pcmk_rc_unpack_error;
}
// Validate operation name
name = crm_element_value(op_expression, PCMK_XA_NAME);
if (name == NULL) { // Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " %s as not "
"passing because it has no " PCMK_XA_NAME, id);
return pcmk_rc_unpack_error;
}
// Validate operation interval
interval_s = crm_element_value(op_expression, PCMK_META_INTERVAL);
if (pcmk_parse_interval_spec(interval_s, &interval_ms) != pcmk_rc_ok) {
pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " %s as not "
"passing because '%s' is not a valid "
PCMK_META_INTERVAL,
id, interval_s);
return pcmk_rc_unpack_error;
}
// Compare operation name
if (!pcmk__str_eq(name, rule_input->op_name, pcmk__str_none)) {
crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because "
"actual name '%s' doesn't match '%s'",
id, pcmk__s(rule_input->op_name, ""), name);
return pcmk_rc_op_unsatisfied;
}
// Compare operation interval (unspecified interval matches all)
if ((interval_s != NULL) && (interval_ms != rule_input->op_interval_ms)) {
crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because "
"actual interval %s doesn't match %s",
id, pcmk__readable_interval(rule_input->op_interval_ms),
pcmk__readable_interval(interval_ms));
return pcmk_rc_op_unsatisfied;
}
crm_trace(PCMK_XE_OP_EXPRESSION " %s is satisfied (name %s, interval %s)",
id, name, pcmk__readable_interval(rule_input->op_interval_ms));
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Evaluate a rule condition
*
* \param[in,out] condition XML containing a rule condition (a subrule, or an
* expression of any type)
* \param[in] rule_input Values used to evaluate rule criteria
* \param[out] next_change If not NULL, set to when evaluation will change
*
* \return Standard Pacemaker return code (\c pcmk_rc_ok if the condition
* passes, some other value if it does not)
*/
int
pcmk__evaluate_condition(xmlNode *condition,
const pcmk_rule_input_t *rule_input,
crm_time_t *next_change)
{
if ((condition == NULL) || (rule_input == NULL)) {
return EINVAL;
}
switch (pcmk__condition_type(condition)) {
case pcmk__condition_rule:
return pcmk_evaluate_rule(condition, rule_input, next_change);
case pcmk__condition_attribute:
case pcmk__condition_location:
return pcmk__evaluate_attr_expression(condition, rule_input);
case pcmk__condition_datetime:
{
int rc = pcmk__evaluate_date_expression(condition,
rule_input->now,
next_change);
return (rc == pcmk_rc_within_range)? pcmk_rc_ok : rc;
}
case pcmk__condition_resource:
return pcmk__evaluate_rsc_expression(condition, rule_input);
case pcmk__condition_operation:
return pcmk__evaluate_op_expression(condition, rule_input);
default: // Not possible with schema validation enabled
pcmk__config_err("Treating rule condition %s as not passing "
"because %s is not a valid condition type",
pcmk__s(pcmk__xe_id(condition), "without ID"),
(const char *) condition->name);
return pcmk_rc_unpack_error;
}
}
/*!
* \brief Evaluate a single rule, including all its conditions
*
* \param[in,out] rule XML containing a rule definition or its id-ref
* \param[in] rule_input Values used to evaluate rule criteria
* \param[out] next_change If not NULL, set to when evaluation will change
*
* \return Standard Pacemaker return code (\c pcmk_rc_ok if the rule is
* satisfied, some other value if it is not)
*/
int
pcmk_evaluate_rule(xmlNode *rule, const pcmk_rule_input_t *rule_input,
crm_time_t *next_change)
{
bool empty = true;
int rc = pcmk_rc_ok;
const char *id = NULL;
const char *value = NULL;
enum pcmk__combine combine = pcmk__combine_unknown;
if ((rule == NULL) || (rule_input == NULL)) {
return EINVAL;
}
rule = pcmk__xe_resolve_idref(rule, NULL);
if (rule == NULL) {
// Not possible with schema validation enabled; message already logged
return pcmk_rc_unpack_error;
}
// Validate XML ID
id = pcmk__xe_id(rule);
if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_RULE " without " PCMK_XA_ID
" as not passing");
return pcmk_rc_unpack_error;
}
value = crm_element_value(rule, PCMK_XA_BOOLEAN_OP);
combine = pcmk__parse_combine(value);
switch (combine) {
case pcmk__combine_and:
// For "and", rc defaults to success (reset on failure below)
break;
case pcmk__combine_or:
// For "or", rc defaults to failure (reset on success below)
rc = pcmk_rc_op_unsatisfied;
break;
default: // Not possible with schema validation enabled
pcmk__config_err("Treating " PCMK_XE_RULE " %s as not passing "
"because '%s' is not a valid " PCMK_XA_BOOLEAN_OP,
id, value);
return pcmk_rc_unpack_error;
}
// Evaluate each condition
for (xmlNode *condition = pcmk__xe_first_child(rule, NULL, NULL, NULL);
condition != NULL; condition = pcmk__xe_next(condition)) {
empty = false;
if (pcmk__evaluate_condition(condition, rule_input,
next_change) == pcmk_rc_ok) {
if (combine == pcmk__combine_or) {
rc = pcmk_rc_ok; // Any pass is final for "or"
break;
}
} else if (combine == pcmk__combine_and) {
rc = pcmk_rc_op_unsatisfied; // Any failure is final for "and"
break;
}
}
if (empty) { // Not possible with schema validation enabled
pcmk__config_warn("Ignoring rule %s because it contains no conditions",
id);
rc = pcmk_rc_ok;
}
crm_trace("Rule %s is %ssatisfied", id, ((rc == pcmk_rc_ok)? "" : "not "));
return rc;
}
/*!
* \internal
* \brief Evaluate all rules contained within an element
*
* \param[in,out] xml XML element possibly containing rule subelements
* \param[in] rule_input Values used to evaluate rule criteria
* \param[out] next_change If not NULL, set to when evaluation will change
*
* \return Standard Pacemaker return code (pcmk_rc_ok if there are no contained
* rules or any contained rule passes, otherwise the result of the last
* rule)
* \deprecated On code paths leading to this function, the schema allows
* multiple top-level rules only in the deprecated lifetime element
* of location constraints. The code also allows multiple top-level
* rules when unpacking attribute sets, but this is deprecated and
* already prevented by schema validation. This function can be
* dropped when support for those is dropped.
*/
int
pcmk__evaluate_rules(xmlNode *xml, const pcmk_rule_input_t *rule_input,
crm_time_t *next_change)
{
// If there are no rules, pass by default
int rc = pcmk_rc_ok;
bool have_rule = false;
for (xmlNode *rule = pcmk__xe_first_child(xml, PCMK_XE_RULE, NULL, NULL);
rule != NULL; rule = pcmk__xe_next_same(rule)) {
if (have_rule) {
pcmk__warn_once(pcmk__wo_multiple_rules,
"Support for multiple top-level rules is "
"deprecated (replace with a single rule containing "
"the existing rules with " PCMK_XA_BOOLEAN_OP
"set to " PCMK_VALUE_OR " instead)");
} else {
have_rule = true;
}
rc = pcmk_evaluate_rule(rule, rule_input, next_change);
if (rc == pcmk_rc_ok) {
break;
}
}
return rc;
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 23, 2:13 PM (1 d, 27 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1018769
Default Alt Text
(52 KB)
Attached To
Mode
rP Pacemaker
Attached
Detach File
Event Timeline
Log In to Comment