Coverage Report

Created: 2026-09-28 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/pacemaker/lib/common/rules.c
Line
Count
Source
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
0
{
36
0
    const char *name = NULL;
37
38
    // Expression types based on element name
39
40
0
    if (pcmk__xe_is(condition, PCMK_XE_DATE_EXPRESSION)) {
41
0
        return pcmk__condition_datetime;
42
43
0
    } else if (pcmk__xe_is(condition, PCMK_XE_RSC_EXPRESSION)) {
44
0
        return pcmk__condition_resource;
45
46
0
    } else if (pcmk__xe_is(condition, PCMK_XE_OP_EXPRESSION)) {
47
0
        return pcmk__condition_operation;
48
49
0
    } else if (pcmk__xe_is(condition, PCMK_XE_RULE)) {
50
0
        return pcmk__condition_rule;
51
52
0
    } else if (!pcmk__xe_is(condition, PCMK_XE_EXPRESSION)) {
53
0
        return pcmk__condition_unknown;
54
0
    }
55
56
    // Expression types based on node attribute name
57
58
0
    name = pcmk__xe_get(condition, PCMK_XA_ATTRIBUTE);
59
60
0
    if (pcmk__str_any_of(name, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID,
61
0
                         NULL)) {
62
0
        return pcmk__condition_location;
63
0
    }
64
65
0
    return pcmk__condition_attribute;
66
0
}
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
0
{
79
    // Default if called without parent (likely for unit testing)
80
0
    const char *parent_id = "implied";
81
82
0
    if ((xml != NULL) && (xml->parent != NULL)) {
83
0
        parent_id = pcmk__xe_id(xml->parent);
84
0
        if (parent_id == NULL) { // Not possible with schema validation enabled
85
0
            parent_id = "without ID";
86
0
        }
87
0
    }
88
0
    return parent_id;
89
0
}
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
0
{
110
0
    int rc = pcmk_rc_ok;
111
0
    const char *range = pcmk__xe_get(date_spec, attr);
112
0
    long long low, high;
113
114
0
    if (range == NULL) {
115
        // Attribute not present
116
117
0
    } else if (pcmk__parse_ll_range(range, &low, &high) != pcmk_rc_ok) {
118
        // Invalid range
119
0
        pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "
120
0
                         "as not passing because '%s' is not a valid range "
121
0
                         "for " PCMK_XE_DATE_SPEC " attribute %s",
122
0
                         id, range, attr);
123
0
        rc = pcmk_rc_unpack_error;
124
125
0
    } else if ((low != -1) && (value < low)) {
126
0
        rc = pcmk_rc_before_range;
127
128
0
    } else if ((high != -1) && (value > high)) {
129
0
        rc = pcmk_rc_after_range;
130
0
    }
131
132
0
    pcmk__trace(PCMK_XE_DATE_EXPRESSION " %s: " PCMK_XE_DATE_SPEC
133
0
                " %s='%s' for %" PRIu32 ": %s",
134
0
                id, attr, pcmk__s(range, ""), value, pcmk_rc_str(rc));
135
0
    return rc;
136
0
}
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
0
{
153
0
    const char *id = NULL;
154
0
    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
0
    struct range {
158
0
        const char *attr;
159
0
        uint32_t value;
160
0
    } ranges[] = {
161
0
        { PCMK_XA_YEARS, 0U },
162
0
        { PCMK_XA_MONTHS, 0U },
163
0
        { PCMK_XA_MONTHDAYS, 0U },
164
0
        { PCMK_XA_HOURS, 0U },
165
0
        { PCMK_XA_MINUTES, 0U },
166
0
        { PCMK_XA_SECONDS, 0U },
167
0
        { PCMK_XA_YEARDAYS, 0U },
168
0
        { PCMK_XA_WEEKYEARS, 0U },
169
0
        { PCMK_XA_WEEKS, 0U },
170
0
        { PCMK_XA_WEEKDAYS, 0U },
171
0
    };
172
173
0
    if ((date_spec == NULL) || (now == NULL)) {
174
0
        return EINVAL;
175
0
    }
176
177
    // Get specification ID (for logging)
178
0
    id = pcmk__xe_id(date_spec);
179
0
    if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
180
0
        pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
181
0
                         "passing because " PCMK_XE_DATE_SPEC
182
0
                         " subelement has no " PCMK_XA_ID, parent_id);
183
0
        return pcmk_rc_unpack_error;
184
0
    }
185
186
    // Year, month, day
187
0
    pcmk__time_get_ymd(now, &ranges[0].value, &ranges[1].value,
188
0
                       &ranges[2].value);
189
190
    // Hour, minute, second
191
0
    pcmk__time_get_timeofday(now, &ranges[3].value, &ranges[4].value,
192
0
                             &ranges[5].value);
193
194
    // Day of year
195
0
    ranges[6].value = now->days;
196
197
    // Week year, week of week year, day of week
198
0
    pcmk__time_get_ywd(now, &ranges[7].value, &ranges[8].value,
199
0
                       &ranges[9].value);
200
201
0
    for (int i = 0; i < PCMK__NELEM(ranges); ++i) {
202
0
        int rc = check_range(date_spec, parent_id, ranges[i].attr,
203
0
                             ranges[i].value);
204
205
0
        if (rc != pcmk_rc_ok) {
206
0
            return rc;
207
0
        }
208
0
    }
209
210
    // All specified ranges passed, or none were given (also considered a pass)
211
0
    return pcmk_rc_ok;
212
0
}
213
214
0
#define ADD_COMPONENT(component) do {                                       \
215
0
        int rc = pcmk__add_time_from_xml(*end, component, duration);        \
216
0
        if (rc != pcmk_rc_ok) {                                             \
217
0
            pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "     \
218
0
                             "as not passing because " PCMK_XE_DURATION     \
219
0
                             " %s attribute %s is invalid: %s",             \
220
0
                             parent_id, id,                                 \
221
0
                             pcmk__time_component_attr(component),          \
222
0
                             pcmk_rc_str(rc));                              \
223
0
            g_clear_pointer(end, free);                                     \
224
0
            return rc;                                                      \
225
0
        }                                                                   \
226
0
    } 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 \c free().
239
 */
240
int
241
pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start,
242
                      crm_time_t **end)
243
0
{
244
0
    const char *id = NULL;
245
0
    const char *parent_id = loggable_parent_id(duration);
246
247
0
    if ((start == NULL) || (duration == NULL)
248
0
        || (end == NULL) || (*end != NULL)) {
249
0
        return EINVAL;
250
0
    }
251
252
    // Get duration ID (for logging)
253
0
    id = pcmk__xe_id(duration);
254
0
    if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
255
0
        pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "
256
0
                         "as not passing because " PCMK_XE_DURATION
257
0
                         " subelement has no " PCMK_XA_ID, parent_id);
258
0
        return pcmk_rc_unpack_error;
259
0
    }
260
261
0
    *end = pcmk__time_copy(start);
262
263
0
    ADD_COMPONENT(pcmk__time_years);
264
0
    ADD_COMPONENT(pcmk__time_months);
265
0
    ADD_COMPONENT(pcmk__time_weeks);
266
0
    ADD_COMPONENT(pcmk__time_days);
267
0
    ADD_COMPONENT(pcmk__time_hours);
268
0
    ADD_COMPONENT(pcmk__time_minutes);
269
0
    ADD_COMPONENT(pcmk__time_seconds);
270
271
0
    return pcmk_rc_ok;
272
0
}
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
0
{
291
0
    crm_time_t *start = NULL;
292
0
    crm_time_t *end = NULL;
293
294
0
    if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START,
295
0
                              &start) != pcmk_rc_ok) {
296
0
        pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
297
0
                         "passing because " PCMK_XA_START " is invalid", id);
298
0
        return pcmk_rc_unpack_error;
299
0
    }
300
301
0
    if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END,
302
0
                              &end) != pcmk_rc_ok) {
303
0
        pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
304
0
                         "passing because " PCMK_XA_END " is invalid", id);
305
0
        free(start);
306
0
        return pcmk_rc_unpack_error;
307
0
    }
308
309
0
    if ((start == NULL) && (end == NULL)) {
310
        // Not possible with schema validation enabled
311
0
        pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
312
0
                         "passing because " PCMK_VALUE_IN_RANGE
313
0
                         " requires at least one of " PCMK_XA_START " or "
314
0
                         PCMK_XA_END, id);
315
0
        return pcmk_rc_unpack_error;
316
0
    }
317
318
0
    if (end == NULL) {
319
0
        xmlNode *duration = pcmk__xe_first_child(date_expression,
320
0
                                                 PCMK_XE_DURATION, NULL, NULL);
321
322
0
        if (duration != NULL) {
323
0
            int rc = pcmk__unpack_duration(duration, start, &end);
324
325
0
            if (rc != pcmk_rc_ok) {
326
0
                pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION
327
0
                                 " %s as not passing because duration "
328
0
                                 "is invalid", id);
329
0
                free(start);
330
0
                return rc;
331
0
            }
332
0
        }
333
0
    }
334
335
0
    if ((start != NULL) && (pcmk__time_compare(now, start) < 0)) {
336
0
        pcmk__set_time_if_earlier(next_change, start);
337
0
        free(start);
338
0
        free(end);
339
0
        return pcmk_rc_before_range;
340
0
    }
341
342
0
    if (end != NULL) {
343
0
        if (pcmk__time_compare(now, end) > 0) {
344
0
            free(start);
345
0
            free(end);
346
0
            return pcmk_rc_after_range;
347
0
        }
348
349
        // Evaluation doesn't change until second after end
350
0
        if (next_change != NULL) {
351
0
            pcmk__time_add_seconds(end, 1);
352
0
            pcmk__set_time_if_earlier(next_change, end);
353
0
        }
354
0
    }
355
356
0
    free(start);
357
0
    free(end);
358
0
    return pcmk_rc_within_range;
359
0
}
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
0
{
378
0
    crm_time_t *start = NULL;
379
380
0
    if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START,
381
0
                              &start) != pcmk_rc_ok) {
382
0
        pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
383
0
                         "passing because " PCMK_XA_START " is invalid",
384
0
                         id);
385
0
        return pcmk_rc_unpack_error;
386
0
    }
387
388
0
    if (start == NULL) { // Not possible with schema validation enabled
389
0
        pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
390
0
                         "passing because " PCMK_VALUE_GT " requires "
391
0
                         PCMK_XA_START, id);
392
0
        return pcmk_rc_unpack_error;
393
0
    }
394
395
0
    if (pcmk__time_compare(now, start) > 0) {
396
0
        free(start);
397
0
        return pcmk_rc_within_range;
398
0
    }
399
400
    // Evaluation doesn't change until second after start time
401
0
    pcmk__time_add_seconds(start, 1);
402
0
    pcmk__set_time_if_earlier(next_change, start);
403
0
    free(start);
404
0
    return pcmk_rc_before_range;
405
0
}
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
0
{
424
0
    crm_time_t *end = NULL;
425
426
0
    if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END,
427
0
                              &end) != pcmk_rc_ok) {
428
0
        pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
429
0
                         "passing because " PCMK_XA_END " is invalid", id);
430
0
        return pcmk_rc_unpack_error;
431
0
    }
432
433
0
    if (end == NULL) { // Not possible with schema validation enabled
434
0
        pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
435
0
                         "passing because " PCMK_VALUE_GT " requires "
436
0
                         PCMK_XA_END, id);
437
0
        return pcmk_rc_unpack_error;
438
0
    }
439
440
0
    if (pcmk__time_compare(now, end) < 0) {
441
0
        pcmk__set_time_if_earlier(next_change, end);
442
0
        free(end);
443
0
        return pcmk_rc_within_range;
444
0
    }
445
446
0
    free(end);
447
0
    return pcmk_rc_after_range;
448
0
}
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
0
{
468
0
    const char *id = NULL;
469
0
    const char *op = NULL;
470
0
    int rc = pcmk_rc_ok;
471
472
0
    if ((date_expression == NULL) || (now == NULL)) {
473
0
        return EINVAL;
474
0
    }
475
476
    // Get expression ID (for logging)
477
0
    id = pcmk__xe_id(date_expression);
478
0
    if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
479
0
        pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " without "
480
0
                         PCMK_XA_ID " as not passing");
481
0
        return pcmk_rc_unpack_error;
482
0
    }
483
484
0
    op = pcmk__xe_get(date_expression, PCMK_XA_OPERATION);
485
0
    if (pcmk__str_eq(op, PCMK_VALUE_IN_RANGE,
486
0
                     pcmk__str_null_matches|pcmk__str_casei)) {
487
0
        rc = evaluate_in_range(date_expression, id, now, next_change);
488
489
0
    } else if (pcmk__str_eq(op, PCMK_VALUE_DATE_SPEC, pcmk__str_casei)) {
490
0
        xmlNode *date_spec = pcmk__xe_first_child(date_expression,
491
0
                                                  PCMK_XE_DATE_SPEC, NULL,
492
0
                                                  NULL);
493
494
0
        if (date_spec == NULL) { // Not possible with schema validation enabled
495
0
            pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "
496
0
                             "as not passing because " PCMK_VALUE_DATE_SPEC
497
0
                             " operations require a " PCMK_XE_DATE_SPEC
498
0
                             " subelement", id);
499
0
            return pcmk_rc_unpack_error;
500
0
        }
501
502
        // @TODO set next_change appropriately
503
0
        rc = pcmk__evaluate_date_spec(date_spec, now);
504
505
0
    } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) {
506
0
        rc = evaluate_gt(date_expression, id, now, next_change);
507
508
0
    } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) {
509
0
        rc = evaluate_lt(date_expression, id, now, next_change);
510
511
0
    } else { // Not possible with schema validation enabled
512
0
        pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION
513
0
                         " %s as not passing because '%s' is not a valid "
514
0
                         PCMK_XE_OPERATION, id, op);
515
0
        return pcmk_rc_unpack_error;
516
0
    }
517
518
0
    pcmk__trace(PCMK_XE_DATE_EXPRESSION " %s (%s): %s (%d)", id, op,
519
0
                pcmk_rc_str(rc), rc);
520
0
    return rc;
521
0
}
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
0
{
544
0
    bool expanded = false;
545
0
    const char *src = string;
546
547
0
    if (nbytes != NULL) {
548
0
        *nbytes = 1; // Include space for terminator
549
0
    }
550
551
0
    while (*src != '\0') {
552
0
        int submatch = 0;
553
0
        size_t match_len = 0;
554
555
0
        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
0
            if (expansion != NULL) {
560
0
                *expansion++ = *src;
561
0
            }
562
0
            if (nbytes != NULL) {
563
0
                ++(*nbytes);
564
0
            }
565
0
            ++src;
566
0
            continue;
567
0
        }
568
569
0
        submatch = src[1] - '0';
570
0
        src += 2; // Skip over %N sequence in source string
571
0
        expanded = true; // Expansion will be different from source
572
573
        // Omit sequence from expansion unless it has a non-empty match
574
0
        if ((nmatches <= submatch)                // Not enough submatches
575
0
            || (submatches[submatch].rm_so < 0)   // Pattern did not match
576
0
            || (submatches[submatch].rm_eo
577
0
                <= submatches[submatch].rm_so)) { // Match was empty
578
0
            continue;
579
0
        }
580
581
0
        match_len = submatches[submatch].rm_eo - submatches[submatch].rm_so;
582
0
        if (nbytes != NULL) {
583
0
            *nbytes += match_len;
584
0
        }
585
0
        if (expansion != NULL) {
586
0
            memcpy(expansion, match + submatches[submatch].rm_so,
587
0
                   match_len);
588
0
            expansion += match_len;
589
0
        }
590
0
    }
591
592
0
    return expanded;
593
0
}
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
0
{
612
0
    size_t nbytes = 0;
613
0
    char *result = NULL;
614
615
0
    if (pcmk__str_empty(string) || pcmk__str_empty(match)) {
616
0
        return NULL; // Nothing to expand
617
0
    }
618
619
    // Calculate how much space will be needed for expanded string
620
0
    if (!process_submatches(string, match, submatches, nmatches, NULL,
621
0
                            &nbytes)) {
622
0
        return NULL; // No expansions needed
623
0
    }
624
625
    // Allocate enough space for expanded string
626
0
    result = pcmk__assert_alloc(nbytes, sizeof(char));
627
628
    // Expand submatches
629
0
    (void) process_submatches(string, match, submatches, nmatches, result,
630
0
                              NULL);
631
0
    return result;
632
0
}
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
0
{
649
0
    if (pcmk__str_eq(op, PCMK_VALUE_DEFINED, pcmk__str_casei)) {
650
0
        return pcmk__comparison_defined;
651
652
0
    } else if (pcmk__str_eq(op, PCMK_VALUE_NOT_DEFINED, pcmk__str_casei)) {
653
0
        return pcmk__comparison_undefined;
654
655
0
    } else if (pcmk__str_eq(op, PCMK_VALUE_EQ, pcmk__str_casei)) {
656
0
        return pcmk__comparison_eq;
657
658
0
    } else if (pcmk__str_eq(op, PCMK_VALUE_NE, pcmk__str_casei)) {
659
0
        return pcmk__comparison_ne;
660
661
0
    } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) {
662
0
        return pcmk__comparison_lt;
663
664
0
    } else if (pcmk__str_eq(op, PCMK_VALUE_LTE, pcmk__str_casei)) {
665
0
        return pcmk__comparison_lte;
666
667
0
    } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) {
668
0
        return pcmk__comparison_gt;
669
670
0
    } else if (pcmk__str_eq(op, PCMK_VALUE_GTE, pcmk__str_casei)) {
671
0
        return pcmk__comparison_gte;
672
0
    }
673
674
0
    return pcmk__comparison_unknown;
675
0
}
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
0
{
692
0
    if (type == NULL) {
693
0
        switch (op) {
694
0
            case pcmk__comparison_lt:
695
0
            case pcmk__comparison_lte:
696
0
            case pcmk__comparison_gt:
697
0
            case pcmk__comparison_gte:
698
0
                if (((value1 != NULL) && (strchr(value1, '.') != NULL))
699
0
                    || ((value2 != NULL) && (strchr(value2, '.') != NULL))) {
700
0
                    return pcmk__type_number;
701
0
                }
702
0
                return pcmk__type_integer;
703
704
0
            default:
705
0
                return pcmk__type_string;
706
0
        }
707
0
    }
708
709
0
    if (pcmk__str_eq(type, PCMK_VALUE_STRING, pcmk__str_casei)) {
710
0
        return pcmk__type_string;
711
712
0
    } else if (pcmk__str_eq(type, PCMK_VALUE_INTEGER, pcmk__str_casei)) {
713
0
        return pcmk__type_integer;
714
715
0
    } else if (pcmk__str_eq(type, PCMK_VALUE_NUMBER, pcmk__str_casei)) {
716
0
        return pcmk__type_number;
717
718
0
    } else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_casei)) {
719
0
        return pcmk__type_version;
720
0
    }
721
722
0
    return pcmk__type_unknown;
723
0
}
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
0
{
740
    //  NULL compares as less than non-NULL
741
0
    if (value2 == NULL) {
742
0
        return (value1 == NULL)? 0 : 1;
743
0
    }
744
0
    if (value1 == NULL) {
745
0
        return -1;
746
0
    }
747
748
0
    switch (type) {
749
0
        case pcmk__type_string:
750
0
            return strcasecmp(value1, value2);
751
752
0
        case pcmk__type_integer:
753
0
            {
754
0
                long long integer1;
755
0
                long long integer2;
756
757
0
                if ((pcmk__scan_ll(value1, &integer1, 0LL) != pcmk_rc_ok)
758
0
                    || (pcmk__scan_ll(value2, &integer2, 0LL) != pcmk_rc_ok)) {
759
0
                    pcmk__warn("Comparing '%s' and '%s' as strings because "
760
0
                               "invalid as integers",
761
0
                               value1, value2);
762
0
                    return strcasecmp(value1, value2);
763
0
                }
764
0
                return (integer1 < integer2)? -1 : (integer1 > integer2)? 1 : 0;
765
0
            }
766
0
            break;
767
768
0
        case pcmk__type_number:
769
0
            {
770
0
                double num1;
771
0
                double num2;
772
773
0
                if ((pcmk__scan_double(value1, &num1, NULL, NULL) != pcmk_rc_ok)
774
0
                    || (pcmk__scan_double(value2, &num2, NULL,
775
0
                                          NULL) != pcmk_rc_ok)) {
776
0
                    pcmk__warn("Comparing '%s' and '%s' as strings because "
777
0
                               "invalid as numbers",
778
0
                               value1, value2);
779
0
                    return strcasecmp(value1, value2);
780
0
                }
781
0
                return (num1 < num2)? -1 : (num1 > num2)? 1 : 0;
782
0
            }
783
0
            break;
784
785
0
        case pcmk__type_version:
786
0
            return pcmk__compare_versions(value1, value2);
787
788
0
        default: // Invalid type
789
0
            return 0;
790
0
    }
791
0
}
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
0
{
804
0
    if (pcmk__str_eq(source, PCMK_VALUE_LITERAL,
805
0
                     pcmk__str_casei|pcmk__str_null_matches)) {
806
0
        return pcmk__source_literal;
807
808
0
    } else if (pcmk__str_eq(source, PCMK_VALUE_PARAM, pcmk__str_casei)) {
809
0
        return pcmk__source_instance_attrs;
810
811
0
    } else if (pcmk__str_eq(source, PCMK_VALUE_META, pcmk__str_casei)) {
812
0
        return pcmk__source_meta_attrs;
813
814
0
    } else {
815
0
        return pcmk__source_unknown;
816
0
    }
817
0
}
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
0
{
830
0
    if (pcmk__str_eq(combine, PCMK_VALUE_AND,
831
0
                     pcmk__str_null_matches|pcmk__str_casei)) {
832
0
        return pcmk__combine_and;
833
834
0
    } else if (pcmk__str_eq(combine, PCMK_VALUE_OR, pcmk__str_casei)) {
835
0
        return pcmk__combine_or;
836
837
0
    } else {
838
0
        return pcmk__combine_unknown;
839
0
    }
840
0
}
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
0
{
860
0
    int cmp = 0;
861
862
0
    switch (comparison) {
863
0
        case pcmk__comparison_defined:
864
0
            return (actual != NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
865
866
0
        case pcmk__comparison_undefined:
867
0
            return (actual == NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
868
869
0
        default:
870
0
            break;
871
0
    }
872
873
0
    cmp = pcmk__cmp_by_type(actual, reference, type);
874
875
0
    switch (comparison) {
876
0
        case pcmk__comparison_eq:
877
0
            return (cmp == 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
878
879
0
        case pcmk__comparison_ne:
880
0
            return (cmp != 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
881
882
0
        default:
883
0
            break;
884
0
    }
885
886
0
    if ((actual == NULL) || (reference == NULL)) {
887
0
        return pcmk_rc_op_unsatisfied; // Comparison would be meaningless
888
0
    }
889
890
0
    switch (comparison) {
891
0
        case pcmk__comparison_lt:
892
0
            return (cmp < 0)? pcmk_rc_ok : pcmk_rc_after_range;
893
894
0
        case pcmk__comparison_lte:
895
0
            return (cmp <= 0)? pcmk_rc_ok : pcmk_rc_after_range;
896
897
0
        case pcmk__comparison_gt:
898
0
            return (cmp > 0)? pcmk_rc_ok : pcmk_rc_before_range;
899
900
0
        case pcmk__comparison_gte:
901
0
            return (cmp >= 0)? pcmk_rc_ok : pcmk_rc_before_range;
902
903
0
        default: // Not possible with schema validation enabled
904
0
            return pcmk_rc_op_unsatisfied;
905
0
    }
906
0
}
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
0
{
920
0
    GHashTable *table = NULL;
921
922
0
    switch (source) {
923
0
        case pcmk__source_literal:
924
0
            return value;
925
926
0
        case pcmk__source_instance_attrs:
927
0
            table = rule_input->rsc_params;
928
0
            break;
929
930
0
        case pcmk__source_meta_attrs:
931
0
            table = rule_input->rsc_meta;
932
0
            break;
933
934
0
        default:
935
0
            return NULL; // Not possible
936
0
    }
937
938
0
    if (table == NULL) {
939
0
        return NULL;
940
0
    }
941
0
    return (const char *) g_hash_table_lookup(table, value);
942
0
}
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
0
{
958
0
    const char *id = NULL;
959
0
    const char *op = NULL;
960
0
    const char *attr = NULL;
961
0
    const char *type_s = NULL;
962
0
    const char *value = NULL;
963
0
    const char *actual = NULL;
964
0
    const char *source_s = NULL;
965
0
    const char *reference = NULL;
966
0
    char *expanded_attr = NULL;
967
0
    int rc = pcmk_rc_ok;
968
969
0
    enum pcmk__type type = pcmk__type_unknown;
970
0
    enum pcmk__reference_source source = pcmk__source_unknown;
971
0
    enum pcmk__comparison comparison = pcmk__comparison_unknown;
972
973
0
    if ((expression == NULL) || (rule_input == NULL)) {
974
0
        return EINVAL;
975
0
    }
976
977
    // Get expression ID (for logging)
978
0
    id = pcmk__xe_id(expression);
979
0
    if (pcmk__str_empty(id)) {
980
0
        pcmk__config_err("Treating " PCMK_XE_EXPRESSION " without " PCMK_XA_ID
981
0
                         " as not passing");
982
0
        return pcmk_rc_unpack_error;
983
0
    }
984
985
    /* Get name of node attribute to compare (expanding any %0-%9 to
986
     * regular expression submatches)
987
     */
988
0
    attr = pcmk__xe_get(expression, PCMK_XA_ATTRIBUTE);
989
0
    if (attr == NULL) {
990
0
        pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
991
0
                         "because " PCMK_XA_ATTRIBUTE " was not specified", id);
992
0
        return pcmk_rc_unpack_error;
993
0
    }
994
0
    expanded_attr = pcmk__replace_submatches(attr, rule_input->rsc_id,
995
0
                                             rule_input->rsc_id_submatches,
996
0
                                             rule_input->rsc_id_nmatches);
997
0
    if (expanded_attr != NULL) {
998
0
        attr = expanded_attr;
999
0
    }
1000
1001
    // Get and validate operation
1002
0
    op = pcmk__xe_get(expression, PCMK_XA_OPERATION);
1003
0
    comparison = pcmk__parse_comparison(op);
1004
0
    if (comparison == pcmk__comparison_unknown) {
1005
        // Not possible with schema validation enabled
1006
0
        if (op == NULL) {
1007
0
            pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
1008
0
                             "passing because it has no " PCMK_XA_OPERATION,
1009
0
                             id);
1010
0
        } else {
1011
0
            pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
1012
0
                             "passing because '%s' is not a valid "
1013
0
                             PCMK_XA_OPERATION, id, op);
1014
0
        }
1015
0
        rc = pcmk_rc_unpack_error;
1016
0
        goto done;
1017
0
    }
1018
1019
    // How reference value is obtained (literal, resource meta-attribute, etc.)
1020
0
    source_s = pcmk__xe_get(expression, PCMK_XA_VALUE_SOURCE);
1021
0
    source = pcmk__parse_source(source_s);
1022
0
    if (source == pcmk__source_unknown) {
1023
        // Not possible with schema validation enabled
1024
0
        pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
1025
0
                         "because '%s' is not a valid " PCMK_XA_VALUE_SOURCE,
1026
0
                         id, source_s);
1027
0
        rc = pcmk_rc_unpack_error;
1028
0
        goto done;
1029
0
    }
1030
1031
    // Get and validate reference value
1032
0
    value = pcmk__xe_get(expression, PCMK_XA_VALUE);
1033
0
    switch (comparison) {
1034
0
        case pcmk__comparison_defined:
1035
0
        case pcmk__comparison_undefined:
1036
0
            if (value != NULL) {
1037
0
                pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
1038
0
                                 "passing because " PCMK_XA_VALUE " is not "
1039
0
                                 "allowed when " PCMK_XA_OPERATION " is %s",
1040
0
                                 id, op);
1041
0
                rc = pcmk_rc_unpack_error;
1042
0
                goto done;
1043
0
            }
1044
0
            break;
1045
1046
0
        default:
1047
0
            if (value == NULL) {
1048
0
                pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
1049
0
                                 "passing because " PCMK_XA_VALUE " is "
1050
0
                                 "required when " PCMK_XA_OPERATION " is %s",
1051
0
                                 id, op);
1052
0
                rc = pcmk_rc_unpack_error;
1053
0
                goto done;
1054
0
            }
1055
0
            reference = value_from_source(value, source, rule_input);
1056
0
            break;
1057
0
    }
1058
1059
    // Get actual value of node attribute
1060
0
    if (rule_input->node_attrs != NULL) {
1061
0
        actual = g_hash_table_lookup(rule_input->node_attrs, attr);
1062
0
    }
1063
1064
    // Get and validate value type (after expanding reference value)
1065
0
    type_s = pcmk__xe_get(expression, PCMK_XA_TYPE);
1066
0
    type = pcmk__parse_type(type_s, comparison, actual, reference);
1067
0
    if (type == pcmk__type_unknown) {
1068
        // Not possible with schema validation enabled
1069
0
        pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
1070
0
                         "because '%s' is not a valid type", id, type_s);
1071
0
        rc = pcmk_rc_unpack_error;
1072
0
        goto done;
1073
0
    }
1074
1075
0
    rc = evaluate_attr_comparison(actual, reference, type, comparison);
1076
0
    switch (comparison) {
1077
0
        case pcmk__comparison_defined:
1078
0
        case pcmk__comparison_undefined:
1079
0
            pcmk__trace(PCMK_XE_EXPRESSION " %s result: %s (for attribute %s "
1080
0
                        "%s)",
1081
0
                        id, pcmk_rc_str(rc), attr, op);
1082
0
            break;
1083
1084
0
        default:
1085
0
            pcmk__trace(PCMK_XE_EXPRESSION " %s result: %s (attribute %s %s "
1086
0
                        "'%s' via %s source as %s type)",
1087
0
                        id, pcmk_rc_str(rc), attr, op, pcmk__s(reference, ""),
1088
0
                        pcmk__s(source_s, "default"),
1089
0
                        pcmk__s(type_s, "default"));
1090
0
            break;
1091
0
    }
1092
1093
0
done:
1094
0
    free(expanded_attr);
1095
0
    return rc;
1096
0
}
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
0
{
1112
0
    const char *id = NULL;
1113
0
    const char *standard = NULL;
1114
0
    const char *provider = NULL;
1115
0
    const char *type = NULL;
1116
1117
0
    if ((rsc_expression == NULL) || (rule_input == NULL)) {
1118
0
        return EINVAL;
1119
0
    }
1120
1121
    // Validate XML ID
1122
0
    id = pcmk__xe_id(rsc_expression);
1123
0
    if (pcmk__str_empty(id)) {
1124
        // Not possible with schema validation enabled
1125
0
        pcmk__config_err("Treating " PCMK_XE_RSC_EXPRESSION " without "
1126
0
                         PCMK_XA_ID " as not passing");
1127
0
        return pcmk_rc_unpack_error;
1128
0
    }
1129
1130
    // Compare resource standard
1131
0
    standard = pcmk__xe_get(rsc_expression, PCMK_XA_CLASS);
1132
0
    if ((standard != NULL)
1133
0
        && !pcmk__str_eq(standard, rule_input->rsc_standard, pcmk__str_none)) {
1134
0
        pcmk__trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because actual "
1135
0
                    "standard '%s' doesn't match '%s'",
1136
0
                    id, pcmk__s(rule_input->rsc_standard, ""), standard);
1137
0
        return pcmk_rc_op_unsatisfied;
1138
0
    }
1139
1140
    // Compare resource provider
1141
0
    provider = pcmk__xe_get(rsc_expression, PCMK_XA_PROVIDER);
1142
0
    if ((provider != NULL)
1143
0
        && !pcmk__str_eq(provider, rule_input->rsc_provider, pcmk__str_none)) {
1144
0
        pcmk__trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because actual "
1145
0
                    "provider '%s' doesn't match '%s'",
1146
0
                    id, pcmk__s(rule_input->rsc_provider, ""), provider);
1147
0
        return pcmk_rc_op_unsatisfied;
1148
0
    }
1149
1150
    // Compare resource agent type
1151
0
    type = pcmk__xe_get(rsc_expression, PCMK_XA_TYPE);
1152
0
    if ((type != NULL)
1153
0
        && !pcmk__str_eq(type, rule_input->rsc_agent, pcmk__str_none)) {
1154
0
        pcmk__trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because actual "
1155
0
                    "agent '%s' doesn't match '%s'",
1156
0
                    id, pcmk__s(rule_input->rsc_agent, ""), type);
1157
0
        return pcmk_rc_op_unsatisfied;
1158
0
    }
1159
1160
0
    pcmk__trace(PCMK_XE_RSC_EXPRESSION " %s is satisfied by %s%s%s:%s", id,
1161
0
                pcmk__s(standard, ""),
1162
0
                ((provider != NULL)? ":" : ""), pcmk__s(provider, ""),
1163
0
                pcmk__s(type, ""));
1164
0
    return pcmk_rc_ok;
1165
0
}
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
0
{
1181
0
    const char *id = NULL;
1182
0
    const char *name = NULL;
1183
0
    const char *interval_s = NULL;
1184
0
    unsigned int interval_ms = 0;
1185
1186
0
    if ((op_expression == NULL) || (rule_input == NULL)) {
1187
0
        return EINVAL;
1188
0
    }
1189
1190
    // Get operation expression ID (for logging)
1191
0
    id = pcmk__xe_id(op_expression);
1192
0
    if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
1193
0
        pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " without "
1194
0
                         PCMK_XA_ID " as not passing");
1195
0
        return pcmk_rc_unpack_error;
1196
0
    }
1197
1198
    // Validate operation name
1199
0
    name = pcmk__xe_get(op_expression, PCMK_XA_NAME);
1200
0
    if (name == NULL) { // Not possible with schema validation enabled
1201
0
        pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " %s as not "
1202
0
                         "passing because it has no " PCMK_XA_NAME, id);
1203
0
        return pcmk_rc_unpack_error;
1204
0
    }
1205
1206
    // Validate operation interval
1207
0
    interval_s = pcmk__xe_get(op_expression, PCMK_META_INTERVAL);
1208
0
    if (pcmk_parse_interval_spec(interval_s, &interval_ms) != pcmk_rc_ok) {
1209
0
        pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " %s as not "
1210
0
                         "passing because '%s' is not a valid "
1211
0
                         PCMK_META_INTERVAL,
1212
0
                         id, interval_s);
1213
0
        return pcmk_rc_unpack_error;
1214
0
    }
1215
1216
    // Compare operation name
1217
0
    if (!pcmk__str_eq(name, rule_input->op_name, pcmk__str_none)) {
1218
0
        pcmk__trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because actual "
1219
0
                    "name '%s' doesn't match '%s'",
1220
0
                    id, pcmk__s(rule_input->op_name, ""), name);
1221
0
        return pcmk_rc_op_unsatisfied;
1222
0
    }
1223
1224
    // Compare operation interval (unspecified interval matches all)
1225
0
    if ((interval_s != NULL) && (interval_ms != rule_input->op_interval_ms)) {
1226
0
        pcmk__trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because actual "
1227
0
                    "interval %s doesn't match %s",
1228
0
                    id, pcmk__readable_interval(rule_input->op_interval_ms),
1229
0
                    pcmk__readable_interval(interval_ms));
1230
0
        return pcmk_rc_op_unsatisfied;
1231
0
    }
1232
1233
0
    pcmk__trace(PCMK_XE_OP_EXPRESSION " %s is satisfied (name %s, interval %s)",
1234
0
                id, name, pcmk__readable_interval(rule_input->op_interval_ms));
1235
0
    return pcmk_rc_ok;
1236
0
}
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
0
{
1255
1256
0
    if ((condition == NULL) || (rule_input == NULL)) {
1257
0
        return EINVAL;
1258
0
    }
1259
1260
0
    switch (pcmk__condition_type(condition)) {
1261
0
        case pcmk__condition_rule:
1262
0
            return pcmk_evaluate_rule(condition, rule_input, next_change);
1263
1264
0
        case pcmk__condition_attribute:
1265
0
        case pcmk__condition_location:
1266
0
            return pcmk__evaluate_attr_expression(condition, rule_input);
1267
1268
0
        case pcmk__condition_datetime:
1269
0
            {
1270
0
                int rc = pcmk__evaluate_date_expression(condition,
1271
0
                                                        rule_input->now,
1272
0
                                                        next_change);
1273
1274
0
                return (rc == pcmk_rc_within_range)? pcmk_rc_ok : rc;
1275
0
            }
1276
1277
0
        case pcmk__condition_resource:
1278
0
            return pcmk__evaluate_rsc_expression(condition, rule_input);
1279
1280
0
        case pcmk__condition_operation:
1281
0
            return pcmk__evaluate_op_expression(condition, rule_input);
1282
1283
0
        default: // Not possible with schema validation enabled
1284
0
            pcmk__config_err("Treating rule condition %s as not passing "
1285
0
                             "because %s is not a valid condition type",
1286
0
                             pcmk__s(pcmk__xe_id(condition), "without ID"),
1287
0
                             (const char *) condition->name);
1288
0
            return pcmk_rc_unpack_error;
1289
0
    }
1290
0
}
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
0
{
1306
0
    bool empty = true;
1307
0
    int rc = pcmk_rc_ok;
1308
0
    const char *id = NULL;
1309
0
    const char *value = NULL;
1310
0
    enum pcmk__combine combine = pcmk__combine_unknown;
1311
1312
0
    if ((rule == NULL) || (rule_input == NULL)) {
1313
0
        return EINVAL;
1314
0
    }
1315
1316
0
    rule = pcmk__xe_resolve_idref(rule, rule->doc);
1317
0
    if (rule == NULL) {
1318
        // Not possible with schema validation enabled; message already logged
1319
0
        return pcmk_rc_unpack_error;
1320
0
    }
1321
1322
    // Validate XML ID
1323
0
    id = pcmk__xe_id(rule);
1324
0
    if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
1325
0
        pcmk__config_err("Treating " PCMK_XE_RULE " without " PCMK_XA_ID
1326
0
                         " as not passing");
1327
0
        return pcmk_rc_unpack_error;
1328
0
    }
1329
1330
0
    value = pcmk__xe_get(rule, PCMK_XA_BOOLEAN_OP);
1331
0
    combine = pcmk__parse_combine(value);
1332
0
    switch (combine) {
1333
0
        case pcmk__combine_and:
1334
            // For "and", rc defaults to success (reset on failure below)
1335
0
            break;
1336
1337
0
        case pcmk__combine_or:
1338
            // For "or", rc defaults to failure (reset on success below)
1339
0
            rc = pcmk_rc_op_unsatisfied;
1340
0
            break;
1341
1342
0
        default: // Not possible with schema validation enabled
1343
0
            pcmk__config_err("Treating " PCMK_XE_RULE " %s as not passing "
1344
0
                             "because '%s' is not a valid " PCMK_XA_BOOLEAN_OP,
1345
0
                             id, value);
1346
0
            return pcmk_rc_unpack_error;
1347
0
    }
1348
1349
    // Evaluate each condition
1350
0
    for (xmlNode *condition = pcmk__xe_first_child(rule, NULL, NULL, NULL);
1351
0
         condition != NULL; condition = pcmk__xe_next(condition, NULL)) {
1352
1353
0
        empty = false;
1354
0
        if (pcmk__evaluate_condition(condition, rule_input,
1355
0
                                     next_change) == pcmk_rc_ok) {
1356
0
            if (combine == pcmk__combine_or) {
1357
0
                rc = pcmk_rc_ok; // Any pass is final for "or"
1358
0
                break;
1359
0
            }
1360
0
        } else if (combine == pcmk__combine_and) {
1361
0
            rc = pcmk_rc_op_unsatisfied; // Any failure is final for "and"
1362
0
            break;
1363
0
        }
1364
0
    }
1365
1366
0
    if (empty) { // Not possible with schema validation enabled
1367
0
        pcmk__config_warn("Ignoring rule %s because it contains no conditions",
1368
0
                          id);
1369
0
        rc = pcmk_rc_ok;
1370
0
    }
1371
1372
0
    pcmk__trace("Rule %s is %ssatisfied", id,
1373
0
                ((rc == pcmk_rc_ok)? "" : "not "));
1374
0
    return rc;
1375
0
}