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);
(8) Event path: Condition "rc != pcmk_rc_ok", taking false branch.
265  	    ADD_COMPONENT(pcmk__time_weeks);
(9) Event path: Condition "rc != pcmk_rc_ok", taking false branch.
266  	    ADD_COMPONENT(pcmk__time_days);
(10) Event path: Condition "rc != pcmk_rc_ok", taking false branch.
267  	    ADD_COMPONENT(pcmk__time_hours);
CID (unavailable; MK=5361f95bd0029573c9e0c4b5aa4ab9e4) (#6 of 7): Inconsistent C union access (INCONSISTENT_UNION_ACCESS):
(11) Event path: Condition "rc != pcmk_rc_ok", taking true branch.
(12) Event path: Condition "pcmk__config_error_handler == NULL", taking true branch.
(13) Event path: Falling through to end of if statement.
(14) Event assign_union_field: The union field "in" of "_pp" is written.
(15) Event inconsistent_union_field_access: In "_pp.out", the union field used: "out" is inconsistent with the field most recently stored: "in".
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, NULL);
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