Coverage Report

Created: 2026-10-30 06:56

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