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