1 /*
2 * Copyright 2004-2026 the Pacemaker project contributors
3 *
4 * The version control history for this file may have further details.
5 *
6 * This source code is licensed under the GNU Lesser General Public License
7 * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
8 */
9
10 #include <crm_internal.h>
11
12 #include <stdio.h> // NULL, size_t
13 #include <stdbool.h> // bool
14 #include <ctype.h> // isdigit()
15 #include <regex.h> // regmatch_t
16 #include <stdint.h> // uint32_t
17 #include <inttypes.h> // PRIu32
18 #include <glib.h> // gboolean, FALSE
19 #include <libxml/tree.h> // xmlNode
20
21 #include <crm/common/scheduler.h>
22
23 #include "crmcommon_private.h"
24
25 /*!
26 * \internal
27 * \brief Get the condition type corresponding to given condition XML
28 *
29 * \param[in] condition Rule condition XML
30 *
31 * \return Condition type corresponding to \p condition
32 */
33 enum expression_type
34 pcmk__condition_type(const xmlNode *condition)
35 {
36 const char *name = NULL;
37
38 // Expression types based on element name
39
40 if (pcmk__xe_is(condition, PCMK_XE_DATE_EXPRESSION)) {
41 return pcmk__condition_datetime;
42
43 } else if (pcmk__xe_is(condition, PCMK_XE_RSC_EXPRESSION)) {
44 return pcmk__condition_resource;
45
46 } else if (pcmk__xe_is(condition, PCMK_XE_OP_EXPRESSION)) {
47 return pcmk__condition_operation;
48
49 } else if (pcmk__xe_is(condition, PCMK_XE_RULE)) {
50 return pcmk__condition_rule;
51
52 } else if (!pcmk__xe_is(condition, PCMK_XE_EXPRESSION)) {
53 return pcmk__condition_unknown;
54 }
55
56 // Expression types based on node attribute name
57
58 name = pcmk__xe_get(condition, PCMK_XA_ATTRIBUTE);
59
60 if (pcmk__str_any_of(name, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID,
61 NULL)) {
62 return pcmk__condition_location;
63 }
64
65 return pcmk__condition_attribute;
66 }
67
68 /*!
69 * \internal
70 * \brief Get parent XML element's ID for logging purposes
71 *
72 * \param[in] xml XML of a subelement
73 *
74 * \return ID of \p xml's parent for logging purposes (guaranteed non-NULL)
75 */
76 static const char *
77 loggable_parent_id(const xmlNode *xml)
78 {
79 // Default if called without parent (likely for unit testing)
80 const char *parent_id = "implied";
81
82 if ((xml != NULL) && (xml->parent != NULL)) {
83 parent_id = pcmk__xe_id(xml->parent);
84 if (parent_id == NULL) { // Not possible with schema validation enabled
85 parent_id = "without ID";
86 }
87 }
88 return parent_id;
89 }
90
91 /*!
92 * \internal
93 * \brief Check an integer value against a range from a date specification
94 *
95 * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to check
96 * \param[in] id XML ID of parent date expression for logging purposes
97 * \param[in] attr Name of XML attribute with range to check against
98 * \param[in] value Value to compare against range
99 *
100 * \return Standard Pacemaker return code (specifically, pcmk_rc_before_range,
101 * pcmk_rc_after_range, or pcmk_rc_ok to indicate that result is either
102 * within range or undetermined)
103 * \note We return pcmk_rc_ok for an undetermined result so we can continue
104 * checking the next range attribute.
105 */
106 static int
107 check_range(const xmlNode *date_spec, const char *id, const char *attr,
108 uint32_t value)
109 {
110 int rc = pcmk_rc_ok;
111 const char *range = pcmk__xe_get(date_spec, attr);
112 long long low, high;
113
114 if (range == NULL) {
115 // Attribute not present
116
117 } else if (pcmk__parse_ll_range(range, &low, &high) != pcmk_rc_ok) {
118 // Invalid range
119 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "
120 "as not passing because '%s' is not a valid range "
121 "for " PCMK_XE_DATE_SPEC " attribute %s",
122 id, range, attr);
123 rc = pcmk_rc_unpack_error;
124
125 } else if ((low != -1) && (value < low)) {
126 rc = pcmk_rc_before_range;
127
128 } else if ((high != -1) && (value > high)) {
129 rc = pcmk_rc_after_range;
130 }
131
132 pcmk__trace(PCMK_XE_DATE_EXPRESSION " %s: " PCMK_XE_DATE_SPEC
133 " %s='%s' for %" PRIu32 ": %s",
134 id, attr, pcmk__s(range, ""), value, pcmk_rc_str(rc));
135 return rc;
136 }
137
138 /*!
139 * \internal
140 * \brief Evaluate a date specification for a given date/time
141 *
142 * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to evaluate
143 * \param[in] now Time to check
144 *
145 * \return Standard Pacemaker return code (specifically, EINVAL for NULL
146 * arguments, pcmk_rc_unpack_error if the specification XML is invalid,
147 * \c pcmk_rc_ok if \p now is within the specification's ranges, or
148 * \c pcmk_rc_before_range or \c pcmk_rc_after_range as appropriate)
149 */
150 int
151 pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now)
152 {
153 const char *id = NULL;
154 const char *parent_id = loggable_parent_id(date_spec);
155
156 // Range attributes that can be specified for a PCMK_XE_DATE_SPEC element
157 struct range {
158 const char *attr;
159 uint32_t value;
160 } ranges[] = {
161 { PCMK_XA_YEARS, 0U },
162 { PCMK_XA_MONTHS, 0U },
163 { PCMK_XA_MONTHDAYS, 0U },
164 { PCMK_XA_HOURS, 0U },
165 { PCMK_XA_MINUTES, 0U },
166 { PCMK_XA_SECONDS, 0U },
167 { PCMK_XA_YEARDAYS, 0U },
168 { PCMK_XA_WEEKYEARS, 0U },
169 { PCMK_XA_WEEKS, 0U },
170 { PCMK_XA_WEEKDAYS, 0U },
171 };
172
173 if ((date_spec == NULL) || (now == NULL)) {
174 return EINVAL;
175 }
176
177 // Get specification ID (for logging)
178 id = pcmk__xe_id(date_spec);
179 if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
180 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
181 "passing because " PCMK_XE_DATE_SPEC
182 " subelement has no " PCMK_XA_ID, parent_id);
183 return pcmk_rc_unpack_error;
184 }
185
186 // Year, month, day
187 crm_time_get_gregorian(now, &(ranges[0].value), &(ranges[1].value),
188 &(ranges[2].value));
189
190 // Hour, minute, second
191 crm_time_get_timeofday(now, &(ranges[3].value), &(ranges[4].value),
192 &(ranges[5].value));
193
194 // Year (redundant) and day of year
195 crm_time_get_ordinal(now, &(ranges[0].value), &(ranges[6].value));
196
197 // Week year, week of week year, day of week
198 pcmk__time_get_ywd(now, &(ranges[7].value), &(ranges[8].value),
199 &(ranges[9].value));
200
201 for (int i = 0; i < PCMK__NELEM(ranges); ++i) {
202 int rc = check_range(date_spec, parent_id, ranges[i].attr,
203 ranges[i].value);
204
205 if (rc != pcmk_rc_ok) {
206 return rc;
207 }
208 }
209
210 // All specified ranges passed, or none were given (also considered a pass)
211 return pcmk_rc_ok;
212 }
213
214 #define ADD_COMPONENT(component) do { \
215 int rc = pcmk__add_time_from_xml(*end, component, duration); \
216 if (rc != pcmk_rc_ok) { \
217 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s " \
218 "as not passing because " PCMK_XE_DURATION \
219 " %s attribute %s is invalid: %s", \
220 parent_id, id, \
221 pcmk__time_component_attr(component), \
222 pcmk_rc_str(rc)); \
223 g_clear_pointer(end, crm_time_free); \
224 return rc; \
225 } \
226 } while (0)
227
228 /*!
229 * \internal
230 * \brief Given a duration and a start time, calculate the end time
231 *
232 * \param[in] duration XML of PCMK_XE_DURATION element
233 * \param[in] start Start time
234 * \param[out] end Where to store end time (\p *end must be NULL
235 * initially)
236 *
237 * \return Standard Pacemaker return code
238 * \note The caller is responsible for freeing \p *end using crm_time_free().
239 */
240 int
241 pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start,
242 crm_time_t **end)
243 {
244 const char *id = NULL;
245 const char *parent_id = loggable_parent_id(duration);
246
|
(1) Event path: |
Condition "start == NULL", taking false branch. |
|
(2) Event path: |
Condition "duration == NULL", taking false branch. |
|
(3) Event path: |
Condition "end == NULL", taking false branch. |
|
(4) Event path: |
Condition "*end != NULL", taking false branch. |
247 if ((start == NULL) || (duration == NULL)
248 || (end == NULL) || (*end != NULL)) {
249 return EINVAL;
250 }
251
252 // Get duration ID (for logging)
253 id = pcmk__xe_id(duration);
|
(5) Event path: |
Condition "pcmk__str_empty(id)", taking false branch. |
254 if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
255 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "
256 "as not passing because " PCMK_XE_DURATION
257 " subelement has no " PCMK_XA_ID, parent_id);
258 return pcmk_rc_unpack_error;
259 }
260
261 *end = pcmk_copy_time(start);
262
|
(6) Event path: |
Condition "rc != pcmk_rc_ok", taking false branch. |
263 ADD_COMPONENT(pcmk__time_years);
|
(7) Event path: |
Condition "rc != pcmk_rc_ok", taking false branch. |
264 ADD_COMPONENT(pcmk__time_months);
|
CID (unavailable; MK=5361f95bd0029573c9e0c4b5aa4ab9e4) (#3 of 7): Inconsistent C union access (INCONSISTENT_UNION_ACCESS): |
|
(8) Event path: |
Condition "rc != pcmk_rc_ok", taking true branch. |
|
(9) Event path: |
Condition "pcmk__config_error_handler == NULL", taking true branch. |
|
(10) Event path: |
Falling through to end of if statement. |
|
(11) Event assign_union_field: |
The union field "in" of "_pp" is written. |
|
(12) Event inconsistent_union_field_access: |
In "_pp.out", the union field used: "out" is inconsistent with the field most recently stored: "in". |
265 ADD_COMPONENT(pcmk__time_weeks);
266 ADD_COMPONENT(pcmk__time_days);
267 ADD_COMPONENT(pcmk__time_hours);
268 ADD_COMPONENT(pcmk__time_minutes);
269 ADD_COMPONENT(pcmk__time_seconds);
270
271 return pcmk_rc_ok;
272 }
273
274 /*!
275 * \internal
276 * \brief Evaluate a range check for a given date/time
277 *
278 * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element
279 * \param[in] id Expression ID for logging purposes
280 * \param[in] now Date/time to compare
281 * \param[in,out] next_change If not NULL, set this to when the evaluation
282 * will change, if known and earlier than the
283 * original value
284 *
285 * \return Standard Pacemaker return code
286 */
287 static int
288 evaluate_in_range(const xmlNode *date_expression, const char *id,
289 const crm_time_t *now, crm_time_t *next_change)
290 {
291 crm_time_t *start = NULL;
292 crm_time_t *end = NULL;
293
294 if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START,
295 &start) != pcmk_rc_ok) {
296 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
297 "passing because " PCMK_XA_START " is invalid", id);
298 return pcmk_rc_unpack_error;
299 }
300
301 if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END,
302 &end) != pcmk_rc_ok) {
303 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
304 "passing because " PCMK_XA_END " is invalid", id);
305 crm_time_free(start);
306 return pcmk_rc_unpack_error;
307 }
308
309 if ((start == NULL) && (end == NULL)) {
310 // Not possible with schema validation enabled
311 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
312 "passing because " PCMK_VALUE_IN_RANGE
313 " requires at least one of " PCMK_XA_START " or "
314 PCMK_XA_END, id);
315 return pcmk_rc_unpack_error;
316 }
317
318 if (end == NULL) {
319 xmlNode *duration = pcmk__xe_first_child(date_expression,
320 PCMK_XE_DURATION, NULL, NULL);
321
322 if (duration != NULL) {
323 int rc = pcmk__unpack_duration(duration, start, &end);
324
325 if (rc != pcmk_rc_ok) {
326 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION
327 " %s as not passing because duration "
328 "is invalid", id);
329 crm_time_free(start);
330 return rc;
331 }
332 }
333 }
334
335 if ((start != NULL) && (crm_time_compare(now, start) < 0)) {
336 pcmk__set_time_if_earlier(next_change, start);
337 crm_time_free(start);
338 crm_time_free(end);
339 return pcmk_rc_before_range;
340 }
341
342 if (end != NULL) {
343 if (crm_time_compare(now, end) > 0) {
344 crm_time_free(start);
345 crm_time_free(end);
346 return pcmk_rc_after_range;
347 }
348
349 // Evaluation doesn't change until second after end
350 if (next_change != NULL) {
351 crm_time_add_seconds(end, 1);
352 pcmk__set_time_if_earlier(next_change, end);
353 }
354 }
355
356 crm_time_free(start);
357 crm_time_free(end);
358 return pcmk_rc_within_range;
359 }
360
361 /*!
362 * \internal
363 * \brief Evaluate a greater-than check for a given date/time
364 *
365 * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element
366 * \param[in] id Expression ID for logging purposes
367 * \param[in] now Date/time to compare
368 * \param[in,out] next_change If not NULL, set this to when the evaluation
369 * will change, if known and earlier than the
370 * original value
371 *
372 * \return Standard Pacemaker return code
373 */
374 static int
375 evaluate_gt(const xmlNode *date_expression, const char *id,
376 const crm_time_t *now, crm_time_t *next_change)
377 {
378 crm_time_t *start = NULL;
379
380 if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START,
381 &start) != pcmk_rc_ok) {
382 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
383 "passing because " PCMK_XA_START " is invalid",
384 id);
385 return pcmk_rc_unpack_error;
386 }
387
388 if (start == NULL) { // Not possible with schema validation enabled
389 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
390 "passing because " PCMK_VALUE_GT " requires "
391 PCMK_XA_START, id);
392 return pcmk_rc_unpack_error;
393 }
394
395 if (crm_time_compare(now, start) > 0) {
396 crm_time_free(start);
397 return pcmk_rc_within_range;
398 }
399
400 // Evaluation doesn't change until second after start time
401 crm_time_add_seconds(start, 1);
402 pcmk__set_time_if_earlier(next_change, start);
403 crm_time_free(start);
404 return pcmk_rc_before_range;
405 }
406
407 /*!
408 * \internal
409 * \brief Evaluate a less-than check for a given date/time
410 *
411 * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element
412 * \param[in] id Expression ID for logging purposes
413 * \param[in] now Date/time to compare
414 * \param[in,out] next_change If not NULL, set this to when the evaluation
415 * will change, if known and earlier than the
416 * original value
417 *
418 * \return Standard Pacemaker return code
419 */
420 static int
421 evaluate_lt(const xmlNode *date_expression, const char *id,
422 const crm_time_t *now, crm_time_t *next_change)
423 {
424 crm_time_t *end = NULL;
425
426 if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END,
427 &end) != pcmk_rc_ok) {
428 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
429 "passing because " PCMK_XA_END " is invalid", id);
430 return pcmk_rc_unpack_error;
431 }
432
433 if (end == NULL) { // Not possible with schema validation enabled
434 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
435 "passing because " PCMK_VALUE_GT " requires "
436 PCMK_XA_END, id);
437 return pcmk_rc_unpack_error;
438 }
439
440 if (crm_time_compare(now, end) < 0) {
441 pcmk__set_time_if_earlier(next_change, end);
442 crm_time_free(end);
443 return pcmk_rc_within_range;
444 }
445
446 crm_time_free(end);
447 return pcmk_rc_after_range;
448 }
449
450 /*!
451 * \internal
452 * \brief Evaluate a rule's date expression for a given date/time
453 *
454 * \param[in] date_expression XML of a PCMK_XE_DATE_EXPRESSION element
455 * \param[in] now Time to use for evaluation
456 * \param[in,out] next_change If not NULL, set this to when the evaluation
457 * will change, if known and earlier than the
458 * original value
459 *
460 * \return Standard Pacemaker return code (unlike most other evaluation
461 * functions, this can return either pcmk_rc_ok or pcmk_rc_within_range
462 * on success)
463 */
464 int
465 pcmk__evaluate_date_expression(const xmlNode *date_expression,
466 const crm_time_t *now, crm_time_t *next_change)
467 {
468 const char *id = NULL;
469 const char *op = NULL;
470 int rc = pcmk_rc_ok;
471
472 if ((date_expression == NULL) || (now == NULL)) {
473 return EINVAL;
474 }
475
476 // Get expression ID (for logging)
477 id = pcmk__xe_id(date_expression);
478 if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
479 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " without "
480 PCMK_XA_ID " as not passing");
481 return pcmk_rc_unpack_error;
482 }
483
484 op = pcmk__xe_get(date_expression, PCMK_XA_OPERATION);
485 if (pcmk__str_eq(op, PCMK_VALUE_IN_RANGE,
486 pcmk__str_null_matches|pcmk__str_casei)) {
487 rc = evaluate_in_range(date_expression, id, now, next_change);
488
489 } else if (pcmk__str_eq(op, PCMK_VALUE_DATE_SPEC, pcmk__str_casei)) {
490 xmlNode *date_spec = pcmk__xe_first_child(date_expression,
491 PCMK_XE_DATE_SPEC, NULL,
492 NULL);
493
494 if (date_spec == NULL) { // Not possible with schema validation enabled
495 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "
496 "as not passing because " PCMK_VALUE_DATE_SPEC
497 " operations require a " PCMK_XE_DATE_SPEC
498 " subelement", id);
499 return pcmk_rc_unpack_error;
500 }
501
502 // @TODO set next_change appropriately
503 rc = pcmk__evaluate_date_spec(date_spec, now);
504
505 } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) {
506 rc = evaluate_gt(date_expression, id, now, next_change);
507
508 } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) {
509 rc = evaluate_lt(date_expression, id, now, next_change);
510
511 } else { // Not possible with schema validation enabled
512 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION
513 " %s as not passing because '%s' is not a valid "
514 PCMK_XE_OPERATION, id, op);
515 return pcmk_rc_unpack_error;
516 }
517
518 pcmk__trace(PCMK_XE_DATE_EXPRESSION " %s (%s): %s (%d)", id, op,
519 pcmk_rc_str(rc), rc);
520 return rc;
521 }
522
523 /*!
524 * \internal
525 * \brief Go through submatches in a string, either counting how many bytes
526 * would be needed for the expansion, or performing the expansion,
527 * as requested
528 *
529 * \param[in] string String possibly containing submatch variables
530 * \param[in] match String that matched the regular expression
531 * \param[in] submatches Regular expression submatches (as set by regexec())
532 * \param[in] nmatches Number of entries in \p submatches[]
533 * \param[out] expansion If not NULL, expand string here (must be
534 * pre-allocated to appropriate size)
535 * \param[out] nbytes If not NULL, set to size needed for expansion
536 *
537 * \return true if any expansion is needed, otherwise false
538 */
539 static bool
540 process_submatches(const char *string, const char *match,
541 const regmatch_t submatches[], int nmatches,
542 char *expansion, size_t *nbytes)
543 {
544 bool expanded = false;
545 const char *src = string;
546
547 if (nbytes != NULL) {
548 *nbytes = 1; // Include space for terminator
549 }
550
551 while (*src != '\0') {
552 int submatch = 0;
553 size_t match_len = 0;
554
555 if ((src[0] != '%') || !isdigit(src[1])) {
556 /* src does not point to the first character of a %N sequence,
557 * so expand this character as-is
558 */
559 if (expansion != NULL) {
560 *expansion++ = *src;
561 }
562 if (nbytes != NULL) {
563 ++(*nbytes);
564 }
565 ++src;
566 continue;
567 }
568
569 submatch = src[1] - '0';
570 src += 2; // Skip over %N sequence in source string
571 expanded = true; // Expansion will be different from source
572
573 // Omit sequence from expansion unless it has a non-empty match
574 if ((nmatches <= submatch) // Not enough submatches
575 || (submatches[submatch].rm_so < 0) // Pattern did not match
576 || (submatches[submatch].rm_eo
577 <= submatches[submatch].rm_so)) { // Match was empty
578 continue;
579 }
580
581 match_len = submatches[submatch].rm_eo - submatches[submatch].rm_so;
582 if (nbytes != NULL) {
583 *nbytes += match_len;
584 }
585 if (expansion != NULL) {
586 memcpy(expansion, match + submatches[submatch].rm_so,
587 match_len);
588 expansion += match_len;
589 }
590 }
591
592 return expanded;
593 }
594
595 /*!
596 * \internal
597 * \brief Expand any regular expression submatches (%0-%9) in a string
598 *
599 * \param[in] string String possibly containing submatch variables
600 * \param[in] match String that matched the regular expression
601 * \param[in] submatches Regular expression submatches (as set by regexec())
602 * \param[in] nmatches Number of entries in \p submatches[]
603 *
604 * \return Newly allocated string identical to \p string with submatches
605 * expanded on success, or NULL if no expansions were needed
606 * \note The caller is responsible for freeing the result with free()
607 */
608 char *
609 pcmk__replace_submatches(const char *string, const char *match,
610 const regmatch_t submatches[], int nmatches)
611 {
612 size_t nbytes = 0;
613 char *result = NULL;
614
615 if (pcmk__str_empty(string) || pcmk__str_empty(match)) {
616 return NULL; // Nothing to expand
617 }
618
619 // Calculate how much space will be needed for expanded string
620 if (!process_submatches(string, match, submatches, nmatches, NULL,
621 &nbytes)) {
622 return NULL; // No expansions needed
623 }
624
625 // Allocate enough space for expanded string
626 result = pcmk__assert_alloc(nbytes, sizeof(char));
627
628 // Expand submatches
629 (void) process_submatches(string, match, submatches, nmatches, result,
630 NULL);
631 return result;
632 }
633
634 /*!
635 * \internal
636 * \brief Parse a comparison type from a string
637 *
638 * \param[in] op String with comparison type (valid values are
639 * \c PCMK_VALUE_DEFINED, \c PCMK_VALUE_NOT_DEFINED,
640 * \c PCMK_VALUE_EQ, \c PCMK_VALUE_NE,
641 * \c PCMK_VALUE_LT, \c PCMK_VALUE_LTE,
642 * \c PCMK_VALUE_GT, or \c PCMK_VALUE_GTE)
643 *
644 * \return Comparison type corresponding to \p op
645 */
646 enum pcmk__comparison
647 pcmk__parse_comparison(const char *op)
648 {
649 if (pcmk__str_eq(op, PCMK_VALUE_DEFINED, pcmk__str_casei)) {
650 return pcmk__comparison_defined;
651
652 } else if (pcmk__str_eq(op, PCMK_VALUE_NOT_DEFINED, pcmk__str_casei)) {
653 return pcmk__comparison_undefined;
654
655 } else if (pcmk__str_eq(op, PCMK_VALUE_EQ, pcmk__str_casei)) {
656 return pcmk__comparison_eq;
657
658 } else if (pcmk__str_eq(op, PCMK_VALUE_NE, pcmk__str_casei)) {
659 return pcmk__comparison_ne;
660
661 } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) {
662 return pcmk__comparison_lt;
663
664 } else if (pcmk__str_eq(op, PCMK_VALUE_LTE, pcmk__str_casei)) {
665 return pcmk__comparison_lte;
666
667 } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) {
668 return pcmk__comparison_gt;
669
670 } else if (pcmk__str_eq(op, PCMK_VALUE_GTE, pcmk__str_casei)) {
671 return pcmk__comparison_gte;
672 }
673
674 return pcmk__comparison_unknown;
675 }
676
677 /*!
678 * \internal
679 * \brief Parse a value type from a string
680 *
681 * \param[in] type String with value type (valid values are NULL,
682 * \c PCMK_VALUE_STRING, \c PCMK_VALUE_INTEGER,
683 * \c PCMK_VALUE_NUMBER, and \c PCMK_VALUE_VERSION)
684 * \param[in] op Operation type (used only to select default)
685 * \param[in] value1 First value being compared (used only to select default)
686 * \param[in] value2 Second value being compared (used only to select default)
687 */
688 enum pcmk__type
689 pcmk__parse_type(const char *type, enum pcmk__comparison op,
690 const char *value1, const char *value2)
691 {
692 if (type == NULL) {
693 switch (op) {
694 case pcmk__comparison_lt:
695 case pcmk__comparison_lte:
696 case pcmk__comparison_gt:
697 case pcmk__comparison_gte:
698 if (((value1 != NULL) && (strchr(value1, '.') != NULL))
699 || ((value2 != NULL) && (strchr(value2, '.') != NULL))) {
700 return pcmk__type_number;
701 }
702 return pcmk__type_integer;
703
704 default:
705 return pcmk__type_string;
706 }
707 }
708
709 if (pcmk__str_eq(type, PCMK_VALUE_STRING, pcmk__str_casei)) {
710 return pcmk__type_string;
711
712 } else if (pcmk__str_eq(type, PCMK_VALUE_INTEGER, pcmk__str_casei)) {
713 return pcmk__type_integer;
714
715 } else if (pcmk__str_eq(type, PCMK_VALUE_NUMBER, pcmk__str_casei)) {
716 return pcmk__type_number;
717
718 } else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_casei)) {
719 return pcmk__type_version;
720 }
721
722 return pcmk__type_unknown;
723 }
724
725 /*!
726 * \internal
727 * \brief Compare two strings according to a given type
728 *
729 * \param[in] value1 String with first value to compare
730 * \param[in] value2 String with second value to compare
731 * \param[in] type How to interpret the values
732 *
733 * \return Standard comparison result (a negative integer if \p value1 is
734 * lesser, 0 if the values are equal, and a positive integer if
735 * \p value1 is greater)
736 */
737 int
738 pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type)
739 {
740 // NULL compares as less than non-NULL
741 if (value2 == NULL) {
742 return (value1 == NULL)? 0 : 1;
743 }
744 if (value1 == NULL) {
745 return -1;
746 }
747
748 switch (type) {
749 case pcmk__type_string:
750 return strcasecmp(value1, value2);
751
752 case pcmk__type_integer:
753 {
754 long long integer1;
755 long long integer2;
756
757 if ((pcmk__scan_ll(value1, &integer1, 0LL) != pcmk_rc_ok)
758 || (pcmk__scan_ll(value2, &integer2, 0LL) != pcmk_rc_ok)) {
759 pcmk__warn("Comparing '%s' and '%s' as strings because "
760 "invalid as integers",
761 value1, value2);
762 return strcasecmp(value1, value2);
763 }
764 return (integer1 < integer2)? -1 : (integer1 > integer2)? 1 : 0;
765 }
766 break;
767
768 case pcmk__type_number:
769 {
770 double num1;
771 double num2;
772
773 if ((pcmk__scan_double(value1, &num1, NULL, NULL) != pcmk_rc_ok)
774 || (pcmk__scan_double(value2, &num2, NULL,
775 NULL) != pcmk_rc_ok)) {
776 pcmk__warn("Comparing '%s' and '%s' as strings because "
777 "invalid as numbers",
778 value1, value2);
779 return strcasecmp(value1, value2);
780 }
781 return (num1 < num2)? -1 : (num1 > num2)? 1 : 0;
782 }
783 break;
784
785 case pcmk__type_version:
786 return pcmk__compare_versions(value1, value2);
787
788 default: // Invalid type
789 return 0;
790 }
791 }
792
793 /*!
794 * \internal
795 * \brief Parse a reference value source from a string
796 *
797 * \param[in] source String indicating reference value source
798 *
799 * \return Reference value source corresponding to \p source
800 */
801 enum pcmk__reference_source
802 pcmk__parse_source(const char *source)
803 {
804 if (pcmk__str_eq(source, PCMK_VALUE_LITERAL,
805 pcmk__str_casei|pcmk__str_null_matches)) {
806 return pcmk__source_literal;
807
808 } else if (pcmk__str_eq(source, PCMK_VALUE_PARAM, pcmk__str_casei)) {
809 return pcmk__source_instance_attrs;
810
811 } else if (pcmk__str_eq(source, PCMK_VALUE_META, pcmk__str_casei)) {
812 return pcmk__source_meta_attrs;
813
814 } else {
815 return pcmk__source_unknown;
816 }
817 }
818
819 /*!
820 * \internal
821 * \brief Parse a boolean operator from a string
822 *
823 * \param[in] combine String indicating boolean operator
824 *
825 * \return Enumeration value corresponding to \p combine
826 */
827 enum pcmk__combine
828 pcmk__parse_combine(const char *combine)
829 {
830 if (pcmk__str_eq(combine, PCMK_VALUE_AND,
831 pcmk__str_null_matches|pcmk__str_casei)) {
832 return pcmk__combine_and;
833
834 } else if (pcmk__str_eq(combine, PCMK_VALUE_OR, pcmk__str_casei)) {
835 return pcmk__combine_or;
836
837 } else {
838 return pcmk__combine_unknown;
839 }
840 }
841
842 /*!
843 * \internal
844 * \brief Get the result of a node attribute comparison for rule evaluation
845 *
846 * \param[in] actual Actual node attribute value
847 * \param[in] reference Node attribute value from rule (ignored for
848 * \p comparison of \c pcmk__comparison_defined or
849 * \c pcmk__comparison_undefined)
850 * \param[in] type How to interpret the values
851 * \param[in] comparison How to compare the values
852 *
853 * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok if the
854 * comparison passes, and some other value if it does not)
855 */
856 static int
857 evaluate_attr_comparison(const char *actual, const char *reference,
858 enum pcmk__type type, enum pcmk__comparison comparison)
859 {
860 int cmp = 0;
861
862 switch (comparison) {
863 case pcmk__comparison_defined:
864 return (actual != NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
865
866 case pcmk__comparison_undefined:
867 return (actual == NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
868
869 default:
870 break;
871 }
872
873 cmp = pcmk__cmp_by_type(actual, reference, type);
874
875 switch (comparison) {
876 case pcmk__comparison_eq:
877 return (cmp == 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
878
879 case pcmk__comparison_ne:
880 return (cmp != 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
881
882 default:
883 break;
884 }
885
886 if ((actual == NULL) || (reference == NULL)) {
887 return pcmk_rc_op_unsatisfied; // Comparison would be meaningless
888 }
889
890 switch (comparison) {
891 case pcmk__comparison_lt:
892 return (cmp < 0)? pcmk_rc_ok : pcmk_rc_after_range;
893
894 case pcmk__comparison_lte:
895 return (cmp <= 0)? pcmk_rc_ok : pcmk_rc_after_range;
896
897 case pcmk__comparison_gt:
898 return (cmp > 0)? pcmk_rc_ok : pcmk_rc_before_range;
899
900 case pcmk__comparison_gte:
901 return (cmp >= 0)? pcmk_rc_ok : pcmk_rc_before_range;
902
903 default: // Not possible with schema validation enabled
904 return pcmk_rc_op_unsatisfied;
905 }
906 }
907
908 /*!
909 * \internal
910 * \brief Get a reference value from a configured source
911 *
912 * \param[in] value Value given in rule expression
913 * \param[in] source Reference value source
914 * \param[in] rule_input Values used to evaluate rule criteria
915 */
916 static const char *
917 value_from_source(const char *value, enum pcmk__reference_source source,
918 const pcmk_rule_input_t *rule_input)
919 {
920 GHashTable *table = NULL;
921
922 switch (source) {
923 case pcmk__source_literal:
924 return value;
925
926 case pcmk__source_instance_attrs:
927 table = rule_input->rsc_params;
928 break;
929
930 case pcmk__source_meta_attrs:
931 table = rule_input->rsc_meta;
932 break;
933
934 default:
935 return NULL; // Not possible
936 }
937
938 if (table == NULL) {
939 return NULL;
940 }
941 return (const char *) g_hash_table_lookup(table, value);
942 }
943
944 /*!
945 * \internal
946 * \brief Evaluate a node attribute rule expression
947 *
948 * \param[in] expression XML of a rule's PCMK_XE_EXPRESSION subelement
949 * \param[in] rule_input Values used to evaluate rule criteria
950 *
951 * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
952 * passes, some other value if it does not)
953 */
954 int
955 pcmk__evaluate_attr_expression(const xmlNode *expression,
956 const pcmk_rule_input_t *rule_input)
957 {
958 const char *id = NULL;
959 const char *op = NULL;
960 const char *attr = NULL;
961 const char *type_s = NULL;
962 const char *value = NULL;
963 const char *actual = NULL;
964 const char *source_s = NULL;
965 const char *reference = NULL;
966 char *expanded_attr = NULL;
967 int rc = pcmk_rc_ok;
968
969 enum pcmk__type type = pcmk__type_unknown;
970 enum pcmk__reference_source source = pcmk__source_unknown;
971 enum pcmk__comparison comparison = pcmk__comparison_unknown;
972
973 if ((expression == NULL) || (rule_input == NULL)) {
974 return EINVAL;
975 }
976
977 // Get expression ID (for logging)
978 id = pcmk__xe_id(expression);
979 if (pcmk__str_empty(id)) {
980 pcmk__config_err("Treating " PCMK_XE_EXPRESSION " without " PCMK_XA_ID
981 " as not passing");
982 return pcmk_rc_unpack_error;
983 }
984
985 /* Get name of node attribute to compare (expanding any %0-%9 to
986 * regular expression submatches)
987 */
988 attr = pcmk__xe_get(expression, PCMK_XA_ATTRIBUTE);
989 if (attr == NULL) {
990 pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
991 "because " PCMK_XA_ATTRIBUTE " was not specified", id);
992 return pcmk_rc_unpack_error;
993 }
994 expanded_attr = pcmk__replace_submatches(attr, rule_input->rsc_id,
995 rule_input->rsc_id_submatches,
996 rule_input->rsc_id_nmatches);
997 if (expanded_attr != NULL) {
998 attr = expanded_attr;
999 }
1000
1001 // Get and validate operation
1002 op = pcmk__xe_get(expression, PCMK_XA_OPERATION);
1003 comparison = pcmk__parse_comparison(op);
1004 if (comparison == pcmk__comparison_unknown) {
1005 // Not possible with schema validation enabled
1006 if (op == NULL) {
1007 pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
1008 "passing because it has no " PCMK_XA_OPERATION,
1009 id);
1010 } else {
1011 pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
1012 "passing because '%s' is not a valid "
1013 PCMK_XA_OPERATION, id, op);
1014 }
1015 rc = pcmk_rc_unpack_error;
1016 goto done;
1017 }
1018
1019 // How reference value is obtained (literal, resource meta-attribute, etc.)
1020 source_s = pcmk__xe_get(expression, PCMK_XA_VALUE_SOURCE);
1021 source = pcmk__parse_source(source_s);
1022 if (source == pcmk__source_unknown) {
1023 // Not possible with schema validation enabled
1024 pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
1025 "because '%s' is not a valid " PCMK_XA_VALUE_SOURCE,
1026 id, source_s);
1027 rc = pcmk_rc_unpack_error;
1028 goto done;
1029 }
1030
1031 // Get and validate reference value
1032 value = pcmk__xe_get(expression, PCMK_XA_VALUE);
1033 switch (comparison) {
1034 case pcmk__comparison_defined:
1035 case pcmk__comparison_undefined:
1036 if (value != NULL) {
1037 pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
1038 "passing because " PCMK_XA_VALUE " is not "
1039 "allowed when " PCMK_XA_OPERATION " is %s",
1040 id, op);
1041 rc = pcmk_rc_unpack_error;
1042 goto done;
1043 }
1044 break;
1045
1046 default:
1047 if (value == NULL) {
1048 pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
1049 "passing because " PCMK_XA_VALUE " is "
1050 "required when " PCMK_XA_OPERATION " is %s",
1051 id, op);
1052 rc = pcmk_rc_unpack_error;
1053 goto done;
1054 }
1055 reference = value_from_source(value, source, rule_input);
1056 break;
1057 }
1058
1059 // Get actual value of node attribute
1060 if (rule_input->node_attrs != NULL) {
1061 actual = g_hash_table_lookup(rule_input->node_attrs, attr);
1062 }
1063
1064 // Get and validate value type (after expanding reference value)
1065 type_s = pcmk__xe_get(expression, PCMK_XA_TYPE);
1066 type = pcmk__parse_type(type_s, comparison, actual, reference);
1067 if (type == pcmk__type_unknown) {
1068 // Not possible with schema validation enabled
1069 pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
1070 "because '%s' is not a valid type", id, type_s);
1071 rc = pcmk_rc_unpack_error;
1072 goto done;
1073 }
1074
1075 rc = evaluate_attr_comparison(actual, reference, type, comparison);
1076 switch (comparison) {
1077 case pcmk__comparison_defined:
1078 case pcmk__comparison_undefined:
1079 pcmk__trace(PCMK_XE_EXPRESSION " %s result: %s (for attribute %s "
1080 "%s)",
1081 id, pcmk_rc_str(rc), attr, op);
1082 break;
1083
1084 default:
1085 pcmk__trace(PCMK_XE_EXPRESSION " %s result: %s (attribute %s %s "
1086 "'%s' via %s source as %s type)",
1087 id, pcmk_rc_str(rc), attr, op, pcmk__s(reference, ""),
1088 pcmk__s(source_s, "default"),
1089 pcmk__s(type_s, "default"));
1090 break;
1091 }
1092
1093 done:
1094 free(expanded_attr);
1095 return rc;
1096 }
1097
1098 /*!
1099 * \internal
1100 * \brief Evaluate a resource rule expression
1101 *
1102 * \param[in] rsc_expression XML of rule's \c PCMK_XE_RSC_EXPRESSION subelement
1103 * \param[in] rule_input Values used to evaluate rule criteria
1104 *
1105 * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
1106 * passes, some other value if it does not)
1107 */
1108 int
1109 pcmk__evaluate_rsc_expression(const xmlNode *rsc_expression,
1110 const pcmk_rule_input_t *rule_input)
1111 {
1112 const char *id = NULL;
1113 const char *standard = NULL;
1114 const char *provider = NULL;
1115 const char *type = NULL;
1116
1117 if ((rsc_expression == NULL) || (rule_input == NULL)) {
1118 return EINVAL;
1119 }
1120
1121 // Validate XML ID
1122 id = pcmk__xe_id(rsc_expression);
1123 if (pcmk__str_empty(id)) {
1124 // Not possible with schema validation enabled
1125 pcmk__config_err("Treating " PCMK_XE_RSC_EXPRESSION " without "
1126 PCMK_XA_ID " as not passing");
1127 return pcmk_rc_unpack_error;
1128 }
1129
1130 // Compare resource standard
1131 standard = pcmk__xe_get(rsc_expression, PCMK_XA_CLASS);
1132 if ((standard != NULL)
1133 && !pcmk__str_eq(standard, rule_input->rsc_standard, pcmk__str_none)) {
1134 pcmk__trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because actual "
1135 "standard '%s' doesn't match '%s'",
1136 id, pcmk__s(rule_input->rsc_standard, ""), standard);
1137 return pcmk_rc_op_unsatisfied;
1138 }
1139
1140 // Compare resource provider
1141 provider = pcmk__xe_get(rsc_expression, PCMK_XA_PROVIDER);
1142 if ((provider != NULL)
1143 && !pcmk__str_eq(provider, rule_input->rsc_provider, pcmk__str_none)) {
1144 pcmk__trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because actual "
1145 "provider '%s' doesn't match '%s'",
1146 id, pcmk__s(rule_input->rsc_provider, ""), provider);
1147 return pcmk_rc_op_unsatisfied;
1148 }
1149
1150 // Compare resource agent type
1151 type = pcmk__xe_get(rsc_expression, PCMK_XA_TYPE);
1152 if ((type != NULL)
1153 && !pcmk__str_eq(type, rule_input->rsc_agent, pcmk__str_none)) {
1154 pcmk__trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because actual "
1155 "agent '%s' doesn't match '%s'",
1156 id, pcmk__s(rule_input->rsc_agent, ""), type);
1157 return pcmk_rc_op_unsatisfied;
1158 }
1159
1160 pcmk__trace(PCMK_XE_RSC_EXPRESSION " %s is satisfied by %s%s%s:%s", id,
1161 pcmk__s(standard, ""),
1162 ((provider != NULL)? ":" : ""), pcmk__s(provider, ""),
1163 pcmk__s(type, ""));
1164 return pcmk_rc_ok;
1165 }
1166
1167 /*!
1168 * \internal
1169 * \brief Evaluate an operation rule expression
1170 *
1171 * \param[in] op_expression XML of a rule's \c PCMK_XE_OP_EXPRESSION subelement
1172 * \param[in] rule_input Values used to evaluate rule criteria
1173 *
1174 * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
1175 * is satisfied, some other value if it is not)
1176 */
1177 int
1178 pcmk__evaluate_op_expression(const xmlNode *op_expression,
1179 const pcmk_rule_input_t *rule_input)
1180 {
1181 const char *id = NULL;
1182 const char *name = NULL;
1183 const char *interval_s = NULL;
1184 guint interval_ms = 0U;
1185
1186 if ((op_expression == NULL) || (rule_input == NULL)) {
1187 return EINVAL;
1188 }
1189
1190 // Get operation expression ID (for logging)
1191 id = pcmk__xe_id(op_expression);
1192 if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
1193 pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " without "
1194 PCMK_XA_ID " as not passing");
1195 return pcmk_rc_unpack_error;
1196 }
1197
1198 // Validate operation name
1199 name = pcmk__xe_get(op_expression, PCMK_XA_NAME);
1200 if (name == NULL) { // Not possible with schema validation enabled
1201 pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " %s as not "
1202 "passing because it has no " PCMK_XA_NAME, id);
1203 return pcmk_rc_unpack_error;
1204 }
1205
1206 // Validate operation interval
1207 interval_s = pcmk__xe_get(op_expression, PCMK_META_INTERVAL);
1208 if (pcmk_parse_interval_spec(interval_s, &interval_ms) != pcmk_rc_ok) {
1209 pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " %s as not "
1210 "passing because '%s' is not a valid "
1211 PCMK_META_INTERVAL,
1212 id, interval_s);
1213 return pcmk_rc_unpack_error;
1214 }
1215
1216 // Compare operation name
1217 if (!pcmk__str_eq(name, rule_input->op_name, pcmk__str_none)) {
1218 pcmk__trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because actual "
1219 "name '%s' doesn't match '%s'",
1220 id, pcmk__s(rule_input->op_name, ""), name);
1221 return pcmk_rc_op_unsatisfied;
1222 }
1223
1224 // Compare operation interval (unspecified interval matches all)
1225 if ((interval_s != NULL) && (interval_ms != rule_input->op_interval_ms)) {
1226 pcmk__trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because actual "
1227 "interval %s doesn't match %s",
1228 id, pcmk__readable_interval(rule_input->op_interval_ms),
1229 pcmk__readable_interval(interval_ms));
1230 return pcmk_rc_op_unsatisfied;
1231 }
1232
1233 pcmk__trace(PCMK_XE_OP_EXPRESSION " %s is satisfied (name %s, interval %s)",
1234 id, name, pcmk__readable_interval(rule_input->op_interval_ms));
1235 return pcmk_rc_ok;
1236 }
1237
1238 /*!
1239 * \internal
1240 * \brief Evaluate a rule condition
1241 *
1242 * \param[in,out] condition XML containing a rule condition (a subrule, or an
1243 * expression of any type)
1244 * \param[in] rule_input Values used to evaluate rule criteria
1245 * \param[out] next_change If not NULL, set to when evaluation will change
1246 *
1247 * \return Standard Pacemaker return code (\c pcmk_rc_ok if the condition
1248 * passes, some other value if it does not)
1249 */
1250 int
1251 pcmk__evaluate_condition(xmlNode *condition,
1252 const pcmk_rule_input_t *rule_input,
1253 crm_time_t *next_change)
1254 {
1255
1256 if ((condition == NULL) || (rule_input == NULL)) {
1257 return EINVAL;
1258 }
1259
1260 switch (pcmk__condition_type(condition)) {
1261 case pcmk__condition_rule:
1262 return pcmk_evaluate_rule(condition, rule_input, next_change);
1263
1264 case pcmk__condition_attribute:
1265 case pcmk__condition_location:
1266 return pcmk__evaluate_attr_expression(condition, rule_input);
1267
1268 case pcmk__condition_datetime:
1269 {
1270 int rc = pcmk__evaluate_date_expression(condition,
1271 rule_input->now,
1272 next_change);
1273
1274 return (rc == pcmk_rc_within_range)? pcmk_rc_ok : rc;
1275 }
1276
1277 case pcmk__condition_resource:
1278 return pcmk__evaluate_rsc_expression(condition, rule_input);
1279
1280 case pcmk__condition_operation:
1281 return pcmk__evaluate_op_expression(condition, rule_input);
1282
1283 default: // Not possible with schema validation enabled
1284 pcmk__config_err("Treating rule condition %s as not passing "
1285 "because %s is not a valid condition type",
1286 pcmk__s(pcmk__xe_id(condition), "without ID"),
1287 (const char *) condition->name);
1288 return pcmk_rc_unpack_error;
1289 }
1290 }
1291
1292 /*!
1293 * \brief Evaluate a single rule, including all its conditions
1294 *
1295 * \param[in,out] rule XML containing a rule definition or its id-ref
1296 * \param[in] rule_input Values used to evaluate rule criteria
1297 * \param[out] next_change If not NULL, set to when evaluation will change
1298 *
1299 * \return Standard Pacemaker return code (\c pcmk_rc_ok if the rule is
1300 * satisfied, some other value if it is not)
1301 */
1302 int
1303 pcmk_evaluate_rule(xmlNode *rule, const pcmk_rule_input_t *rule_input,
1304 crm_time_t *next_change)
1305 {
1306 bool empty = true;
1307 int rc = pcmk_rc_ok;
1308 const char *id = NULL;
1309 const char *value = NULL;
1310 enum pcmk__combine combine = pcmk__combine_unknown;
1311
1312 if ((rule == NULL) || (rule_input == NULL)) {
1313 return EINVAL;
1314 }
1315
1316 rule = pcmk__xe_resolve_idref(rule, rule->doc);
1317 if (rule == NULL) {
1318 // Not possible with schema validation enabled; message already logged
1319 return pcmk_rc_unpack_error;
1320 }
1321
1322 // Validate XML ID
1323 id = pcmk__xe_id(rule);
1324 if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
1325 pcmk__config_err("Treating " PCMK_XE_RULE " without " PCMK_XA_ID
1326 " as not passing");
1327 return pcmk_rc_unpack_error;
1328 }
1329
1330 value = pcmk__xe_get(rule, PCMK_XA_BOOLEAN_OP);
1331 combine = pcmk__parse_combine(value);
1332 switch (combine) {
1333 case pcmk__combine_and:
1334 // For "and", rc defaults to success (reset on failure below)
1335 break;
1336
1337 case pcmk__combine_or:
1338 // For "or", rc defaults to failure (reset on success below)
1339 rc = pcmk_rc_op_unsatisfied;
1340 break;
1341
1342 default: // Not possible with schema validation enabled
1343 pcmk__config_err("Treating " PCMK_XE_RULE " %s as not passing "
1344 "because '%s' is not a valid " PCMK_XA_BOOLEAN_OP,
1345 id, value);
1346 return pcmk_rc_unpack_error;
1347 }
1348
1349 // Evaluate each condition
1350 for (xmlNode *condition = pcmk__xe_first_child(rule, NULL, NULL, NULL);
1351 condition != NULL; condition = pcmk__xe_next(condition, NULL)) {
1352
1353 empty = false;
1354 if (pcmk__evaluate_condition(condition, rule_input,
1355 next_change) == pcmk_rc_ok) {
1356 if (combine == pcmk__combine_or) {
1357 rc = pcmk_rc_ok; // Any pass is final for "or"
1358 break;
1359 }
1360 } else if (combine == pcmk__combine_and) {
1361 rc = pcmk_rc_op_unsatisfied; // Any failure is final for "and"
1362 break;
1363 }
1364 }
1365
1366 if (empty) { // Not possible with schema validation enabled
1367 pcmk__config_warn("Ignoring rule %s because it contains no conditions",
1368 id);
1369 rc = pcmk_rc_ok;
1370 }
1371
1372 pcmk__trace("Rule %s is %ssatisfied", id,
1373 ((rc == pcmk_rc_ok)? "" : "not "));
1374 return rc;
1375 }
1376