Coverage Report

Created: 2024-10-12 00:30

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