Page MenuHomeClusterLabs Projects

No OneTemporary

diff --git a/include/crm/common/rules.h b/include/crm/common/rules.h
index 2f3e3c9491..0ea9ccb554 100644
--- a/include/crm/common/rules.h
+++ b/include/crm/common/rules.h
@@ -1,108 +1,112 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef PCMK__CRM_COMMON_RULES__H
# define PCMK__CRM_COMMON_RULES__H
#include <glib.h> // guint, GHashTable
#include <regex.h> // regmatch_t
+#include <libxml/tree.h> // xmlNode
#include <crm/common/iso8601.h> // crm_time_t
#ifdef __cplusplus
extern "C" {
#endif
/*!
* \file
* \brief Scheduler API for rules
* \ingroup core
*/
/* Allowed subexpressions of a rule
* @COMPAT This should be made internal at an API compatibility break
*/
//!@{
//! \deprecated For Pacemaker use only
enum expression_type {
- pcmk__subexpr_unknown = 0, // Unknown subexpression type
- pcmk__subexpr_rule = 1, // Nested rule
- pcmk__subexpr_attribute = 2, // Node attribute expression
- pcmk__subexpr_location = 3, // Node location expression
- pcmk__subexpr_datetime = 5, // Date/time expression
- pcmk__subexpr_resource = 7, // Resource agent expression
- pcmk__subexpr_operation = 8, // Operation expression
+ pcmk__condition_unknown = 0, // Unknown or invalid condition
+ pcmk__condition_rule = 1, // Nested rule
+ pcmk__condition_attribute = 2, // Node attribute expression
+ pcmk__condition_location = 3, // Node location expression
+ pcmk__condition_datetime = 5, // Date/time expression
+ pcmk__condition_resource = 7, // Resource agent expression
+ pcmk__condition_operation = 8, // Operation expression
#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
- not_expr = pcmk__subexpr_unknown,
- nested_rule = pcmk__subexpr_rule,
- attr_expr = pcmk__subexpr_attribute,
- loc_expr = pcmk__subexpr_location,
+ not_expr = pcmk__condition_unknown,
+ nested_rule = pcmk__condition_rule,
+ attr_expr = pcmk__condition_attribute,
+ loc_expr = pcmk__condition_location,
role_expr = 4,
- time_expr = pcmk__subexpr_datetime,
+ time_expr = pcmk__condition_datetime,
version_expr = 6,
- rsc_expr = pcmk__subexpr_resource,
- op_expr = pcmk__subexpr_operation,
+ rsc_expr = pcmk__condition_resource,
+ op_expr = pcmk__condition_operation,
#endif
};
//!@}
//! Data used to evaluate a rule (any NULL items are ignored)
typedef struct pcmk_rule_input {
// Used to evaluate date expressions
const crm_time_t *now; //!< Current time for rule evaluation purposes
// Used to evaluate resource type expressions
const char *rsc_standard; //!< Resource standard that rule applies to
const char *rsc_provider; //!< Resource provider that rule applies to
const char *rsc_agent; //!< Resource agent that rule applies to
// Used to evaluate operation type expressions
const char *op_name; //! Operation name that rule applies to
guint op_interval_ms; //! Operation interval that rule applies to
// Remaining members are used to evaluate node attribute expressions
/*!
* Node attributes for rule evaluation purposes
*
* \note Though not const, this is used only with g_hash_table_lookup().
*/
GHashTable *node_attrs;
// Remaining members are used only within location constraint rules
/*!
* Resource parameters that can be used as the reference value source
*
* \note Though not const, this is used only with g_hash_table_lookup().
*/
GHashTable *rsc_params;
/*!
* Resource meta-attributes that can be used as the reference value source
*
* \note Though not const, this is used only with g_hash_table_lookup().
*/
GHashTable *rsc_meta;
//! Resource ID to compare against a location constraint's resource pattern
const char *rsc_id;
//! Resource pattern submatches (as set by regexec()) for rsc_id
const regmatch_t *rsc_id_submatches;
//! Number of entries in rsc_id_submatches
int rsc_id_nmatches;
} pcmk_rule_input_t;
+int pcmk_evaluate_rule(xmlNode *rule, const pcmk_rule_input_t *rule_input,
+ crm_time_t *next_change);
+
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_COMMON_RULES__H
diff --git a/include/crm/common/rules_internal.h b/include/crm/common/rules_internal.h
index c27539d37f..d536b857eb 100644
--- a/include/crm/common/rules_internal.h
+++ b/include/crm/common/rules_internal.h
@@ -1,33 +1,36 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef PCMK__CRM_COMMON_RULES_INTERNAL__H
#define PCMK__CRM_COMMON_RULES_INTERNAL__H
#include <regex.h> // regmatch_t
#include <libxml/tree.h> // xmlNode
#include <crm/common/rules.h> // enum expression_type, etc.
#include <crm/common/iso8601.h> // crm_time_t
-enum expression_type pcmk__expression_type(const xmlNode *expr);
+enum pcmk__combine {
+ pcmk__combine_unknown,
+ pcmk__combine_and,
+ pcmk__combine_or,
+};
+
+enum expression_type pcmk__condition_type(const xmlNode *condition);
char *pcmk__replace_submatches(const char *string, const char *match,
const regmatch_t submatches[], int nmatches);
+enum pcmk__combine pcmk__parse_combine(const char *combine);
int pcmk__evaluate_date_expression(const xmlNode *date_expression,
const crm_time_t *now,
crm_time_t *next_change);
-int pcmk__evaluate_attr_expression(const xmlNode *expression,
- const pcmk_rule_input_t *rule_input);
-int pcmk__evaluate_rsc_expression(const xmlNode *expr,
- const pcmk_rule_input_t *rule_input);
-int pcmk__evaluate_op_expression(const xmlNode *expr,
- const pcmk_rule_input_t *rule_input);
+int pcmk__evaluate_condition(xmlNode *expr, const pcmk_rule_input_t *rule_input,
+ crm_time_t *next_change);
#endif // PCMK__CRM_COMMON_RULES_INTERNAL__H
diff --git a/include/crm/pengine/rules.h b/include/crm/pengine/rules.h
index 1e87d2de4e..0337e12cc8 100644
--- a/include/crm/pengine/rules.h
+++ b/include/crm/pengine/rules.h
@@ -1,61 +1,48 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef PCMK__CRM_PENGINE_RULES__H
# define PCMK__CRM_PENGINE_RULES__H
# include <glib.h>
# include <crm/crm.h>
# include <crm/common/iso8601.h>
# include <crm/common/scheduler.h>
# include <crm/pengine/common.h>
#ifdef __cplusplus
extern "C" {
#endif
gboolean pe_evaluate_rules(xmlNode *ruleset, GHashTable *node_hash,
crm_time_t *now, crm_time_t *next_change);
-gboolean pe_test_rule(xmlNode *rule, GHashTable *node_hash,
- enum rsc_role_e role, crm_time_t *now,
- crm_time_t *next_change, pe_match_data_t *match_data);
-
-gboolean pe_test_expression(xmlNode *expr, GHashTable *node_hash,
- enum rsc_role_e role, crm_time_t *now,
- crm_time_t *next_change,
- pe_match_data_t *match_data);
-
void pe_eval_nvpairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name,
const pe_rule_eval_data_t *rule_data, GHashTable *hash,
const char *always_first, gboolean overwrite,
crm_time_t *next_change);
void pe_unpack_nvpairs(xmlNode *top, const xmlNode *xml_obj,
const char *set_name, GHashTable *node_hash,
GHashTable *hash, const char *always_first,
gboolean overwrite, crm_time_t *now,
crm_time_t *next_change);
gboolean pe_eval_rules(xmlNode *ruleset, const pe_rule_eval_data_t *rule_data,
crm_time_t *next_change);
-gboolean pe_eval_expr(xmlNode *rule, const pe_rule_eval_data_t *rule_data,
- crm_time_t *next_change);
-gboolean pe_eval_subexpr(xmlNode *expr, const pe_rule_eval_data_t *rule_data,
- crm_time_t *next_change);
#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
#include <crm/pengine/rules_compat.h>
#endif
#ifdef __cplusplus
}
#endif
#endif
diff --git a/include/crm/pengine/rules_compat.h b/include/crm/pengine/rules_compat.h
index 76d8e8c564..b0a99174cf 100644
--- a/include/crm/pengine/rules_compat.h
+++ b/include/crm/pengine/rules_compat.h
@@ -1,79 +1,98 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef PCMK__CRM_PENGINE_RULES_COMPAT__H
# define PCMK__CRM_PENGINE_RULES_COMPAT__H
#include <glib.h>
#include <libxml/tree.h> // xmlNode
#include <crm/common/iso8601.h> // crm_time_t
#include <crm/pengine/pe_types.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file
* \brief Deprecated Pacemaker rule API
* \ingroup pengine
* \deprecated Do not include this header directly. The rule APIs in this
* header, and the header itself, will be removed in a future
* release.
*/
//! \deprecated Use pe_evaluate_rules() instead
gboolean test_ruleset(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now);
-//! \deprecated Use pe_test_rule() instead
+//! \deprecated Use pcmk_evaluate_rule() instead
gboolean test_rule(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role,
crm_time_t *now);
-//! \deprecated Use pe_test_rule() instead
+//! \deprecated Use pcmk_evaluate_rule() instead
+gboolean pe_test_rule(xmlNode *rule, GHashTable *node_hash,
+ enum rsc_role_e role, crm_time_t *now,
+ crm_time_t *next_change, pe_match_data_t *match_data);
+
+//! \deprecated Use pcmk_evaluate_rule() instead
gboolean pe_test_rule_re(xmlNode *rule, GHashTable *node_hash,
enum rsc_role_e role, crm_time_t *now,
pe_re_match_data_t *re_match_data);
-//! \deprecated Use pe_test_rule() instead
+//! \deprecated Use pcmk_evaluate_rule() instead
gboolean pe_test_rule_full(xmlNode *rule, GHashTable *node_hash,
enum rsc_role_e role, crm_time_t *now,
pe_match_data_t *match_data);
-//! \deprecated Use pe_test_expression() instead
+//! \deprecated Use pcmk_evaluate_rule() on parent rule instead
+gboolean pe_test_expression(xmlNode *expr, GHashTable *node_hash,
+ enum rsc_role_e role, crm_time_t *now,
+ crm_time_t *next_change,
+ pe_match_data_t *match_data);
+
+//! \deprecated Use pcmk_evaluate_rule() on parent rule instead
gboolean test_expression(xmlNode *expr, GHashTable *node_hash,
enum rsc_role_e role, crm_time_t *now);
-//! \deprecated Use pe_test_expression() instead
+//! \deprecated Use pcmk_evaluate_rule() on parent rule instead
gboolean pe_test_expression_re(xmlNode *expr, GHashTable *node_hash,
enum rsc_role_e role, crm_time_t *now,
pe_re_match_data_t *re_match_data);
-//! \deprecated Use pe_test_expression() instead
+//! \deprecated Use pcmk_evaluate_rule() on parent rule instead
gboolean pe_test_expression_full(xmlNode *expr, GHashTable *node_hash,
enum rsc_role_e role,
crm_time_t *now, pe_match_data_t *match_data);
+//! \deprecated Use pcmk_evaluate_rule() on parent rule instead
+gboolean pe_eval_expr(xmlNode *rule, const pe_rule_eval_data_t *rule_data,
+ crm_time_t *next_change);
+
+//! \deprecated Use pcmk_evaluate_rule() on parent rule instead
+gboolean pe_eval_subexpr(xmlNode *expr, const pe_rule_eval_data_t *rule_data,
+ crm_time_t *next_change);
+
//! \deprecated Use pe_unpack_nvpairs() instead
void unpack_instance_attributes(xmlNode *top, xmlNode *xml_obj,
const char *set_name, GHashTable *node_hash,
GHashTable *hash, const char *always_first,
gboolean overwrite, crm_time_t *now);
//! \deprecated Do not use
enum expression_type find_expression_type(xmlNode *expr);
//! \deprecated Do not use
char *pe_expand_re_matches(const char *string,
const pe_re_match_data_t *match_data);
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_PENGINE_RULES_COMPAT__H
diff --git a/include/crm/pengine/rules_internal.h b/include/crm/pengine/rules_internal.h
index 4d6741b298..d659ee043b 100644
--- a/include/crm/pengine/rules_internal.h
+++ b/include/crm/pengine/rules_internal.h
@@ -1,31 +1,22 @@
/*
* Copyright 2015-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef RULES_INTERNAL_H
#define RULES_INTERNAL_H
#include <glib.h>
#include <libxml/tree.h>
#include <crm/common/iso8601.h>
#include <crm/pengine/common.h>
#include <crm/pengine/rules.h>
GList *pe_unpack_alerts(const xmlNode *alerts);
void pe_free_alert_list(GList *alert_list);
-gboolean pe__eval_attr_expr(const xmlNode *expr,
- const pe_rule_eval_data_t *rule_data);
-gboolean pe__eval_op_expr(const xmlNode *expr,
- const pe_rule_eval_data_t *rule_data);
-gboolean pe__eval_role_expr(const xmlNode *expr,
- const pe_rule_eval_data_t *rule_data);
-gboolean pe__eval_rsc_expr(const xmlNode *expr,
- const pe_rule_eval_data_t *rule_data);
-
#endif
diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h
index dc598673a5..4b3f984177 100644
--- a/lib/common/crmcommon_private.h
+++ b/lib/common/crmcommon_private.h
@@ -1,409 +1,422 @@
/*
* Copyright 2018-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef CRMCOMMON_PRIVATE__H
# define CRMCOMMON_PRIVATE__H
/* This header is for the sole use of libcrmcommon, so that functions can be
* declared with G_GNUC_INTERNAL for efficiency.
*/
#include <stdint.h> // uint8_t, uint32_t
#include <stdbool.h> // bool
#include <sys/types.h> // size_t
#include <glib.h> // gchar, GList
#include <libxml/tree.h> // xmlNode, xmlAttr
#include <qb/qbipcc.h> // struct qb_ipc_response_header
// Decent chunk size for processing large amounts of data
#define PCMK__BUFFER_SIZE 4096
#if defined(PCMK__UNIT_TESTING)
#undef G_GNUC_INTERNAL
#define G_GNUC_INTERNAL
#endif
/* When deleting portions of an XML tree, we keep a record so we can know later
* (e.g. when checking differences) that something was deleted.
*/
typedef struct pcmk__deleted_xml_s {
gchar *path;
int position;
} pcmk__deleted_xml_t;
typedef struct xml_node_private_s {
uint32_t check;
uint32_t flags;
} xml_node_private_t;
typedef struct xml_doc_private_s {
uint32_t check;
uint32_t flags;
char *user;
GList *acls;
GList *deleted_objs; // List of pcmk__deleted_xml_t
} xml_doc_private_t;
// XML entity references
#define PCMK__XML_ENTITY_AMP "&amp;"
#define PCMK__XML_ENTITY_GT "&gt;"
#define PCMK__XML_ENTITY_LT "&lt;"
#define PCMK__XML_ENTITY_QUOT "&quot;"
//! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5
#define PCMK__XML_VERSION ((pcmkXmlStr) "1.0")
#define pcmk__set_xml_flags(xml_priv, flags_to_set) do { \
(xml_priv)->flags = pcmk__set_flags_as(__func__, __LINE__, \
LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \
(flags_to_set), #flags_to_set); \
} while (0)
#define pcmk__clear_xml_flags(xml_priv, flags_to_clear) do { \
(xml_priv)->flags = pcmk__clear_flags_as(__func__, __LINE__, \
LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \
(flags_to_clear), #flags_to_clear); \
} while (0)
G_GNUC_INTERNAL
bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *),
void *user_data);
G_GNUC_INTERNAL
bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy);
G_GNUC_INTERNAL
void pcmk__mark_xml_created(xmlNode *xml);
G_GNUC_INTERNAL
int pcmk__xml_position(const xmlNode *xml,
enum xml_private_flags ignore_if_set);
G_GNUC_INTERNAL
xmlNode *pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle,
bool exact);
G_GNUC_INTERNAL
void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update,
bool as_diff);
G_GNUC_INTERNAL
xmlNode *pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment,
bool exact);
G_GNUC_INTERNAL
void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update);
G_GNUC_INTERNAL
void pcmk__free_acls(GList *acls);
G_GNUC_INTERNAL
void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user);
G_GNUC_INTERNAL
bool pcmk__is_user_in_group(const char *user, const char *group);
G_GNUC_INTERNAL
void pcmk__apply_acl(xmlNode *xml);
G_GNUC_INTERNAL
void pcmk__apply_creation_acl(xmlNode *xml, bool check_top);
G_GNUC_INTERNAL
void pcmk__mark_xml_attr_dirty(xmlAttr *a);
G_GNUC_INTERNAL
bool pcmk__xa_filterable(const char *name);
G_GNUC_INTERNAL
void pcmk__log_xmllib_err(void *ctx, const char *fmt, ...)
G_GNUC_PRINTF(2, 3);
G_GNUC_INTERNAL
void pcmk__mark_xml_node_dirty(xmlNode *xml);
G_GNUC_INTERNAL
bool pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data);
G_GNUC_INTERNAL
void pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer);
/*
* Date/times
*/
// For use with pcmk__add_time_from_xml()
enum pcmk__time_component {
pcmk__time_unknown,
pcmk__time_years,
pcmk__time_months,
pcmk__time_weeks,
pcmk__time_days,
pcmk__time_hours,
pcmk__time_minutes,
pcmk__time_seconds,
};
G_GNUC_INTERNAL
const char *pcmk__time_component_attr(enum pcmk__time_component component);
G_GNUC_INTERNAL
int pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component,
const xmlNode *xml);
G_GNUC_INTERNAL
void pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source);
/*
* IPC
*/
#define PCMK__IPC_VERSION 1
#define PCMK__CONTROLD_API_MAJOR "1"
#define PCMK__CONTROLD_API_MINOR "0"
// IPC behavior that varies by daemon
typedef struct pcmk__ipc_methods_s {
/*!
* \internal
* \brief Allocate any private data needed by daemon IPC
*
* \param[in,out] api IPC API connection
*
* \return Standard Pacemaker return code
*/
int (*new_data)(pcmk_ipc_api_t *api);
/*!
* \internal
* \brief Free any private data used by daemon IPC
*
* \param[in,out] api_data Data allocated by new_data() method
*/
void (*free_data)(void *api_data);
/*!
* \internal
* \brief Perform daemon-specific handling after successful connection
*
* Some daemons require clients to register before sending any other
* commands. The controller requires a CRM_OP_HELLO (with no reply), and
* the CIB manager, executor, and fencer require a CRM_OP_REGISTER (with a
* reply). Ideally this would be consistent across all daemons, but for now
* this allows each to do its own authorization.
*
* \param[in,out] api IPC API connection
*
* \return Standard Pacemaker return code
*/
int (*post_connect)(pcmk_ipc_api_t *api);
/*!
* \internal
* \brief Check whether an IPC request results in a reply
*
* \param[in,out] api IPC API connection
* \param[in] request IPC request XML
*
* \return true if request would result in an IPC reply, false otherwise
*/
bool (*reply_expected)(pcmk_ipc_api_t *api, const xmlNode *request);
/*!
* \internal
* \brief Perform daemon-specific handling of an IPC message
*
* \param[in,out] api IPC API connection
* \param[in,out] msg Message read from IPC connection
*
* \return true if more IPC reply messages should be expected
*/
bool (*dispatch)(pcmk_ipc_api_t *api, xmlNode *msg);
/*!
* \internal
* \brief Perform daemon-specific handling of an IPC disconnect
*
* \param[in,out] api IPC API connection
*/
void (*post_disconnect)(pcmk_ipc_api_t *api);
} pcmk__ipc_methods_t;
// Implementation of pcmk_ipc_api_t
struct pcmk_ipc_api_s {
enum pcmk_ipc_server server; // Daemon this IPC API instance is for
enum pcmk_ipc_dispatch dispatch_type; // How replies should be dispatched
size_t ipc_size_max; // maximum IPC buffer size
crm_ipc_t *ipc; // IPC connection
mainloop_io_t *mainloop_io; // If using mainloop, I/O source for IPC
bool free_on_disconnect; // Whether disconnect should free object
pcmk_ipc_callback_t cb; // Caller-registered callback (if any)
void *user_data; // Caller-registered data (if any)
void *api_data; // For daemon-specific use
pcmk__ipc_methods_t *cmds; // Behavior that varies by daemon
};
typedef struct pcmk__ipc_header_s {
struct qb_ipc_response_header qb;
uint32_t size_uncompressed;
uint32_t size_compressed;
uint32_t flags;
uint8_t version;
} pcmk__ipc_header_t;
G_GNUC_INTERNAL
int pcmk__send_ipc_request(pcmk_ipc_api_t *api, const xmlNode *request);
G_GNUC_INTERNAL
void pcmk__call_ipc_callback(pcmk_ipc_api_t *api,
enum pcmk_ipc_event event_type,
crm_exit_t status, void *event_data);
G_GNUC_INTERNAL
unsigned int pcmk__ipc_buffer_size(unsigned int max);
G_GNUC_INTERNAL
bool pcmk__valid_ipc_header(const pcmk__ipc_header_t *header);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__attrd_api_methods(void);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__controld_api_methods(void);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__pacemakerd_api_methods(void);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__schedulerd_api_methods(void);
/*
* Logging
*/
//! XML is newly created
#define PCMK__XML_PREFIX_CREATED "++"
//! XML has been deleted
#define PCMK__XML_PREFIX_DELETED "--"
//! XML has been modified
#define PCMK__XML_PREFIX_MODIFIED "+ "
//! XML has been moved
#define PCMK__XML_PREFIX_MOVED "+~"
/*
* Output
*/
G_GNUC_INTERNAL
int pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name,
const char *filename, char **argv);
G_GNUC_INTERNAL
void pcmk__register_option_messages(pcmk__output_t *out);
G_GNUC_INTERNAL
void pcmk__register_patchset_messages(pcmk__output_t *out);
G_GNUC_INTERNAL
bool pcmk__output_text_get_fancy(pcmk__output_t *out);
/*
* Rules
*/
// How node attribute values may be compared in rules
enum pcmk__comparison {
pcmk__comparison_unknown,
pcmk__comparison_defined,
pcmk__comparison_undefined,
pcmk__comparison_eq,
pcmk__comparison_ne,
pcmk__comparison_lt,
pcmk__comparison_lte,
pcmk__comparison_gt,
pcmk__comparison_gte,
};
// How node attribute values may be parsed in rules
enum pcmk__type {
pcmk__type_unknown,
pcmk__type_string,
pcmk__type_integer,
pcmk__type_number,
pcmk__type_version,
};
// Where to obtain reference value for a node attribute comparison
enum pcmk__reference_source {
pcmk__source_unknown,
pcmk__source_literal,
pcmk__source_instance_attrs,
pcmk__source_meta_attrs,
};
G_GNUC_INTERNAL
enum pcmk__comparison pcmk__parse_comparison(const char *op);
G_GNUC_INTERNAL
enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op,
const char *value1, const char *value2);
G_GNUC_INTERNAL
enum pcmk__reference_source pcmk__parse_source(const char *source);
G_GNUC_INTERNAL
int pcmk__cmp_by_type(const char *value1, const char *value2,
enum pcmk__type type);
G_GNUC_INTERNAL
int pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start,
crm_time_t **end);
G_GNUC_INTERNAL
int pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now);
+G_GNUC_INTERNAL
+int pcmk__evaluate_attr_expression(const xmlNode *expression,
+ const pcmk_rule_input_t *rule_input);
+
+G_GNUC_INTERNAL
+int pcmk__evaluate_rsc_expression(const xmlNode *expr,
+ const pcmk_rule_input_t *rule_input);
+
+G_GNUC_INTERNAL
+int pcmk__evaluate_op_expression(const xmlNode *expr,
+ const pcmk_rule_input_t *rule_input);
+
+
/*
* Utils
*/
#define PCMK__PW_BUFFER_LEN 500
/*
* Schemas
*/
typedef struct {
unsigned char v[2];
} pcmk__schema_version_t;
enum pcmk__schema_validator {
pcmk__schema_validator_none,
pcmk__schema_validator_rng
};
typedef struct {
char *name;
char *transform;
void *cache;
enum pcmk__schema_validator validator;
pcmk__schema_version_t version;
char *transform_enter;
bool transform_onleave;
} pcmk__schema_t;
G_GNUC_INTERNAL
int pcmk__find_x_0_schema_index(GList *schemas);
#endif // CRMCOMMON_PRIVATE__H
diff --git a/lib/common/rules.c b/lib/common/rules.c
index f13c3deade..e719df3fba 100644
--- a/lib/common/rules.c
+++ b/lib/common/rules.c
@@ -1,1295 +1,1465 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <stdio.h> // NULL, size_t
#include <stdbool.h> // bool
#include <ctype.h> // isdigit()
#include <regex.h> // regmatch_t
#include <stdint.h> // uint32_t
#include <inttypes.h> // PRIu32
#include <glib.h> // gboolean, FALSE
#include <libxml/tree.h> // xmlNode
#include <crm/common/scheduler.h>
#include <crm/common/iso8601_internal.h>
#include <crm/common/nvpair_internal.h>
#include <crm/common/scheduler_internal.h>
#include "crmcommon_private.h"
/*!
* \internal
- * \brief Get the expression type corresponding to given expression XML
+ * \brief Get the condition type corresponding to given condition XML
*
- * \param[in] expr Rule expression XML
+ * \param[in] condition Rule condition XML
*
- * \return Expression type corresponding to \p expr
+ * \return Condition type corresponding to \p condition
*/
enum expression_type
-pcmk__expression_type(const xmlNode *expr)
+pcmk__condition_type(const xmlNode *condition)
{
const char *name = NULL;
// Expression types based on element name
- if (pcmk__xe_is(expr, PCMK_XE_DATE_EXPRESSION)) {
- return pcmk__subexpr_datetime;
+ if (pcmk__xe_is(condition, PCMK_XE_DATE_EXPRESSION)) {
+ return pcmk__condition_datetime;
- } else if (pcmk__xe_is(expr, PCMK_XE_RSC_EXPRESSION)) {
- return pcmk__subexpr_resource;
+ } else if (pcmk__xe_is(condition, PCMK_XE_RSC_EXPRESSION)) {
+ return pcmk__condition_resource;
- } else if (pcmk__xe_is(expr, PCMK_XE_OP_EXPRESSION)) {
- return pcmk__subexpr_operation;
+ } else if (pcmk__xe_is(condition, PCMK_XE_OP_EXPRESSION)) {
+ return pcmk__condition_operation;
- } else if (pcmk__xe_is(expr, PCMK_XE_RULE)) {
- return pcmk__subexpr_rule;
+ } else if (pcmk__xe_is(condition, PCMK_XE_RULE)) {
+ return pcmk__condition_rule;
- } else if (!pcmk__xe_is(expr, PCMK_XE_EXPRESSION)) {
- return pcmk__subexpr_unknown;
+ } else if (!pcmk__xe_is(condition, PCMK_XE_EXPRESSION)) {
+ return pcmk__condition_unknown;
}
// Expression types based on node attribute name
- name = crm_element_value(expr, PCMK_XA_ATTRIBUTE);
+ name = crm_element_value(condition, PCMK_XA_ATTRIBUTE);
if (pcmk__str_any_of(name, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID,
NULL)) {
- return pcmk__subexpr_location;
+ return pcmk__condition_location;
}
- return pcmk__subexpr_attribute;
+ return pcmk__condition_attribute;
}
/*!
* \internal
* \brief Get parent XML element's ID for logging purposes
*
* \param[in] xml XML of a subelement
*
* \return ID of \p xml's parent for logging purposes (guaranteed non-NULL)
*/
static const char *
loggable_parent_id(const xmlNode *xml)
{
// Default if called without parent (likely for unit testing)
const char *parent_id = "implied";
if ((xml != NULL) && (xml->parent != NULL)) {
parent_id = pcmk__xe_id(xml->parent);
if (parent_id == NULL) { // Not possible with schema validation enabled
parent_id = "without ID";
}
}
return parent_id;
}
/*!
* \internal
* \brief Get the moon phase corresponding to a given date/time
*
* \param[in] now Date/time to get moon phase for
*
* \return Phase of the moon corresponding to \p now, where 0 is the new moon
* and 7 is the full moon
* \deprecated This feature has been deprecated since 2.1.6.
*/
static int
phase_of_the_moon(const crm_time_t *now)
{
/* As per the nethack rules:
* - A moon period is 29.53058 days ~= 30
* - A year is 365.2422 days
* - Number of days moon phase advances on first day of year compared to
* preceding year is (365.2422 - 12 * 29.53058) ~= 11
* - Number of years until same phases fall on the same days of the month
* is 18.6 ~= 19
* - Moon phase on first day of year (epact) ~= (11 * (year%19) + 29) % 30
* (29 as initial condition)
* - Current phase in days = first day phase + days elapsed in year
* - 6 moons ~= 177 days ~= 8 reported phases * 22 (+ 11/22 for rounding)
*/
uint32_t epact, diy, goldn;
uint32_t y;
crm_time_get_ordinal(now, &y, &diy);
goldn = (y % 19) + 1;
epact = (11 * goldn + 18) % 30;
if (((epact == 25) && (goldn > 11)) || (epact == 24)) {
epact++;
}
return (((((diy + epact) * 6) + 11) % 177) / 22) & 7;
}
/*!
* \internal
* \brief Check an integer value against a range from a date specification
*
* \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to check
* \param[in] id XML ID for logging purposes
* \param[in] attr Name of XML attribute with range to check against
* \param[in] value Value to compare against range
*
* \return Standard Pacemaker return code (specifically, pcmk_rc_before_range,
* pcmk_rc_after_range, or pcmk_rc_ok to indicate that result is either
* within range or undetermined)
* \note We return pcmk_rc_ok for an undetermined result so we can continue
* checking the next range attribute.
*/
static int
check_range(const xmlNode *date_spec, const char *id, const char *attr,
uint32_t value)
{
int rc = pcmk_rc_ok;
const char *range = crm_element_value(date_spec, attr);
long long low, high;
if (range == NULL) { // Attribute not present
goto bail;
}
if (pcmk__parse_ll_range(range, &low, &high) != pcmk_rc_ok) {
// Invalid range
/* @COMPAT When we can break behavioral backward compatibility, treat
* the entire rule as not passing.
*/
pcmk__config_err("Ignoring " PCMK_XE_DATE_SPEC
" %s attribute %s because '%s' is not a valid range",
id, attr, range);
} else if ((low != -1) && (value < low)) {
rc = pcmk_rc_before_range;
} else if ((high != -1) && (value > high)) {
rc = pcmk_rc_after_range;
}
bail:
crm_trace("Checked " PCMK_XE_DATE_SPEC " %s %s='%s' for %" PRIu32 ": %s",
id, attr, pcmk__s(range, ""), value, pcmk_rc_str(rc));
return rc;
}
/*!
* \internal
* \brief Evaluate a date specification for a given date/time
*
* \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to evaluate
* \param[in] now Time to check
*
* \return Standard Pacemaker return code (specifically, EINVAL for NULL
* arguments, pcmk_rc_ok if time matches specification, or
* pcmk_rc_before_range, pcmk_rc_after_range, or pcmk_rc_op_unsatisfied
* as appropriate to how time relates to specification)
*/
int
pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now)
{
const char *id = NULL;
const char *parent_id = loggable_parent_id(date_spec);
// Range attributes that can be specified for a PCMK_XE_DATE_SPEC element
struct range {
const char *attr;
uint32_t value;
} ranges[] = {
{ PCMK_XA_YEARS, 0U },
{ PCMK_XA_MONTHS, 0U },
{ PCMK_XA_MONTHDAYS, 0U },
{ PCMK_XA_HOURS, 0U },
{ PCMK_XA_MINUTES, 0U },
{ PCMK_XA_SECONDS, 0U },
{ PCMK_XA_YEARDAYS, 0U },
{ PCMK_XA_WEEKYEARS, 0U },
{ PCMK_XA_WEEKS, 0U },
{ PCMK_XA_WEEKDAYS, 0U },
{ PCMK__XA_MOON, 0U },
};
if ((date_spec == NULL) || (now == NULL)) {
return EINVAL;
}
// Get specification ID (for logging)
id = pcmk__xe_id(date_spec);
if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
/* @COMPAT When we can break behavioral backward compatibility,
* fail the specification
*/
pcmk__config_warn(PCMK_XE_DATE_SPEC " subelement of "
PCMK_XE_DATE_EXPRESSION " %s has no " PCMK_XA_ID,
parent_id);
id = "without ID"; // for logging
}
// Year, month, day
crm_time_get_gregorian(now, &(ranges[0].value), &(ranges[1].value),
&(ranges[2].value));
// Hour, minute, second
crm_time_get_timeofday(now, &(ranges[3].value), &(ranges[4].value),
&(ranges[5].value));
// Year (redundant) and day of year
crm_time_get_ordinal(now, &(ranges[0].value), &(ranges[6].value));
// Week year, week of week year, day of week
crm_time_get_isoweek(now, &(ranges[7].value), &(ranges[8].value),
&(ranges[9].value));
// Moon phase (deprecated)
ranges[10].value = phase_of_the_moon(now);
if (crm_element_value(date_spec, PCMK__XA_MOON) != NULL) {
pcmk__config_warn("Support for '" PCMK__XA_MOON "' in "
PCMK_XE_DATE_SPEC " elements (such as %s) is "
"deprecated and will be removed in a future release "
"of Pacemaker", id);
}
for (int i = 0; i < PCMK__NELEM(ranges); ++i) {
int rc = check_range(date_spec, id, ranges[i].attr, ranges[i].value);
if (rc != pcmk_rc_ok) {
return rc;
}
}
// All specified ranges passed, or none were given (also considered a pass)
return pcmk_rc_ok;
}
#define ADD_COMPONENT(component) do { \
int sub_rc = pcmk__add_time_from_xml(*end, component, duration); \
if (sub_rc != pcmk_rc_ok) { \
/* @COMPAT return sub_rc when we can break compatibility */ \
pcmk__config_warn("Ignoring %s in " PCMK_XE_DURATION " %s " \
"because it is invalid", \
pcmk__time_component_attr(component), id); \
rc = sub_rc; \
} \
} while (0)
/*!
* \internal
* \brief Given a duration and a start time, calculate the end time
*
* \param[in] duration XML of PCMK_XE_DURATION element
* \param[in] start Start time
* \param[out] end Where to store end time (\p *end must be NULL
* initially)
*
* \return Standard Pacemaker return code
* \note The caller is responsible for freeing \p *end using crm_time_free().
*/
int
pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start,
crm_time_t **end)
{
int rc = pcmk_rc_ok;
const char *id = NULL;
const char *parent_id = loggable_parent_id(duration);
if ((start == NULL) || (duration == NULL)
|| (end == NULL) || (*end != NULL)) {
return EINVAL;
}
// Get duration ID (for logging)
id = pcmk__xe_id(duration);
if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
/* @COMPAT When we can break behavioral backward compatibility,
* return pcmk_rc_unpack_error instead
*/
pcmk__config_warn(PCMK_XE_DURATION " subelement of "
PCMK_XE_DATE_EXPRESSION " %s has no " PCMK_XA_ID,
parent_id);
id = "without ID";
}
*end = pcmk_copy_time(start);
ADD_COMPONENT(pcmk__time_years);
ADD_COMPONENT(pcmk__time_months);
ADD_COMPONENT(pcmk__time_weeks);
ADD_COMPONENT(pcmk__time_days);
ADD_COMPONENT(pcmk__time_hours);
ADD_COMPONENT(pcmk__time_minutes);
ADD_COMPONENT(pcmk__time_seconds);
return rc;
}
/*!
* \internal
* \brief Evaluate a range check for a given date/time
*
* \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element
* \param[in] id Expression ID for logging purposes
* \param[in] now Date/time to compare
* \param[in,out] next_change If not NULL, set this to when the evaluation
* will change, if known and earlier than the
* original value
*
* \return Standard Pacemaker return code
*/
static int
evaluate_in_range(const xmlNode *date_expression, const char *id,
const crm_time_t *now, crm_time_t *next_change)
{
crm_time_t *start = NULL;
crm_time_t *end = NULL;
if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START,
&start) != pcmk_rc_ok) {
/* @COMPAT When we can break behavioral backward compatibility,
* return pcmk_rc_unpack_error
*/
pcmk__config_warn("Ignoring " PCMK_XA_START " in "
PCMK_XE_DATE_EXPRESSION " %s because it is invalid",
id);
}
if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END,
&end) != pcmk_rc_ok) {
/* @COMPAT When we can break behavioral backward compatibility,
* return pcmk_rc_unpack_error
*/
pcmk__config_warn("Ignoring " PCMK_XA_END " in "
PCMK_XE_DATE_EXPRESSION " %s because it is invalid",
id);
}
if ((start == NULL) && (end == NULL)) {
// Not possible with schema validation enabled
/* @COMPAT When we can break behavioral backward compatibility,
* return pcmk_rc_unpack_error
*/
pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because in_range requires at least one of "
PCMK_XA_START " or " PCMK_XA_END, id);
return pcmk_rc_undetermined;
}
if (end == NULL) {
xmlNode *duration = pcmk__xe_first_child(date_expression,
PCMK_XE_DURATION, NULL, NULL);
if (duration != NULL) {
/* @COMPAT When we can break behavioral backward compatibility,
* return the result of this if not OK
*/
pcmk__unpack_duration(duration, start, &end);
}
}
if ((start != NULL) && (crm_time_compare(now, start) < 0)) {
pcmk__set_time_if_earlier(next_change, start);
crm_time_free(start);
crm_time_free(end);
return pcmk_rc_before_range;
}
if (end != NULL) {
if (crm_time_compare(now, end) > 0) {
crm_time_free(start);
crm_time_free(end);
return pcmk_rc_after_range;
}
// Evaluation doesn't change until second after end
if (next_change != NULL) {
crm_time_add_seconds(end, 1);
pcmk__set_time_if_earlier(next_change, end);
}
}
crm_time_free(start);
crm_time_free(end);
return pcmk_rc_within_range;
}
/*!
* \internal
* \brief Evaluate a greater-than check for a given date/time
*
* \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element
* \param[in] id Expression ID for logging purposes
* \param[in] now Date/time to compare
* \param[in,out] next_change If not NULL, set this to when the evaluation
* will change, if known and earlier than the
* original value
*
* \return Standard Pacemaker return code
*/
static int
evaluate_gt(const xmlNode *date_expression, const char *id,
const crm_time_t *now, crm_time_t *next_change)
{
crm_time_t *start = NULL;
if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START,
&start) != pcmk_rc_ok) {
/* @COMPAT When we can break behavioral backward compatibility,
* return pcmk_rc_unpack_error
*/
pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because " PCMK_XA_START " is invalid",
id);
return pcmk_rc_undetermined;
}
if (start == NULL) { // Not possible with schema validation enabled
/* @COMPAT When we can break behavioral backward compatibility,
* return pcmk_rc_unpack_error
*/
pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because " PCMK_VALUE_GT " requires "
PCMK_XA_START, id);
return pcmk_rc_undetermined;
}
if (crm_time_compare(now, start) > 0) {
crm_time_free(start);
return pcmk_rc_within_range;
}
// Evaluation doesn't change until second after start time
crm_time_add_seconds(start, 1);
pcmk__set_time_if_earlier(next_change, start);
crm_time_free(start);
return pcmk_rc_before_range;
}
/*!
* \internal
* \brief Evaluate a less-than check for a given date/time
*
* \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element
* \param[in] id Expression ID for logging purposes
* \param[in] now Date/time to compare
* \param[in,out] next_change If not NULL, set this to when the evaluation
* will change, if known and earlier than the
* original value
*
* \return Standard Pacemaker return code
*/
static int
evaluate_lt(const xmlNode *date_expression, const char *id,
const crm_time_t *now, crm_time_t *next_change)
{
crm_time_t *end = NULL;
if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END,
&end) != pcmk_rc_ok) {
/* @COMPAT When we can break behavioral backward compatibility,
* return pcmk_rc_unpack_error
*/
pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because " PCMK_XA_END " is invalid", id);
return pcmk_rc_undetermined;
}
if (end == NULL) { // Not possible with schema validation enabled
/* @COMPAT When we can break behavioral backward compatibility,
* return pcmk_rc_unpack_error
*/
pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
"passing because " PCMK_VALUE_GT " requires "
PCMK_XA_END, id);
return pcmk_rc_undetermined;
}
if (crm_time_compare(now, end) < 0) {
pcmk__set_time_if_earlier(next_change, end);
crm_time_free(end);
return pcmk_rc_within_range;
}
crm_time_free(end);
return pcmk_rc_after_range;
}
/*!
* \internal
* \brief Evaluate a rule's date expression for a given date/time
*
* \param[in] date_expression XML of a PCMK_XE_DATE_EXPRESSION element
* \param[in] now Time to use for evaluation
* \param[in,out] next_change If not NULL, set this to when the evaluation
* will change, if known and earlier than the
* original value
*
* \return Standard Pacemaker return code (unlike most other evaluation
* functions, this can return either pcmk_rc_ok or pcmk_rc_within_range
* on success)
*/
int
pcmk__evaluate_date_expression(const xmlNode *date_expression,
const crm_time_t *now, crm_time_t *next_change)
{
const char *id = NULL;
const char *op = NULL;
int rc = pcmk_rc_undetermined;
if ((date_expression == NULL) || (now == NULL)) {
return EINVAL;
}
// Get expression ID (for logging)
id = pcmk__xe_id(date_expression);
if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
/* @COMPAT When we can break behavioral backward compatibility,
* return pcmk_rc_unpack_error
*/
pcmk__config_warn(PCMK_XE_DATE_EXPRESSION " element has no "
PCMK_XA_ID);
id = "without ID"; // for logging
}
op = crm_element_value(date_expression, PCMK_XA_OPERATION);
if (pcmk__str_eq(op, PCMK_VALUE_IN_RANGE,
pcmk__str_null_matches|pcmk__str_casei)) {
rc = evaluate_in_range(date_expression, id, now, next_change);
} else if (pcmk__str_eq(op, PCMK_VALUE_DATE_SPEC, pcmk__str_casei)) {
xmlNode *date_spec = pcmk__xe_first_child(date_expression,
PCMK_XE_DATE_SPEC, NULL,
NULL);
if (date_spec == NULL) { // Not possible with schema validation enabled
/* @COMPAT When we can break behavioral backward compatibility,
* return pcmk_rc_unpack_error
*/
pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s "
"as not passing because " PCMK_VALUE_DATE_SPEC
" operations require a " PCMK_XE_DATE_SPEC
" subelement", id);
} else {
// @TODO set next_change appropriately
rc = pcmk__evaluate_date_spec(date_spec, now);
}
} else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) {
rc = evaluate_gt(date_expression, id, now, next_change);
} else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) {
rc = evaluate_lt(date_expression, id, now, next_change);
} else { // Not possible with schema validation enabled
/* @COMPAT When we can break behavioral backward compatibility,
* return pcmk_rc_unpack_error
*/
pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION
" %s as not passing because '%s' is not a valid "
PCMK_XE_OPERATION, op);
}
crm_trace(PCMK_XE_DATE_EXPRESSION " %s (%s): %s (%d)",
id, op, pcmk_rc_str(rc), rc);
return rc;
}
/*!
* \internal
* \brief Go through submatches in a string, either counting how many bytes
* would be needed for the expansion, or performing the expansion,
* as requested
*
* \param[in] string String possibly containing submatch variables
* \param[in] match String that matched the regular expression
* \param[in] submatches Regular expression submatches (as set by regexec())
* \param[in] nmatches Number of entries in \p submatches[]
* \param[out] expansion If not NULL, expand string here (must be
* pre-allocated to appropriate size)
* \param[out] nbytes If not NULL, set to size needed for expansion
*
* \return true if any expansion is needed, otherwise false
*/
static bool
process_submatches(const char *string, const char *match,
const regmatch_t submatches[], int nmatches,
char *expansion, size_t *nbytes)
{
bool expanded = false;
const char *src = string;
if (nbytes != NULL) {
*nbytes = 1; // Include space for terminator
}
while (*src != '\0') {
int submatch = 0;
size_t match_len = 0;
if ((src[0] != '%') || !isdigit(src[1])) {
/* src does not point to the first character of a %N sequence,
* so expand this character as-is
*/
if (expansion != NULL) {
*expansion++ = *src;
}
if (nbytes != NULL) {
++(*nbytes);
}
++src;
continue;
}
submatch = src[1] - '0';
src += 2; // Skip over %N sequence in source string
expanded = true; // Expansion will be different from source
// Omit sequence from expansion unless it has a non-empty match
if ((nmatches <= submatch) // Not enough submatches
|| (submatches[submatch].rm_so < 0) // Pattern did not match
|| (submatches[submatch].rm_eo
<= submatches[submatch].rm_so)) { // Match was empty
continue;
}
match_len = submatches[submatch].rm_eo - submatches[submatch].rm_so;
if (nbytes != NULL) {
*nbytes += match_len;
}
if (expansion != NULL) {
memcpy(expansion, match + submatches[submatch].rm_so,
match_len);
expansion += match_len;
}
}
return expanded;
}
/*!
* \internal
* \brief Expand any regular expression submatches (%0-%9) in a string
*
* \param[in] string String possibly containing submatch variables
* \param[in] match String that matched the regular expression
* \param[in] submatches Regular expression submatches (as set by regexec())
* \param[in] nmatches Number of entries in \p submatches[]
*
* \return Newly allocated string identical to \p string with submatches
* expanded on success, or NULL if no expansions were needed
* \note The caller is responsible for freeing the result with free()
*/
char *
pcmk__replace_submatches(const char *string, const char *match,
const regmatch_t submatches[], int nmatches)
{
size_t nbytes = 0;
char *result = NULL;
if (pcmk__str_empty(string) || pcmk__str_empty(match)) {
return NULL; // Nothing to expand
}
// Calculate how much space will be needed for expanded string
if (!process_submatches(string, match, submatches, nmatches, NULL,
&nbytes)) {
return NULL; // No expansions needed
}
// Allocate enough space for expanded string
result = pcmk__assert_alloc(nbytes, sizeof(char));
// Expand submatches
(void) process_submatches(string, match, submatches, nmatches, result,
NULL);
return result;
}
/*!
* \internal
* \brief Parse a comparison type from a string
*
* \param[in] op String with comparison type (valid values are
* \c PCMK_VALUE_DEFINED, \c PCMK_VALUE_NOT_DEFINED,
* \c PCMK_VALUE_EQ, \c PCMK_VALUE_NE,
* \c PCMK_VALUE_LT, \c PCMK_VALUE_LTE,
* \c PCMK_VALUE_GT, or \c PCMK_VALUE_GTE)
*
* \return Comparison type corresponding to \p op
*/
enum pcmk__comparison
pcmk__parse_comparison(const char *op)
{
if (pcmk__str_eq(op, PCMK_VALUE_DEFINED, pcmk__str_casei)) {
return pcmk__comparison_defined;
} else if (pcmk__str_eq(op, PCMK_VALUE_NOT_DEFINED, pcmk__str_casei)) {
return pcmk__comparison_undefined;
} else if (pcmk__str_eq(op, PCMK_VALUE_EQ, pcmk__str_casei)) {
return pcmk__comparison_eq;
} else if (pcmk__str_eq(op, PCMK_VALUE_NE, pcmk__str_casei)) {
return pcmk__comparison_ne;
} else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) {
return pcmk__comparison_lt;
} else if (pcmk__str_eq(op, PCMK_VALUE_LTE, pcmk__str_casei)) {
return pcmk__comparison_lte;
} else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) {
return pcmk__comparison_gt;
} else if (pcmk__str_eq(op, PCMK_VALUE_GTE, pcmk__str_casei)) {
return pcmk__comparison_gte;
}
return pcmk__comparison_unknown;
}
/*!
* \internal
* \brief Parse a value type from a string
*
* \param[in] type String with value type (valid values are NULL,
* \c PCMK_VALUE_STRING, \c PCMK_VALUE_INTEGER,
* \c PCMK_VALUE_NUMBER, and \c PCMK_VALUE_VERSION)
* \param[in] op Operation type (used only to select default)
* \param[in] value1 First value being compared (used only to select default)
* \param[in] value2 Second value being compared (used only to select default)
*/
enum pcmk__type
pcmk__parse_type(const char *type, enum pcmk__comparison op,
const char *value1, const char *value2)
{
if (type == NULL) {
switch (op) {
case pcmk__comparison_lt:
case pcmk__comparison_lte:
case pcmk__comparison_gt:
case pcmk__comparison_gte:
if (((value1 != NULL) && (strchr(value1, '.') != NULL))
|| ((value2 != NULL) && (strchr(value2, '.') != NULL))) {
return pcmk__type_number;
}
return pcmk__type_integer;
default:
return pcmk__type_string;
}
}
if (pcmk__str_eq(type, PCMK_VALUE_STRING, pcmk__str_casei)) {
return pcmk__type_string;
} else if (pcmk__str_eq(type, PCMK_VALUE_INTEGER, pcmk__str_casei)) {
return pcmk__type_integer;
} else if (pcmk__str_eq(type, PCMK_VALUE_NUMBER, pcmk__str_casei)) {
return pcmk__type_number;
} else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_casei)) {
return pcmk__type_version;
}
return pcmk__type_unknown;
}
/*!
* \internal
* \brief Compare two strings according to a given type
*
* \param[in] value1 String with first value to compare
* \param[in] value2 String with second value to compare
* \param[in] type How to interpret the values
*
* \return Standard comparison result (a negative integer if \p value1 is
* lesser, 0 if the values are equal, and a positive integer if
* \p value1 is greater)
*/
int
pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type)
{
// NULL compares as less than non-NULL
if (value2 == NULL) {
return (value1 == NULL)? 0 : 1;
}
if (value1 == NULL) {
return -1;
}
switch (type) {
case pcmk__type_string:
return strcasecmp(value1, value2);
case pcmk__type_integer:
{
long long integer1;
long long integer2;
if ((pcmk__scan_ll(value1, &integer1, 0LL) != pcmk_rc_ok)
|| (pcmk__scan_ll(value2, &integer2, 0LL) != pcmk_rc_ok)) {
crm_warn("Comparing '%s' and '%s' as strings because "
"invalid as integers", value1, value2);
return strcasecmp(value1, value2);
}
return (integer1 < integer2)? -1 : (integer1 > integer2)? 1 : 0;
}
break;
case pcmk__type_number:
{
double num1;
double num2;
if ((pcmk__scan_double(value1, &num1, NULL, NULL) != pcmk_rc_ok)
|| (pcmk__scan_double(value2, &num2, NULL,
NULL) != pcmk_rc_ok)) {
crm_warn("Comparing '%s' and '%s' as strings because invalid as "
"numbers", value1, value2);
return strcasecmp(value1, value2);
}
return (num1 < num2)? -1 : (num1 > num2)? 1 : 0;
}
break;
case pcmk__type_version:
return compare_version(value1, value2);
default: // Invalid type
return 0;
}
}
/*!
* \internal
* \brief Parse a reference value source from a string
*
* \param[in] source String indicating reference value source
*
* \return Reference value source corresponding to \p source
*/
enum pcmk__reference_source
pcmk__parse_source(const char *source)
{
if (pcmk__str_eq(source, PCMK_VALUE_LITERAL,
pcmk__str_casei|pcmk__str_null_matches)) {
return pcmk__source_literal;
} else if (pcmk__str_eq(source, PCMK_VALUE_PARAM, pcmk__str_casei)) {
return pcmk__source_instance_attrs;
} else if (pcmk__str_eq(source, PCMK_VALUE_META, pcmk__str_casei)) {
return pcmk__source_meta_attrs;
} else {
return pcmk__source_unknown;
}
}
+/*!
+ * \internal
+ * \brief Parse a boolean operator from a string
+ *
+ * \param[in] combine String indicating boolean operator
+ *
+ * \return Enumeration value corresponding to \p combine
+ */
+enum pcmk__combine
+pcmk__parse_combine(const char *combine)
+{
+ if (pcmk__str_eq(combine, PCMK_VALUE_AND,
+ pcmk__str_null_matches|pcmk__str_casei)) {
+ return pcmk__combine_and;
+
+ } else if (pcmk__str_eq(combine, PCMK_VALUE_OR, pcmk__str_casei)) {
+ return pcmk__combine_or;
+
+ } else {
+ return pcmk__combine_unknown;
+ }
+}
+
/*!
* \internal
* \brief Get the result of a node attribute comparison for rule evaluation
*
* \param[in] actual Actual node attribute value
* \param[in] reference Node attribute value from rule (ignored for
* \p comparison of \c pcmk__comparison_defined or
* \c pcmk__comparison_undefined)
* \param[in] type How to interpret the values
* \param[in] comparison How to compare the values
*
* \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok if the
* comparison passes, and some other value if it does not)
*/
static int
evaluate_attr_comparison(const char *actual, const char *reference,
enum pcmk__type type, enum pcmk__comparison comparison)
{
int cmp = 0;
switch (comparison) {
case pcmk__comparison_defined:
return (actual != NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
case pcmk__comparison_undefined:
return (actual == NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
default:
break;
}
cmp = pcmk__cmp_by_type(actual, reference, type);
switch (comparison) {
case pcmk__comparison_eq:
return (cmp == 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
case pcmk__comparison_ne:
return (cmp != 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
default:
break;
}
if ((actual == NULL) || (reference == NULL)) {
return pcmk_rc_op_unsatisfied; // Comparison would be meaningless
}
switch (comparison) {
case pcmk__comparison_lt:
return (cmp < 0)? pcmk_rc_ok : pcmk_rc_after_range;
case pcmk__comparison_lte:
return (cmp <= 0)? pcmk_rc_ok : pcmk_rc_after_range;
case pcmk__comparison_gt:
return (cmp > 0)? pcmk_rc_ok : pcmk_rc_before_range;
case pcmk__comparison_gte:
return (cmp >= 0)? pcmk_rc_ok : pcmk_rc_before_range;
default: // Not possible with schema validation enabled
return pcmk_rc_op_unsatisfied;
}
}
/*!
* \internal
* \brief Get a reference value from a configured source
*
* \param[in] value Value given in rule expression
* \param[in] source Reference value source
* \param[in] rule_input Values used to evaluate rule criteria
*/
static const char *
value_from_source(const char *value, enum pcmk__reference_source source,
const pcmk_rule_input_t *rule_input)
{
GHashTable *table = NULL;
if (pcmk__str_empty(value)) {
/* @COMPAT When we can break backward compatibility, drop this block so
* empty strings are treated as such (there should never be an empty
* string as an instance attribute or meta-attribute name, so those will
* get NULL anyway, but it could matter for literal comparisons)
*/
return NULL;
}
switch (source) {
case pcmk__source_literal:
return value;
case pcmk__source_instance_attrs:
table = rule_input->rsc_params;
break;
case pcmk__source_meta_attrs:
table = rule_input->rsc_meta;
break;
default:
return NULL; // Not possible
}
if (table == NULL) {
return NULL;
}
return (const char *) g_hash_table_lookup(table, value);
}
/*!
* \internal
* \brief Evaluate a node attribute rule expression
*
* \param[in] expression XML of a rule's PCMK_XE_EXPRESSION subelement
* \param[in] rule_input Values used to evaluate rule criteria
*
* \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
* passes, some other value if it does not)
*/
int
pcmk__evaluate_attr_expression(const xmlNode *expression,
const pcmk_rule_input_t *rule_input)
{
const char *id = NULL;
const char *op = NULL;
const char *attr = NULL;
const char *type_s = NULL;
const char *value = NULL;
const char *actual = NULL;
const char *source_s = NULL;
const char *reference = NULL;
char *expanded_attr = NULL;
int rc = pcmk_rc_ok;
enum pcmk__type type = pcmk__type_unknown;
enum pcmk__reference_source source = pcmk__source_unknown;
enum pcmk__comparison comparison = pcmk__comparison_unknown;
if ((expression == NULL) || (rule_input == NULL)) {
return EINVAL;
}
// Get expression ID (for logging)
id = pcmk__xe_id(expression);
if (pcmk__str_empty(id)) {
/* @COMPAT When we can break behavioral backward compatibility,
* fail the expression
*/
pcmk__config_warn(PCMK_XE_EXPRESSION " element has no " PCMK_XA_ID);
id = "without ID"; // for logging
}
/* Get name of node attribute to compare (expanding any %0-%9 to
* regular expression submatches)
*/
attr = crm_element_value(expression, PCMK_XA_ATTRIBUTE);
if (pcmk__str_empty(attr)) {
pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
"because " PCMK_XA_ATTRIBUTE " was not specified", id);
return pcmk_rc_unpack_error;
}
expanded_attr = pcmk__replace_submatches(attr, rule_input->rsc_id,
rule_input->rsc_id_submatches,
rule_input->rsc_id_nmatches);
if (expanded_attr != NULL) {
attr = expanded_attr;
}
// Get and validate operation
op = crm_element_value(expression, PCMK_XA_OPERATION);
comparison = pcmk__parse_comparison(op);
if (comparison == pcmk__comparison_unknown) {
// Not possible with schema validation enabled
if (op == NULL) {
pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
"passing because it has no " PCMK_XA_OPERATION,
id);
} else {
pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
"passing because '%s' is not a valid "
PCMK_XA_OPERATION, id, op);
}
rc = pcmk_rc_unpack_error;
goto done;
}
// How reference value is obtained (literal, resource meta-attribute, etc.)
source_s = crm_element_value(expression, PCMK_XA_VALUE_SOURCE);
source = pcmk__parse_source(source_s);
if (source == pcmk__source_unknown) {
// Not possible with schema validation enabled
// @COMPAT Fail expression once we can break backward compatibility
pcmk__config_warn("Expression %s has invalid " PCMK_XA_VALUE_SOURCE
" value '%s', using default "
"('" PCMK_VALUE_LITERAL "')", id, source_s);
source = pcmk__source_literal;
}
// Get and validate reference value
value = crm_element_value(expression, PCMK_XA_VALUE);
switch (comparison) {
case pcmk__comparison_defined:
case pcmk__comparison_undefined:
if (value != NULL) {
pcmk__config_warn("Ignoring " PCMK_XA_VALUE " in "
PCMK_XE_EXPRESSION " %s because it is unused "
"when " PCMK_XA_BOOLEAN_OP " is %s", id, op);
}
break;
default:
if (value == NULL) {
pcmk__config_warn(PCMK_XE_EXPRESSION " %s has no "
PCMK_XA_VALUE, id);
}
break;
}
reference = value_from_source(value, source, rule_input);
// Get actual value of node attribute
if (rule_input->node_attrs != NULL) {
actual = g_hash_table_lookup(rule_input->node_attrs, attr);
}
// Get and validate value type (after expanding reference value)
type_s = crm_element_value(expression, PCMK_XA_TYPE);
type = pcmk__parse_type(type_s, comparison, actual, reference);
if (type == pcmk__type_unknown) {
/* Not possible with schema validation enabled
*
* @COMPAT When we can break behavioral backward compatibility, treat
* the expression as not passing.
*/
pcmk__config_warn("Non-empty node attribute values will be treated as "
"equal for " PCMK_XE_EXPRESSION " %s because '%s' "
"is not a valid type", id, type);
}
rc = evaluate_attr_comparison(actual, reference, type, comparison);
switch (comparison) {
case pcmk__comparison_defined:
case pcmk__comparison_undefined:
crm_trace(PCMK_XE_EXPRESSION " %s result: %s (for attribute %s %s)",
id, pcmk_rc_str(rc), attr, op);
break;
default:
crm_trace(PCMK_XE_EXPRESSION " %s result: "
"%s (attribute %s %s '%s' via %s source as %s type)",
id, pcmk_rc_str(rc), attr, op, pcmk__s(reference, ""),
pcmk__s(source_s, "default"), pcmk__s(type_s, "default"));
break;
}
done:
free(expanded_attr);
return rc;
}
/*!
* \internal
* \brief Evaluate a resource rule expression
*
* \param[in] rsc_expression XML of rule's \c PCMK_XE_RSC_EXPRESSION subelement
* \param[in] rule_input Values used to evaluate rule criteria
*
* \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
* passes, some other value if it does not)
*/
int
pcmk__evaluate_rsc_expression(const xmlNode *rsc_expression,
const pcmk_rule_input_t *rule_input)
{
const char *id = NULL;
const char *standard = NULL;
const char *provider = NULL;
const char *type = NULL;
if ((rsc_expression == NULL) || (rule_input == NULL)) {
return EINVAL;
}
// Validate XML ID
id = pcmk__xe_id(rsc_expression);
if (pcmk__str_empty(id)) {
// Not possible with schema validation enabled
/* @COMPAT When we can break behavioral backward compatibility,
* fail the expression
*/
pcmk__config_warn(PCMK_XE_RSC_EXPRESSION " has no " PCMK_XA_ID);
id = "without ID"; // for logging
}
// Compare resource standard
standard = crm_element_value(rsc_expression, PCMK_XA_CLASS);
if ((standard != NULL)
&& !pcmk__str_eq(standard, rule_input->rsc_standard, pcmk__str_none)) {
crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because "
"actual standard '%s' doesn't match '%s'",
id, pcmk__s(rule_input->rsc_standard, ""), standard);
return pcmk_rc_op_unsatisfied;
}
// Compare resource provider
provider = crm_element_value(rsc_expression, PCMK_XA_PROVIDER);
if ((provider != NULL)
&& !pcmk__str_eq(provider, rule_input->rsc_provider, pcmk__str_none)) {
crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because "
"actual provider '%s' doesn't match '%s'",
id, pcmk__s(rule_input->rsc_provider, ""), provider);
return pcmk_rc_op_unsatisfied;
}
// Compare resource agent type
type = crm_element_value(rsc_expression, PCMK_XA_TYPE);
if ((type != NULL)
&& !pcmk__str_eq(type, rule_input->rsc_agent, pcmk__str_none)) {
crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because "
"actual agent '%s' doesn't match '%s'",
id, pcmk__s(rule_input->rsc_agent, ""), type);
return pcmk_rc_op_unsatisfied;
}
crm_trace(PCMK_XE_RSC_EXPRESSION " %s is satisfied by %s%s%s:%s",
id, pcmk__s(standard, ""),
((provider == NULL)? "" : ":"), pcmk__s(provider, ""),
pcmk__s(type, ""));
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Evaluate an operation rule expression
*
* \param[in] op_expression XML of a rule's \c PCMK_XE_OP_EXPRESSION subelement
* \param[in] rule_input Values used to evaluate rule criteria
*
* \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
* is satisfied, some other value if it is not)
*/
int
pcmk__evaluate_op_expression(const xmlNode *op_expression,
const pcmk_rule_input_t *rule_input)
{
const char *id = NULL;
const char *name = NULL;
const char *interval_s = NULL;
guint interval_ms = 0U;
if ((op_expression == NULL) || (rule_input == NULL)) {
return EINVAL;
}
// Get operation expression ID (for logging)
id = pcmk__xe_id(op_expression);
if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
/* @COMPAT When we can break behavioral backward compatibility,
* return pcmk_rc_op_unsatisfied
*/
pcmk__config_warn(PCMK_XE_OP_EXPRESSION " element has no " PCMK_XA_ID);
id = "without ID"; // for logging
}
// Validate operation name
name = crm_element_value(op_expression, PCMK_XA_NAME);
if (name == NULL) { // Not possible with schema validation enabled
pcmk__config_warn("Treating " PCMK_XE_OP_EXPRESSION " %s as not "
"passing because it has no " PCMK_XA_NAME, id);
return pcmk_rc_unpack_error;
}
// Validate operation interval
interval_s = crm_element_value(op_expression, PCMK_META_INTERVAL);
if (pcmk_parse_interval_spec(interval_s, &interval_ms) != pcmk_rc_ok) {
pcmk__config_warn("Treating " PCMK_XE_OP_EXPRESSION " %s as not "
"passing because '%s' is not a valid interval",
id, interval_s);
return pcmk_rc_unpack_error;
}
// Compare operation name
if (!pcmk__str_eq(name, rule_input->op_name, pcmk__str_none)) {
crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because "
"actual name '%s' doesn't match '%s'",
id, pcmk__s(rule_input->op_name, ""), name);
return pcmk_rc_op_unsatisfied;
}
// Compare operation interval (unspecified interval matches all)
if ((interval_s != NULL) && (interval_ms != rule_input->op_interval_ms)) {
crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because "
"actual interval %s doesn't match %s",
id, pcmk__readable_interval(rule_input->op_interval_ms),
pcmk__readable_interval(interval_ms));
return pcmk_rc_op_unsatisfied;
}
crm_trace(PCMK_XE_OP_EXPRESSION " %s is satisfied (name %s, interval %s)",
id, name, pcmk__readable_interval(rule_input->op_interval_ms));
return pcmk_rc_ok;
}
+
+/*!
+ * \internal
+ * \brief Evaluate a rule condition
+ *
+ * \param[in,out] condition XML containing a rule condition (a subrule, or an
+ * expression of any type)
+ * \param[in] rule_input Values used to evaluate rule criteria
+ * \param[out] next_change If not NULL, set to when evaluation will change
+ *
+ * \return Standard Pacemaker return code (\c pcmk_rc_ok if the condition
+ * passes, some other value if it does not)
+ */
+int
+pcmk__evaluate_condition(xmlNode *condition,
+ const pcmk_rule_input_t *rule_input,
+ crm_time_t *next_change)
+{
+
+ if ((condition == NULL) || (rule_input == NULL)) {
+ return EINVAL;
+ }
+
+ switch (pcmk__condition_type(condition)) {
+ case pcmk__condition_rule:
+ return pcmk_evaluate_rule(condition, rule_input, next_change);
+
+ case pcmk__condition_attribute:
+ case pcmk__condition_location:
+ return pcmk__evaluate_attr_expression(condition, rule_input);
+
+ case pcmk__condition_datetime:
+ {
+ int rc = pcmk__evaluate_date_expression(condition,
+ rule_input->now,
+ next_change);
+
+ return (rc == pcmk_rc_within_range)? pcmk_rc_ok : rc;
+ }
+
+ case pcmk__condition_resource:
+ return pcmk__evaluate_rsc_expression(condition, rule_input);
+
+ case pcmk__condition_operation:
+ return pcmk__evaluate_op_expression(condition, rule_input);
+
+ default: // Not possible with schema validation enabled
+ pcmk__config_err("Treating rule condition %s as not passing "
+ "because %s is not a valid condition type",
+ pcmk__s(pcmk__xe_id(condition), "without ID"),
+ (const char *) condition->name);
+ return pcmk_rc_unpack_error;
+ }
+}
+
+/*!
+ * \brief Evaluate a single rule, including all its conditions
+ *
+ * \param[in,out] rule XML containing a rule definition or its id-ref
+ * \param[in] rule_input Values used to evaluate rule criteria
+ * \param[out] next_change If not NULL, set to when evaluation will change
+ *
+ * \return Standard Pacemaker return code (\c pcmk_rc_ok if the rule is
+ * satisfied, some other value if it is not)
+ */
+int
+pcmk_evaluate_rule(xmlNode *rule, const pcmk_rule_input_t *rule_input,
+ crm_time_t *next_change)
+{
+ bool empty = true;
+ int rc = pcmk_rc_ok;
+ const char *id = NULL;
+ const char *value = NULL;
+ enum pcmk__combine combine = pcmk__combine_unknown;
+
+ if ((rule == NULL) || (rule_input == NULL)) {
+ return EINVAL;
+ }
+
+ rule = expand_idref(rule, NULL);
+ if (rule == NULL) {
+ // Not possible with schema validation enabled; message already logged
+ return pcmk_rc_unpack_error;
+ }
+
+ // Validate XML ID
+ id = pcmk__xe_id(rule);
+ if (pcmk__str_empty(id)) {
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * fail the rule
+ */
+ pcmk__config_warn(PCMK_XE_RULE " has no " PCMK_XA_ID);
+ id = "without ID"; // for logging
+ }
+
+ value = crm_element_value(rule, PCMK_XA_BOOLEAN_OP);
+ combine = pcmk__parse_combine(value);
+ switch (combine) {
+ case pcmk__combine_and:
+ // For "and", rc defaults to success (reset on failure below)
+ break;
+
+ case pcmk__combine_or:
+ // For "or", rc defaults to failure (reset on success below)
+ rc = pcmk_rc_op_unsatisfied;
+ break;
+
+ default:
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error
+ */
+ pcmk__config_warn("Rule %s has invalid " PCMK_XA_BOOLEAN_OP
+ " value '%s', using default '" PCMK_VALUE_AND "'",
+ pcmk__xe_id(rule), value);
+ combine = pcmk__combine_and;
+ break;
+ }
+
+ // Evaluate each condition
+ for (xmlNode *condition = pcmk__xe_first_child(rule, NULL, NULL, NULL);
+ condition != NULL; condition = pcmk__xe_next(condition)) {
+
+ empty = false;
+ if (pcmk__evaluate_condition(condition, rule_input,
+ next_change) == pcmk_rc_ok) {
+ if (combine == pcmk__combine_or) {
+ rc = pcmk_rc_ok; // Any pass is final for "or"
+ break;
+ }
+ } else if (combine == pcmk__combine_and) {
+ rc = pcmk_rc_op_unsatisfied; // Any failure is final for "and"
+ break;
+ }
+ }
+
+ if (empty) { // Not possible with schema validation enabled
+ /* @COMPAT Currently, we don't actually ignore "or" rules because
+ * rc is initialized to failure above in that case. When we can break
+ * backward compatibility, reset rc to pcmk_rc_ok here.
+ */
+ pcmk__config_warn("Ignoring rule %s because it contains no conditions",
+ id);
+ }
+
+ crm_trace("Rule %s is %ssatisfied", id, ((rc == pcmk_rc_ok)? "" : "not "));
+ return rc;
+}
diff --git a/lib/common/tests/rules/Makefile.am b/lib/common/tests/rules/Makefile.am
index 0062706d88..41630371da 100644
--- a/lib/common/tests/rules/Makefile.am
+++ b/lib/common/tests/rules/Makefile.am
@@ -1,26 +1,29 @@
#
# Copyright 2020-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
# This source code is licensed under the GNU General Public License version 2
# or later (GPLv2+) WITHOUT ANY WARRANTY.
#
include $(top_srcdir)/mk/tap.mk
include $(top_srcdir)/mk/unittest.mk
# Add "_test" to the end of all test program names to simplify .gitignore.
check_PROGRAMS = pcmk__cmp_by_type_test \
pcmk__evaluate_attr_expression_test \
pcmk__evaluate_date_expression_test \
pcmk__evaluate_date_spec_test \
+ pcmk__evaluate_condition_test \
pcmk__evaluate_op_expression_test \
pcmk__evaluate_rsc_expression_test \
+ pcmk__parse_combine_test \
pcmk__parse_comparison_test \
pcmk__parse_source_test \
pcmk__parse_type_test \
pcmk__replace_submatches_test \
- pcmk__unpack_duration_test
+ pcmk__unpack_duration_test \
+ pcmk_evaluate_rule_test
TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/rules/pcmk__evaluate_condition_test.c b/lib/common/tests/rules/pcmk__evaluate_condition_test.c
new file mode 100644
index 0000000000..025908b0c0
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_condition_test.c
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+
+/*
+ * Shared data
+ */
+
+static pcmk_rule_input_t rule_input = {
+ .rsc_standard = PCMK_RESOURCE_CLASS_OCF,
+ .rsc_provider = "heartbeat",
+ .rsc_agent = "IPaddr2",
+ .op_name = PCMK_ACTION_MONITOR,
+ .op_interval_ms = 10000,
+};
+
+
+/*
+ * Test invalid arguments
+ */
+
+#define EXPR_ATTRIBUTE \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = NULL;
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ assert_int_equal(pcmk__evaluate_condition(NULL, NULL, next_change), EINVAL);
+
+ xml = pcmk__xml_parse(EXPR_ATTRIBUTE);
+ assert_int_equal(pcmk__evaluate_condition(xml, NULL, next_change), EINVAL);
+ free_xml(xml);
+
+ assert_int_equal(pcmk__evaluate_condition(NULL, &rule_input, next_change),
+ EINVAL);
+
+ crm_time_free(next_change);
+}
+
+
+#define EXPR_INVALID "<not_an_expression " PCMK_XA_ID "='e' />"
+
+static void
+invalid_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_INVALID);
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, next_change),
+ pcmk_rc_unpack_error);
+
+ crm_time_free(next_change);
+ free_xml(xml);
+}
+
+
+/* Each expression type function already has unit tests, so we just need to test
+ * that they are called correctly (essentially, one of each one's own tests).
+ */
+
+static void
+attribute_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_ATTRIBUTE);
+
+ rule_input.node_attrs = pcmk__strkey_table(free, free);
+ pcmk__insert_dup(rule_input.node_attrs, "foo", "bar");
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+
+ g_hash_table_destroy(rule_input.node_attrs);
+ rule_input.node_attrs = NULL;
+ free_xml(xml);
+}
+
+#define EXPR_LOCATION \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='" CRM_ATTR_UNAME "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='node1' />"
+
+static void
+location_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_LOCATION);
+
+ rule_input.node_attrs = pcmk__strkey_table(free, free);
+ pcmk__insert_dup(rule_input.node_attrs, CRM_ATTR_UNAME, "node1");
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+
+ g_hash_table_destroy(rule_input.node_attrs);
+ rule_input.node_attrs = NULL;
+ free_xml(xml);
+}
+
+#define EXPR_DATE \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+date_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_DATE);
+ crm_time_t *now = crm_time_new("2024-02-01 11:59:59");
+ crm_time_t *next_change = crm_time_new("2024-02-01 14:00:00");
+ crm_time_t *reference = crm_time_new("2024-02-01 12:00:00");
+
+ rule_input.now = now;
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, next_change),
+ pcmk_rc_before_range);
+ assert_int_equal(crm_time_compare(next_change, reference), 0);
+ rule_input.now = NULL;
+
+ crm_time_free(reference);
+ crm_time_free(next_change);
+ crm_time_free(now);
+}
+
+#define EXPR_RESOURCE \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+resource_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_RESOURCE);
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+ free_xml(xml);
+}
+
+#define EXPR_OP \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+static void
+op_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_OP);
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+ free_xml(xml);
+}
+
+#define EXPR_SUBRULE \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' /> />"
+
+static void
+subrule(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_SUBRULE);
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(invalid_expression),
+ cmocka_unit_test(attribute_expression),
+ cmocka_unit_test(location_expression),
+ cmocka_unit_test(date_expression),
+ cmocka_unit_test(resource_expression),
+ cmocka_unit_test(op_expression),
+ cmocka_unit_test(subrule))
diff --git a/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c
index a95c1ef80f..71b9fe4ae5 100644
--- a/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c
+++ b/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c
@@ -1,226 +1,227 @@
/*
* Copyright 2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <stdio.h>
#include <glib.h>
#include <crm/common/xml.h>
#include <crm/common/rules_internal.h>
#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
/*
* Shared data
*/
static pcmk_rule_input_t rule_input = {
// These are the only members used to evaluate resource expressions
.rsc_standard = PCMK_RESOURCE_CLASS_OCF,
.rsc_provider = "heartbeat",
.rsc_agent = "IPaddr2",
};
/*!
* \internal
* \brief Run one test, comparing return value
*
* \param[in] xml_string Resource expression XML as string
* \param[in] reference_rc Assert that evaluation result equals this
*/
static void
assert_rsc_expression(const char *xml_string, int reference_rc)
{
xmlNode *xml = pcmk__xml_parse(xml_string);
assert_int_equal(pcmk__evaluate_rsc_expression(xml, &rule_input),
reference_rc);
free_xml(xml);
}
/*
* Invalid arguments
*/
#define EXPR_ALL_MATCH \
"<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
PCMK_XA_PROVIDER "='heartbeat' " \
PCMK_XA_TYPE "='IPaddr2' />"
static void
null_invalid(void **state)
{
xmlNode *xml = NULL;
assert_int_equal(pcmk__evaluate_rsc_expression(NULL, NULL), EINVAL);
xml = pcmk__xml_parse(EXPR_ALL_MATCH);
assert_int_equal(pcmk__evaluate_rsc_expression(xml, NULL), EINVAL);
free_xml(xml);
assert_rsc_expression(NULL, EINVAL);
}
/*
* Test PCMK_XA_ID
*/
#define EXPR_ID_MISSING \
"<" PCMK_XE_RSC_EXPRESSION " " \
PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
PCMK_XA_PROVIDER "='heartbeat' " \
PCMK_XA_TYPE "='IPaddr2' />"
#define EXPR_ID_EMPTY \
"<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='' " \
PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
PCMK_XA_PROVIDER "='heartbeat' " \
PCMK_XA_TYPE "='IPaddr2' />"
static void
id_missing(void **state)
{
// Currently acceptable
assert_rsc_expression(EXPR_ID_MISSING, pcmk_rc_ok);
assert_rsc_expression(EXPR_ID_EMPTY, pcmk_rc_ok);
}
/*
* Test standard, provider, and agent
*/
#define EXPR_FAIL_STANDARD \
"<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_LSB "' />"
#define EXPR_EMPTY_STANDARD \
"<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
PCMK_XA_CLASS "='' />"
static void
fail_standard(void **state)
{
assert_rsc_expression(EXPR_FAIL_STANDARD, pcmk_rc_op_unsatisfied);
assert_rsc_expression(EXPR_EMPTY_STANDARD, pcmk_rc_op_unsatisfied);
rule_input.rsc_standard = NULL;
assert_rsc_expression(EXPR_FAIL_STANDARD, pcmk_rc_op_unsatisfied);
assert_rsc_expression(EXPR_EMPTY_STANDARD, pcmk_rc_op_unsatisfied);
rule_input.rsc_standard = PCMK_RESOURCE_CLASS_OCF;
}
#define EXPR_FAIL_PROVIDER \
"<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
PCMK_XA_PROVIDER "='pacemaker' " \
PCMK_XA_TYPE "='IPaddr2' />"
#define EXPR_EMPTY_PROVIDER \
"<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
PCMK_XA_PROVIDER "='' " PCMK_XA_TYPE "='IPaddr2' />"
static void
fail_provider(void **state)
{
assert_rsc_expression(EXPR_FAIL_PROVIDER, pcmk_rc_op_unsatisfied);
assert_rsc_expression(EXPR_EMPTY_PROVIDER, pcmk_rc_op_unsatisfied);
rule_input.rsc_provider = NULL;
assert_rsc_expression(EXPR_FAIL_PROVIDER, pcmk_rc_op_unsatisfied);
assert_rsc_expression(EXPR_EMPTY_PROVIDER, pcmk_rc_op_unsatisfied);
rule_input.rsc_provider = "heartbeat";
}
#define EXPR_FAIL_AGENT \
"<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
PCMK_XA_PROVIDER "='heartbeat' " \
PCMK_XA_TYPE "='IPaddr3' />"
#define EXPR_EMPTY_AGENT \
"<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
PCMK_XA_PROVIDER "='heartbeat' " PCMK_XA_TYPE "='' />"
static void
fail_agent(void **state)
{
assert_rsc_expression(EXPR_FAIL_AGENT, pcmk_rc_op_unsatisfied);
assert_rsc_expression(EXPR_EMPTY_AGENT, pcmk_rc_op_unsatisfied);
rule_input.rsc_agent = NULL;
assert_rsc_expression(EXPR_FAIL_AGENT, pcmk_rc_op_unsatisfied);
assert_rsc_expression(EXPR_EMPTY_AGENT, pcmk_rc_op_unsatisfied);
rule_input.rsc_agent = "IPaddr2";
}
#define EXPR_NO_STANDARD_MATCHES \
"<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
PCMK_XA_PROVIDER "='heartbeat' " \
PCMK_XA_TYPE "='IPaddr2' />"
static void
no_standard_matches(void **state)
{
assert_rsc_expression(EXPR_NO_STANDARD_MATCHES, pcmk_rc_ok);
}
#define EXPR_NO_PROVIDER_MATCHES \
"<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
PCMK_XA_TYPE "='IPaddr2' />"
static void
no_provider_matches(void **state)
{
assert_rsc_expression(EXPR_NO_PROVIDER_MATCHES, pcmk_rc_ok);
}
#define EXPR_NO_AGENT_MATCHES \
"<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
PCMK_XA_PROVIDER "='heartbeat' />"
static void
no_agent_matches(void **state)
{
assert_rsc_expression(EXPR_NO_AGENT_MATCHES, pcmk_rc_ok);
}
#define EXPR_NO_CRITERIA_MATCHES \
"<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' />"
static void
no_criteria_matches(void **state)
{
assert_rsc_expression(EXPR_NO_CRITERIA_MATCHES, pcmk_rc_ok);
}
static void
all_match(void **state)
{
assert_rsc_expression(EXPR_ALL_MATCH, pcmk_rc_ok);
}
PCMK__UNIT_TEST(NULL, NULL,
cmocka_unit_test(null_invalid),
cmocka_unit_test(id_missing),
cmocka_unit_test(fail_standard),
cmocka_unit_test(fail_provider),
cmocka_unit_test(fail_agent),
cmocka_unit_test(no_standard_matches),
cmocka_unit_test(no_provider_matches),
cmocka_unit_test(no_agent_matches),
cmocka_unit_test(no_criteria_matches),
cmocka_unit_test(all_match))
diff --git a/lib/common/tests/rules/pcmk__parse_combine_test.c b/lib/common/tests/rules/pcmk__parse_combine_test.c
new file mode 100644
index 0000000000..afebcf8f3f
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__parse_combine_test.c
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+default_and(void **state)
+{
+ assert_int_equal(pcmk__parse_combine(NULL), pcmk__combine_and);
+}
+
+static void
+invalid(void **state)
+{
+ assert_int_equal(pcmk__parse_combine(""), pcmk__combine_unknown);
+ assert_int_equal(pcmk__parse_combine(" "), pcmk__combine_unknown);
+ assert_int_equal(pcmk__parse_combine("but"), pcmk__combine_unknown);
+}
+
+static void
+valid(void **state)
+{
+ assert_int_equal(pcmk__parse_combine(PCMK_VALUE_AND), pcmk__combine_and);
+ assert_int_equal(pcmk__parse_combine(PCMK_VALUE_OR), pcmk__combine_or);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_int_equal(pcmk__parse_combine("And"),
+ pcmk__combine_and);
+
+ assert_int_equal(pcmk__parse_combine("OR"),
+ pcmk__combine_or);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(default_and),
+ cmocka_unit_test(invalid),
+ cmocka_unit_test(valid),
+ cmocka_unit_test(case_insensitive))
diff --git a/lib/common/tests/rules/pcmk_evaluate_rule_test.c b/lib/common/tests/rules/pcmk_evaluate_rule_test.c
new file mode 100644
index 0000000000..ac2cf8b522
--- /dev/null
+++ b/lib/common/tests/rules/pcmk_evaluate_rule_test.c
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+
+/*
+ * Shared data
+ */
+
+static pcmk_rule_input_t rule_input = {
+ .rsc_standard = PCMK_RESOURCE_CLASS_OCF,
+ .rsc_provider = "heartbeat",
+ .rsc_agent = "IPaddr2",
+ .op_name = PCMK_ACTION_MONITOR,
+ .op_interval_ms = 10000,
+};
+
+
+/*
+ * Test invalid arguments
+ */
+
+#define RULE_OP \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' > " \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = NULL;
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ assert_int_equal(pcmk_evaluate_rule(NULL, NULL, next_change),
+ EINVAL);
+
+ xml = pcmk__xml_parse(RULE_OP);
+ assert_int_equal(pcmk_evaluate_rule(xml, NULL, next_change), EINVAL);
+ free_xml(xml);
+
+ assert_int_equal(pcmk_evaluate_rule(NULL, &rule_input, next_change),
+ EINVAL);
+
+ crm_time_free(next_change);
+}
+
+#define RULE_OP_MISSING_ID \
+ "<" PCMK_XE_RULE "> " \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+id_missing(void **state)
+{
+ // Currently acceptable
+ xmlNode *xml = pcmk__xml_parse(RULE_OP_MISSING_ID);
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, next_change),
+ pcmk_rc_ok);
+
+ crm_time_free(next_change);
+ free_xml(xml);
+}
+
+#define RULE_IDREF_PARENT "<" PCMK_XE_CIB ">" RULE_OP "</" PCMK_XE_CIB ">"
+
+static void
+good_idref(void **state)
+{
+ xmlNode *parent_xml = pcmk__xml_parse(RULE_IDREF_PARENT);
+ xmlNode *rule_xml = pcmk__xe_create(parent_xml, PCMK_XE_RULE);
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ crm_xml_add(rule_xml, PCMK_XA_ID_REF, "r");
+ assert_int_equal(pcmk_evaluate_rule(rule_xml, &rule_input, next_change),
+ pcmk_rc_ok);
+
+ crm_time_free(next_change);
+ free_xml(parent_xml);
+}
+
+static void
+bad_idref(void **state)
+{
+ xmlNode *parent_xml = pcmk__xml_parse(RULE_IDREF_PARENT);
+ xmlNode *rule_xml = pcmk__xe_create(parent_xml, PCMK_XE_RULE);
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ crm_xml_add(rule_xml, PCMK_XA_ID_REF, "x");
+ assert_int_equal(pcmk_evaluate_rule(rule_xml, &rule_input, next_change),
+ pcmk_rc_unpack_error);
+
+ crm_time_free(next_change);
+ free_xml(parent_xml);
+}
+
+#define RULE_EMPTY "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' />"
+
+static void
+empty_default(void **state)
+{
+ // Currently acceptable
+ xmlNode *xml = pcmk__xml_parse(RULE_EMPTY);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_EMPTY_AND \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' />"
+
+static void
+empty_and(void **state)
+{
+ // Currently acceptable
+ xmlNode *xml = pcmk__xml_parse(RULE_EMPTY_AND);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_EMPTY_OR \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' />"
+
+static void
+empty_or(void **state)
+{
+ // Currently treated as unsatisfied
+ xmlNode *xml = pcmk__xml_parse(RULE_EMPTY_OR);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_DEFAULT_BOOLEAN_OP \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+default_boolean_op(void **state)
+{
+ // Defaults to PCMK_VALUE_AND
+ xmlNode *xml = pcmk__xml_parse(RULE_DEFAULT_BOOLEAN_OP);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_INVALID_BOOLEAN_OP \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='not-an-op' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+invalid_boolean_op(void **state)
+{
+ // Currently defaults to PCMK_VALUE_AND
+ xmlNode *xml = pcmk__xml_parse(RULE_INVALID_BOOLEAN_OP);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_AND_PASSES \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='IPaddr2' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+and_passes(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_AND_PASSES);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_LONELY_AND \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='IPaddr2' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+lonely_and_passes(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_LONELY_AND);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_AND_ONE_FAILS \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+and_one_fails(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_AND_ONE_FAILS);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_AND_TWO_FAIL \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='9s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+and_two_fail(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_AND_TWO_FAIL);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_OR_ONE_PASSES \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+or_one_passes(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_OR_ONE_PASSES);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_OR_TWO_PASS \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='IPAddr2' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+or_two_pass(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_OR_TWO_PASS);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_LONELY_OR \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+lonely_or_passes(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_LONELY_OR);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_OR_FAILS \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='20s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+or_fails(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_OR_FAILS);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(id_missing),
+ cmocka_unit_test(good_idref),
+ cmocka_unit_test(bad_idref),
+ cmocka_unit_test(empty_default),
+ cmocka_unit_test(empty_and),
+ cmocka_unit_test(empty_or),
+ cmocka_unit_test(default_boolean_op),
+ cmocka_unit_test(invalid_boolean_op),
+ cmocka_unit_test(and_passes),
+ cmocka_unit_test(lonely_and_passes),
+ cmocka_unit_test(and_one_fails),
+ cmocka_unit_test(and_two_fail),
+ cmocka_unit_test(or_one_passes),
+ cmocka_unit_test(or_two_pass),
+ cmocka_unit_test(lonely_or_passes),
+ cmocka_unit_test(or_fails))
diff --git a/lib/pacemaker/pcmk_rule.c b/lib/pacemaker/pcmk_rule.c
index 0795d81e41..cda8774cc0 100644
--- a/lib/pacemaker/pcmk_rule.c
+++ b/lib/pacemaker/pcmk_rule.c
@@ -1,214 +1,214 @@
/*
* Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <crm/cib/internal.h>
#include <crm/common/cib.h>
#include <crm/common/iso8601.h>
#include <crm/common/xml.h>
#include <crm/pengine/internal.h>
#include <crm/pengine/rules_internal.h>
#include <pacemaker-internal.h>
#include "libpacemaker_private.h"
#define XPATH_NODE_RULE "//" PCMK_XE_RULE "[@" PCMK_XA_ID "='%s']"
/*!
* \internal
* \brief Check whether a given rule is in effect
*
* \param[in] scheduler Scheduler data
* \param[in] rule_id The ID of the rule to check
* \param[out] error Where to store a rule evaluation error message
*
* \return Standard Pacemaker return code
*/
static int
eval_rule(pcmk_scheduler_t *scheduler, const char *rule_id, const char **error)
{
xmlNodePtr cib_constraints = NULL;
xmlNodePtr match = NULL;
xmlXPathObjectPtr xpath_obj = NULL;
char *xpath = NULL;
int rc = pcmk_rc_ok;
int num_results = 0;
*error = NULL;
/* Rules are under the constraints node in the XML, so first find that. */
cib_constraints = pcmk_find_cib_element(scheduler->input,
PCMK_XE_CONSTRAINTS);
/* Get all rules matching the given ID that are also simple enough for us
* to check. For the moment, these rules must only have a single
* date_expression child and:
* - Do not have a date_spec operation, or
* - Have a date_spec operation that contains years= but does not contain
* moon=.
*
* We do this in steps to provide better error messages. First, check that
* there's any rule with the given ID.
*/
xpath = crm_strdup_printf(XPATH_NODE_RULE, rule_id);
xpath_obj = xpath_search(cib_constraints, xpath);
num_results = numXpathResults(xpath_obj);
free(xpath);
freeXpathObject(xpath_obj);
if (num_results == 0) {
*error = "Rule not found";
return ENXIO;
}
if (num_results > 1) {
// Should not be possible; schema prevents this
*error = "Found more than one rule with matching ID";
return pcmk_rc_duplicate_id;
}
/* Next, make sure it has exactly one date_expression. */
xpath = crm_strdup_printf(XPATH_NODE_RULE "//date_expression", rule_id);
xpath_obj = xpath_search(cib_constraints, xpath);
num_results = numXpathResults(xpath_obj);
free(xpath);
freeXpathObject(xpath_obj);
if (num_results != 1) {
if (num_results == 0) {
*error = "Rule does not have a date expression";
} else {
*error = "Rule has more than one date expression";
}
return EOPNOTSUPP;
}
/* Then, check that it's something we actually support. */
xpath = crm_strdup_printf(XPATH_NODE_RULE
"//" PCMK_XE_DATE_EXPRESSION
"[@" PCMK_XA_OPERATION
"!='" PCMK_VALUE_DATE_SPEC "']",
rule_id);
xpath_obj = xpath_search(cib_constraints, xpath);
num_results = numXpathResults(xpath_obj);
free(xpath);
if (num_results == 0) {
freeXpathObject(xpath_obj);
xpath = crm_strdup_printf(XPATH_NODE_RULE
"//" PCMK_XE_DATE_EXPRESSION
"[@" PCMK_XA_OPERATION
"='" PCMK_VALUE_DATE_SPEC "' "
"and " PCMK_XE_DATE_SPEC
"/@" PCMK_XA_YEARS " "
"and not(" PCMK_XE_DATE_SPEC
"/@" PCMK__XA_MOON ")]",
rule_id);
xpath_obj = xpath_search(cib_constraints, xpath);
num_results = numXpathResults(xpath_obj);
free(xpath);
if (num_results == 0) {
freeXpathObject(xpath_obj);
*error = "Rule must either not use " PCMK_XE_DATE_SPEC ", or use "
PCMK_XE_DATE_SPEC " with " PCMK_XA_YEARS "= but not "
PCMK__XA_MOON "=";
return EOPNOTSUPP;
}
}
match = getXpathResult(xpath_obj, 0);
/* We should have ensured this with the xpath query above, but double-
* checking can't hurt.
*/
CRM_ASSERT(match != NULL);
- CRM_ASSERT(pcmk__expression_type(match) == pcmk__subexpr_datetime);
+ CRM_ASSERT(pcmk__condition_type(match) == pcmk__condition_datetime);
rc = pcmk__evaluate_date_expression(match, scheduler->now, NULL);
if (rc == pcmk_rc_undetermined) { // Malformed or missing
*error = "Error parsing rule";
}
freeXpathObject(xpath_obj);
return rc;
}
/*!
* \internal
* \brief Check whether each rule in a list is in effect
*
* \param[in,out] out Output object
* \param[in] input The CIB XML to check (if \c NULL, use current CIB)
* \param[in] date Check whether the rule is in effect at this date and
* time (if \c NULL, use current date and time)
* \param[in] rule_ids The IDs of the rules to check, as a <tt>NULL</tt>-
* terminated list.
*
* \return Standard Pacemaker return code
*/
int
pcmk__check_rules(pcmk__output_t *out, xmlNodePtr input, const crm_time_t *date,
const char **rule_ids)
{
pcmk_scheduler_t *scheduler = NULL;
int rc = pcmk_rc_ok;
CRM_ASSERT(out != NULL);
if (rule_ids == NULL) {
// Trivial case; every rule specified is in effect
return pcmk_rc_ok;
}
rc = pcmk__init_scheduler(out, input, date, &scheduler);
if (rc != pcmk_rc_ok) {
return rc;
}
for (const char **rule_id = rule_ids; *rule_id != NULL; rule_id++) {
const char *error = NULL;
int last_rc = eval_rule(scheduler, *rule_id, &error);
out->message(out, "rule-check", *rule_id, last_rc, error);
if (last_rc != pcmk_rc_ok) {
rc = last_rc;
}
}
pe_free_working_set(scheduler);
return rc;
}
// Documented in pacemaker.h
int
pcmk_check_rules(xmlNodePtr *xml, xmlNodePtr input, const crm_time_t *date,
const char **rule_ids)
{
pcmk__output_t *out = NULL;
int rc = pcmk_rc_ok;
rc = pcmk__xml_output_new(&out, xml);
if (rc != pcmk_rc_ok) {
return rc;
}
pcmk__register_lib_messages(out);
rc = pcmk__check_rules(out, input, date, rule_ids);
pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml);
return rc;
}
diff --git a/lib/pacemaker/pcmk_sched_location.c b/lib/pacemaker/pcmk_sched_location.c
index f31126ee9a..00dce25532 100644
--- a/lib/pacemaker/pcmk_sched_location.c
+++ b/lib/pacemaker/pcmk_sched_location.c
@@ -1,735 +1,750 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <stdbool.h>
#include <glib.h>
#include <crm/crm.h>
+#include <crm/common/rules_internal.h>
#include <crm/pengine/status.h>
#include <crm/pengine/rules.h>
#include <pacemaker-internal.h>
#include "libpacemaker_private.h"
static int
get_node_score(const char *rule, const char *score, bool raw,
pcmk_node_t *node, pcmk_resource_t *rsc)
{
int score_f = 0;
if (score == NULL) {
pcmk__config_warn("Rule %s: no score specified (assuming 0)", rule);
} else if (raw) {
score_f = char2score(score);
} else {
const char *target = NULL;
const char *attr_score = NULL;
target = g_hash_table_lookup(rsc->meta,
PCMK_META_CONTAINER_ATTRIBUTE_TARGET);
attr_score = pcmk__node_attr(node, score, target,
pcmk__rsc_node_current);
if (attr_score == NULL) {
crm_debug("Rule %s: %s did not have a value for %s",
rule, pcmk__node_name(node), score);
score_f = -PCMK_SCORE_INFINITY;
} else {
crm_debug("Rule %s: %s had value %s for %s",
rule, pcmk__node_name(node), attr_score, score);
score_f = char2score(attr_score);
}
}
return score_f;
}
/*!
* \internal
* \brief Parse a role configuration for a location constraint
*
* \param[in] role_spec Role specification
* \param[out] role Where to store parsed role
*
* \return true if role specification is valid, otherwise false
*/
static bool
parse_location_role(const char *role_spec, enum rsc_role_e *role)
{
if (role_spec == NULL) {
*role = pcmk_role_unknown;
return true;
}
*role = pcmk_parse_role(role_spec);
switch (*role) {
case pcmk_role_unknown:
return false;
case pcmk_role_started:
case pcmk_role_unpromoted:
/* Any promotable clone instance cannot be promoted without being in
* the unpromoted role first. Therefore, any constraint for the
* started or unpromoted role applies to every role.
*/
*role = pcmk_role_unknown;
break;
default:
break;
}
return true;
}
/*!
* \internal
* \brief Generate a location constraint from a rule
*
* \param[in,out] rsc Resource that constraint is for
* \param[in] rule_xml Rule XML (sub-element of location constraint)
* \param[in] discovery Value of \c PCMK_XA_RESOURCE_DISCOVERY for
* constraint
* \param[out] next_change Where to set when rule evaluation will change
* \param[in] re_match_data Regular expression submatches
*
* \return New location constraint if rule is valid, otherwise NULL
*/
static pcmk__location_t *
generate_location_rule(pcmk_resource_t *rsc, xmlNode *rule_xml,
const char *discovery, crm_time_t *next_change,
pe_re_match_data_t *re_match_data)
{
const char *rule_id = NULL;
const char *score = NULL;
const char *boolean = NULL;
const char *role_spec = NULL;
GList *iter = NULL;
GList *nodes = NULL;
- bool do_and = true;
- bool accept = true;
bool raw_score = true;
bool score_allocated = false;
pcmk__location_t *location_rule = NULL;
enum rsc_role_e role = pcmk_role_unknown;
+ enum pcmk__combine combine = pcmk__combine_unknown;
rule_xml = expand_idref(rule_xml, rsc->cluster->input);
if (rule_xml == NULL) {
return NULL; // Error already logged
}
rule_id = crm_element_value(rule_xml, PCMK_XA_ID);
boolean = crm_element_value(rule_xml, PCMK_XA_BOOLEAN_OP);
role_spec = crm_element_value(rule_xml, PCMK_XA_ROLE);
if (parse_location_role(role_spec, &role)) {
crm_trace("Setting rule %s role filter to %s", rule_id, role_spec);
} else {
pcmk__config_err("Ignoring rule %s: Invalid " PCMK_XA_ROLE " '%s'",
rule_id, role_spec);
return NULL;
}
crm_trace("Processing location constraint rule %s", rule_id);
score = crm_element_value(rule_xml, PCMK_XA_SCORE);
if (score == NULL) {
score = crm_element_value(rule_xml, PCMK_XA_SCORE_ATTRIBUTE);
if (score != NULL) {
raw_score = false;
}
}
- if (pcmk__str_eq(boolean, PCMK_VALUE_OR, pcmk__str_casei)) {
- do_and = false;
+ combine = pcmk__parse_combine(boolean);
+ switch (combine) {
+ case pcmk__combine_and:
+ case pcmk__combine_or:
+ break;
- } else if (!pcmk__str_eq(boolean, PCMK_VALUE_AND,
- pcmk__str_null_matches|pcmk__str_casei)) {
- pcmk__config_warn("Location constraint rule %s has invalid "
- PCMK_XA_BOOLEAN_OP " value '%s', using default "
- "('" PCMK_VALUE_AND "')",
- rule_id, boolean);
+ default:
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return NULL
+ */
+ pcmk__config_warn("Location constraint rule %s has invalid "
+ PCMK_XA_BOOLEAN_OP " value '%s', using default "
+ "'" PCMK_VALUE_AND "'",
+ rule_id, boolean);
+ combine = pcmk__combine_and;
+ break;
}
location_rule = pcmk__new_location(rule_id, rsc, 0, discovery, NULL);
if (location_rule == NULL) {
return NULL; // Error already logged
}
location_rule->role_filter = role;
if ((re_match_data != NULL) && (re_match_data->nregs > 0)
&& (re_match_data->pmatch[0].rm_so != -1) && !raw_score) {
char *result = pcmk__replace_submatches(score, re_match_data->string,
re_match_data->pmatch,
re_match_data->nregs);
if (result != NULL) {
score = result;
score_allocated = true;
}
}
- if (do_and) {
+ if (combine == pcmk__combine_and) {
nodes = pcmk__copy_node_list(rsc->cluster->nodes, true);
for (iter = nodes; iter != NULL; iter = iter->next) {
pcmk_node_t *node = iter->data;
node->weight = get_node_score(rule_id, score, raw_score, node, rsc);
}
}
for (iter = rsc->cluster->nodes; iter != NULL; iter = iter->next) {
+ int rc = pcmk_rc_ok;
int score_f = 0;
pcmk_node_t *node = iter->data;
- pe_match_data_t match_data = {
- .re = re_match_data,
- .params = pe_rsc_params(rsc, node, rsc->cluster),
- .meta = rsc->meta,
+ pcmk_rule_input_t rule_input = {
+ .now = rsc->cluster->now,
+ .node_attrs = node->details->attrs,
+ .rsc_params = pe_rsc_params(rsc, node, rsc->cluster),
+ .rsc_meta = rsc->meta,
};
- accept = pe_test_rule(rule_xml, node->details->attrs, pcmk_role_unknown,
- rsc->cluster->now, next_change, &match_data);
+ if (re_match_data != NULL) {
+ rule_input.rsc_id = re_match_data->string;
+ rule_input.rsc_id_submatches = re_match_data->pmatch;
+ rule_input.rsc_id_nmatches = re_match_data->nregs;
+ }
+
+ rc = pcmk_evaluate_rule(rule_xml, &rule_input, next_change);
crm_trace("Rule %s %s on %s",
- pcmk__xe_id(rule_xml), (accept? "passed" : "failed"),
+ pcmk__xe_id(rule_xml),
+ ((rc == pcmk_rc_ok)? "passed" : "failed"),
pcmk__node_name(node));
score_f = get_node_score(rule_id, score, raw_score, node, rsc);
- if (accept) {
+ if (rc == pcmk_rc_ok) {
pcmk_node_t *local = pe_find_node_id(nodes, node->details->id);
- if ((local == NULL) && do_and) {
+ if ((local == NULL) && (combine == pcmk__combine_and)) {
continue;
} else if (local == NULL) {
local = pe__copy_node(node);
nodes = g_list_append(nodes, local);
}
- if (!do_and) {
+ if (combine == pcmk__combine_or) {
local->weight = pcmk__add_scores(local->weight, score_f);
}
crm_trace("%s has score %s after %s", pcmk__node_name(node),
pcmk_readable_score(local->weight), rule_id);
- } else if (do_and && !accept) {
+ } else if (combine == pcmk__combine_and) {
// Remove it
pcmk_node_t *delete = pe_find_node_id(nodes, node->details->id);
if (delete != NULL) {
nodes = g_list_remove(nodes, delete);
crm_trace("%s did not match", pcmk__node_name(node));
}
free(delete);
}
}
if (score_allocated) {
free((char *)score);
}
location_rule->nodes = nodes;
if (location_rule->nodes == NULL) {
crm_trace("No matching nodes for location constraint rule %s", rule_id);
return NULL;
} else {
crm_trace("Location constraint rule %s matched %d nodes",
rule_id, g_list_length(location_rule->nodes));
}
return location_rule;
}
static void
unpack_rsc_location(xmlNode *xml_obj, pcmk_resource_t *rsc,
const char *role_spec, const char *score,
pe_re_match_data_t *re_match_data)
{
const char *rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC);
const char *id = crm_element_value(xml_obj, PCMK_XA_ID);
const char *node = crm_element_value(xml_obj, PCMK_XE_NODE);
const char *discovery = crm_element_value(xml_obj,
PCMK_XA_RESOURCE_DISCOVERY);
if (rsc == NULL) {
pcmk__config_warn("Ignoring constraint '%s' because resource '%s' "
"does not exist", id, rsc_id);
return;
}
if (score == NULL) {
score = crm_element_value(xml_obj, PCMK_XA_SCORE);
}
if ((node != NULL) && (score != NULL)) {
int score_i = char2score(score);
pcmk_node_t *match = pe_find_node(rsc->cluster->nodes, node);
enum rsc_role_e role = pcmk_role_unknown;
pcmk__location_t *location = NULL;
if (!match) {
return;
}
if (role_spec == NULL) {
role_spec = crm_element_value(xml_obj, PCMK_XA_ROLE);
}
if (parse_location_role(role_spec, &role)) {
crm_trace("Setting location constraint %s role filter: %s",
id, role_spec);
} else {
/* @COMPAT The previous behavior of creating the constraint ignoring
* the role is retained for now, but we should ignore the entire
* constraint when we can break backward compatibility.
*/
pcmk__config_err("Ignoring role in constraint %s: "
"Invalid value '%s'", id, role_spec);
}
location = pcmk__new_location(id, rsc, score_i, discovery, match);
if (location == NULL) {
return; // Error already logged
}
location->role_filter = role;
} else {
bool empty = true;
crm_time_t *next_change = crm_time_new_undefined();
/* This loop is logically parallel to pe_evaluate_rules(), except
* instead of checking whether any rule is active, we set up location
* constraints for each active rule.
*/
for (xmlNode *rule_xml = pcmk__xe_first_child(xml_obj, PCMK_XE_RULE,
NULL, NULL);
rule_xml != NULL; rule_xml = pcmk__xe_next_same(rule_xml)) {
empty = false;
crm_trace("Unpacking %s/%s", id, pcmk__xe_id(rule_xml));
generate_location_rule(rsc, rule_xml, discovery, next_change,
re_match_data);
}
if (empty) {
pcmk__config_err("Ignoring constraint '%s' because it contains "
"no rules", id);
}
/* If there is a point in the future when the evaluation of a rule will
* change, make sure the scheduler is re-run by that time.
*/
if (crm_time_is_defined(next_change)) {
time_t t = (time_t) crm_time_get_seconds_since_epoch(next_change);
pe__update_recheck_time(t, rsc->cluster,
"location rule evaluation");
}
crm_time_free(next_change);
}
}
static void
unpack_simple_location(xmlNode *xml_obj, pcmk_scheduler_t *scheduler)
{
const char *id = crm_element_value(xml_obj, PCMK_XA_ID);
const char *value = crm_element_value(xml_obj, PCMK_XA_RSC);
if (value) {
pcmk_resource_t *rsc;
rsc = pcmk__find_constraint_resource(scheduler->resources, value);
unpack_rsc_location(xml_obj, rsc, NULL, NULL, NULL);
}
value = crm_element_value(xml_obj, PCMK_XA_RSC_PATTERN);
if (value) {
regex_t *r_patt = pcmk__assert_alloc(1, sizeof(regex_t));
bool invert = false;
if (value[0] == '!') {
value++;
invert = true;
}
if (regcomp(r_patt, value, REG_EXTENDED) != 0) {
pcmk__config_err("Ignoring constraint '%s' because "
PCMK_XA_RSC_PATTERN
" has invalid value '%s'", id, value);
free(r_patt);
return;
}
for (GList *iter = scheduler->resources; iter != NULL;
iter = iter->next) {
pcmk_resource_t *r = iter->data;
int nregs = 0;
regmatch_t *pmatch = NULL;
int status;
if (r_patt->re_nsub > 0) {
nregs = r_patt->re_nsub + 1;
} else {
nregs = 1;
}
pmatch = pcmk__assert_alloc(nregs, sizeof(regmatch_t));
status = regexec(r_patt, r->id, nregs, pmatch, 0);
if (!invert && (status == 0)) {
pe_re_match_data_t re_match_data = {
.string = r->id,
.nregs = nregs,
.pmatch = pmatch
};
crm_debug("'%s' matched '%s' for %s", r->id, value, id);
unpack_rsc_location(xml_obj, r, NULL, NULL, &re_match_data);
} else if (invert && (status != 0)) {
crm_debug("'%s' is an inverted match of '%s' for %s",
r->id, value, id);
unpack_rsc_location(xml_obj, r, NULL, NULL, NULL);
} else {
crm_trace("'%s' does not match '%s' for %s", r->id, value, id);
}
free(pmatch);
}
regfree(r_patt);
free(r_patt);
}
}
// \return Standard Pacemaker return code
static int
unpack_location_tags(xmlNode *xml_obj, xmlNode **expanded_xml,
pcmk_scheduler_t *scheduler)
{
const char *id = NULL;
const char *rsc_id = NULL;
const char *state = NULL;
pcmk_resource_t *rsc = NULL;
pcmk_tag_t *tag = NULL;
xmlNode *rsc_set = NULL;
*expanded_xml = NULL;
CRM_CHECK(xml_obj != NULL, return EINVAL);
id = pcmk__xe_id(xml_obj);
if (id == NULL) {
pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID,
xml_obj->name);
return pcmk_rc_unpack_error;
}
// Check whether there are any resource sets with template or tag references
*expanded_xml = pcmk__expand_tags_in_sets(xml_obj, scheduler);
if (*expanded_xml != NULL) {
crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_LOCATION);
return pcmk_rc_ok;
}
rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC);
if (rsc_id == NULL) {
return pcmk_rc_ok;
}
if (!pcmk__valid_resource_or_tag(scheduler, rsc_id, &rsc, &tag)) {
pcmk__config_err("Ignoring constraint '%s' because '%s' is not a "
"valid resource or tag", id, rsc_id);
return pcmk_rc_unpack_error;
} else if (rsc != NULL) {
// No template is referenced
return pcmk_rc_ok;
}
state = crm_element_value(xml_obj, PCMK_XA_ROLE);
*expanded_xml = pcmk__xml_copy(NULL, xml_obj);
/* Convert any template or tag reference into constraint
* PCMK_XE_RESOURCE_SET
*/
if (!pcmk__tag_to_set(*expanded_xml, &rsc_set, PCMK_XA_RSC,
false, scheduler)) {
free_xml(*expanded_xml);
*expanded_xml = NULL;
return pcmk_rc_unpack_error;
}
if (rsc_set != NULL) {
if (state != NULL) {
/* Move PCMK_XA_RSC_ROLE into converted PCMK_XE_RESOURCE_SET as
* PCMK_XA_ROLE attribute
*/
crm_xml_add(rsc_set, PCMK_XA_ROLE, state);
pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_ROLE);
}
crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_LOCATION);
} else {
// No sets
free_xml(*expanded_xml);
*expanded_xml = NULL;
}
return pcmk_rc_ok;
}
// \return Standard Pacemaker return code
static int
unpack_location_set(xmlNode *location, xmlNode *set,
pcmk_scheduler_t *scheduler)
{
xmlNode *xml_rsc = NULL;
pcmk_resource_t *resource = NULL;
const char *set_id;
const char *role;
const char *local_score;
CRM_CHECK(set != NULL, return EINVAL);
set_id = pcmk__xe_id(set);
if (set_id == NULL) {
pcmk__config_err("Ignoring " PCMK_XE_RESOURCE_SET " without "
PCMK_XA_ID " in constraint '%s'",
pcmk__s(pcmk__xe_id(location), "(missing ID)"));
return pcmk_rc_unpack_error;
}
role = crm_element_value(set, PCMK_XA_ROLE);
local_score = crm_element_value(set, PCMK_XA_SCORE);
for (xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL);
xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) {
resource = pcmk__find_constraint_resource(scheduler->resources,
pcmk__xe_id(xml_rsc));
if (resource == NULL) {
pcmk__config_err("%s: No resource found for %s",
set_id, pcmk__xe_id(xml_rsc));
return pcmk_rc_unpack_error;
}
unpack_rsc_location(location, resource, role, local_score, NULL);
}
return pcmk_rc_ok;
}
void
pcmk__unpack_location(xmlNode *xml_obj, pcmk_scheduler_t *scheduler)
{
xmlNode *set = NULL;
bool any_sets = false;
xmlNode *orig_xml = NULL;
xmlNode *expanded_xml = NULL;
if (unpack_location_tags(xml_obj, &expanded_xml, scheduler) != pcmk_rc_ok) {
return;
}
if (expanded_xml) {
orig_xml = xml_obj;
xml_obj = expanded_xml;
}
for (set = pcmk__xe_first_child(xml_obj, PCMK_XE_RESOURCE_SET, NULL, NULL);
set != NULL; set = pcmk__xe_next_same(set)) {
any_sets = true;
set = expand_idref(set, scheduler->input);
if ((set == NULL) // Configuration error, message already logged
|| (unpack_location_set(xml_obj, set, scheduler) != pcmk_rc_ok)) {
if (expanded_xml) {
free_xml(expanded_xml);
}
return;
}
}
if (expanded_xml) {
free_xml(expanded_xml);
xml_obj = orig_xml;
}
if (!any_sets) {
unpack_simple_location(xml_obj, scheduler);
}
}
/*!
* \internal
* \brief Add a new location constraint to scheduler data
*
* \param[in] id XML ID of location constraint
* \param[in,out] rsc Resource in location constraint
* \param[in] node_score Constraint score
* \param[in] discover_mode Resource discovery option for constraint
* \param[in] node Node in constraint (or NULL if rule-based)
*
* \return Newly allocated location constraint
* \note The result will be added to the cluster (via \p rsc) and should not be
* freed separately.
*/
pcmk__location_t *
pcmk__new_location(const char *id, pcmk_resource_t *rsc,
int node_score, const char *discover_mode, pcmk_node_t *node)
{
pcmk__location_t *new_con = NULL;
if (id == NULL) {
pcmk__config_err("Invalid constraint: no ID specified");
return NULL;
} else if (rsc == NULL) {
pcmk__config_err("Invalid constraint %s: no resource specified", id);
return NULL;
} else if (node == NULL) {
CRM_CHECK(node_score == 0, return NULL);
}
new_con = calloc(1, sizeof(pcmk__location_t));
if (new_con != NULL) {
new_con->id = strdup(id);
new_con->rsc = rsc;
new_con->nodes = NULL;
new_con->role_filter = pcmk_role_unknown;
if (pcmk__str_eq(discover_mode, PCMK_VALUE_ALWAYS,
pcmk__str_null_matches|pcmk__str_casei)) {
new_con->discover_mode = pcmk_probe_always;
} else if (pcmk__str_eq(discover_mode, PCMK_VALUE_NEVER,
pcmk__str_casei)) {
new_con->discover_mode = pcmk_probe_never;
} else if (pcmk__str_eq(discover_mode, PCMK_VALUE_EXCLUSIVE,
pcmk__str_casei)) {
new_con->discover_mode = pcmk_probe_exclusive;
rsc->exclusive_discover = TRUE;
} else {
pcmk__config_err("Invalid " PCMK_XA_RESOURCE_DISCOVERY " value %s "
"in location constraint", discover_mode);
}
if (node != NULL) {
pcmk_node_t *copy = pe__copy_node(node);
copy->weight = node_score;
new_con->nodes = g_list_prepend(NULL, copy);
}
rsc->cluster->placement_constraints = g_list_prepend(
rsc->cluster->placement_constraints, new_con);
rsc->rsc_location = g_list_prepend(rsc->rsc_location, new_con);
}
return new_con;
}
/*!
* \internal
* \brief Apply all location constraints
*
* \param[in,out] scheduler Scheduler data
*/
void
pcmk__apply_locations(pcmk_scheduler_t *scheduler)
{
for (GList *iter = scheduler->placement_constraints;
iter != NULL; iter = iter->next) {
pcmk__location_t *location = iter->data;
location->rsc->cmds->apply_location(location->rsc, location);
}
}
/*!
* \internal
* \brief Apply a location constraint to a resource's allowed node scores
*
* \param[in,out] rsc Resource to apply constraint to
* \param[in,out] location Location constraint to apply
*
* \note This does not consider the resource's children, so the resource's
* apply_location() method should be used instead in most cases.
*/
void
pcmk__apply_location(pcmk_resource_t *rsc, pcmk__location_t *location)
{
bool need_role = false;
CRM_ASSERT((rsc != NULL) && (location != NULL));
// If a role was specified, ensure constraint is applicable
need_role = (location->role_filter > pcmk_role_unknown);
if (need_role && (location->role_filter != rsc->next_role)) {
pcmk__rsc_trace(rsc,
"Not applying %s to %s because role will be %s not %s",
location->id, rsc->id, pcmk_role_text(rsc->next_role),
pcmk_role_text(location->role_filter));
return;
}
if (location->nodes == NULL) {
pcmk__rsc_trace(rsc, "Not applying %s to %s because no nodes match",
location->id, rsc->id);
return;
}
pcmk__rsc_trace(rsc, "Applying %s%s%s to %s", location->id,
(need_role? " for role " : ""),
(need_role? pcmk_role_text(location->role_filter) : ""),
rsc->id);
for (GList *iter = location->nodes; iter != NULL; iter = iter->next) {
pcmk_node_t *node = iter->data;
pcmk_node_t *allowed_node = g_hash_table_lookup(rsc->allowed_nodes,
node->details->id);
if (allowed_node == NULL) {
pcmk__rsc_trace(rsc, "* = %d on %s",
node->weight, pcmk__node_name(node));
allowed_node = pe__copy_node(node);
g_hash_table_insert(rsc->allowed_nodes,
(gpointer) allowed_node->details->id,
allowed_node);
} else {
pcmk__rsc_trace(rsc, "* + %d on %s",
node->weight, pcmk__node_name(node));
allowed_node->weight = pcmk__add_scores(allowed_node->weight,
node->weight);
}
if (allowed_node->rsc_discover_mode < location->discover_mode) {
if (location->discover_mode == pcmk_probe_exclusive) {
rsc->exclusive_discover = TRUE;
}
/* exclusive > never > always... always is default */
allowed_node->rsc_discover_mode = location->discover_mode;
}
}
}
diff --git a/lib/pengine/rules.c b/lib/pengine/rules.c
index 31c5a0912d..64f9bdba0c 100644
--- a/lib/pengine/rules.c
+++ b/lib/pengine/rules.c
@@ -1,671 +1,524 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <glib.h>
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <crm/pengine/rules.h>
#include <crm/common/iso8601_internal.h>
#include <crm/common/nvpair_internal.h>
#include <crm/common/rules_internal.h>
#include <crm/common/xml_internal.h>
#include <crm/pengine/internal.h>
#include <crm/pengine/rules_internal.h>
#include <sys/types.h>
#include <regex.h>
CRM_TRACE_INIT_DATA(pe_rules);
/*!
* \internal
* \brief Map pe_rule_eval_data_t to pcmk_rule_input_t
*
* \param[out] new New data struct
* \param[in] old Old data struct
*/
static void
map_rule_input(pcmk_rule_input_t *new, const pe_rule_eval_data_t *old)
{
if (old == NULL) {
return;
}
new->now = old->now;
new->node_attrs = old->node_hash;
if (old->rsc_data != NULL) {
new->rsc_standard = old->rsc_data->standard;
new->rsc_provider = old->rsc_data->provider;
new->rsc_agent = old->rsc_data->agent;
}
if (old->match_data != NULL) {
new->rsc_params = old->match_data->params;
new->rsc_meta = old->match_data->meta;
if (old->match_data->re != NULL) {
new->rsc_id = old->match_data->re->string;
new->rsc_id_submatches = old->match_data->re->pmatch;
new->rsc_id_nmatches = old->match_data->re->nregs;
}
}
if (old->op_data != NULL) {
new->op_name = old->op_data->op_name;
new->op_interval_ms = old->op_data->interval;
}
}
/*!
* \brief Evaluate any rules contained by given XML element
*
* \param[in,out] xml XML element to check for rules
* \param[in] node_hash Node attributes to use to evaluate expressions
* \param[in] now Time to use when evaluating expressions
* \param[out] next_change If not NULL, set to when evaluation will change
*
* \return TRUE if no rules, or any of rules present is in effect, else FALSE
*/
gboolean
pe_evaluate_rules(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now,
crm_time_t *next_change)
{
pe_rule_eval_data_t rule_data = {
.node_hash = node_hash,
.now = now,
.match_data = NULL,
.rsc_data = NULL,
.op_data = NULL
};
return pe_eval_rules(ruleset, &rule_data, next_change);
}
-gboolean
-pe_test_rule(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role,
- crm_time_t *now, crm_time_t *next_change,
- pe_match_data_t *match_data)
-{
- pe_rule_eval_data_t rule_data = {
- .node_hash = node_hash,
- .now = now,
- .match_data = match_data,
- .rsc_data = NULL,
- .op_data = NULL
- };
-
- return pe_eval_expr(rule, &rule_data, next_change);
-}
-
-/*!
- * \brief Evaluate one rule subelement (pass/fail)
- *
- * A rule element may contain another rule, a node attribute expression, or a
- * date expression. Given any one of those, evaluate it and return whether it
- * passed.
- *
- * \param[in,out] expr Rule subelement XML
- * \param[in] node_hash Node attributes to use when evaluating expression
- * \param[in] role Ignored (deprecated)
- * \param[in] now Time to use when evaluating expression
- * \param[out] next_change If not NULL, set to when evaluation will change
- * \param[in] match_data If not NULL, resource back-references and params
- *
- * \return TRUE if expression is in effect under given conditions, else FALSE
- */
-gboolean
-pe_test_expression(xmlNode *expr, GHashTable *node_hash, enum rsc_role_e role,
- crm_time_t *now, crm_time_t *next_change,
- pe_match_data_t *match_data)
-{
- pe_rule_eval_data_t rule_data = {
- .node_hash = node_hash,
- .now = now,
- .match_data = match_data,
- .rsc_data = NULL,
- .op_data = NULL
- };
-
- return pe_eval_subexpr(expr, &rule_data, next_change);
-}
-
// Information about a block of nvpair elements
typedef struct sorted_set_s {
int score; // This block's score for sorting
const char *name; // This block's ID
const char *special_name; // ID that should sort first
xmlNode *attr_set; // This block
gboolean overwrite; // Whether existing values will be overwritten
} sorted_set_t;
static gint
sort_pairs(gconstpointer a, gconstpointer b)
{
const sorted_set_t *pair_a = a;
const sorted_set_t *pair_b = b;
if (a == NULL && b == NULL) {
return 0;
} else if (a == NULL) {
return 1;
} else if (b == NULL) {
return -1;
}
if (pcmk__str_eq(pair_a->name, pair_a->special_name, pcmk__str_casei)) {
return -1;
} else if (pcmk__str_eq(pair_b->name, pair_a->special_name, pcmk__str_casei)) {
return 1;
}
/* If we're overwriting values, we want lowest score first, so the highest
* score is processed last; if we're not overwriting values, we want highest
* score first, so nothing else overwrites it.
*/
if (pair_a->score < pair_b->score) {
return pair_a->overwrite? -1 : 1;
} else if (pair_a->score > pair_b->score) {
return pair_a->overwrite? 1 : -1;
}
return 0;
}
static void
populate_hash(xmlNode * nvpair_list, GHashTable * hash, gboolean overwrite, xmlNode * top)
{
const char *name = NULL;
const char *value = NULL;
const char *old_value = NULL;
xmlNode *list = nvpair_list;
xmlNode *an_attr = NULL;
if (pcmk__xe_is(list->children, PCMK__XE_ATTRIBUTES)) {
list = list->children;
}
for (an_attr = pcmk__xe_first_child(list, NULL, NULL, NULL);
an_attr != NULL; an_attr = pcmk__xe_next(an_attr)) {
if (pcmk__xe_is(an_attr, PCMK_XE_NVPAIR)) {
xmlNode *ref_nvpair = expand_idref(an_attr, top);
name = crm_element_value(an_attr, PCMK_XA_NAME);
if ((name == NULL) && (ref_nvpair != NULL)) {
name = crm_element_value(ref_nvpair, PCMK_XA_NAME);
}
value = crm_element_value(an_attr, PCMK_XA_VALUE);
if ((value == NULL) && (ref_nvpair != NULL)) {
value = crm_element_value(ref_nvpair, PCMK_XA_VALUE);
}
if (name == NULL || value == NULL) {
continue;
}
old_value = g_hash_table_lookup(hash, name);
if (pcmk__str_eq(value, "#default", pcmk__str_casei)) {
if (old_value) {
crm_trace("Letting %s default (removing explicit value \"%s\")",
name, value);
g_hash_table_remove(hash, name);
}
continue;
} else if (old_value == NULL) {
crm_trace("Setting %s=\"%s\"", name, value);
pcmk__insert_dup(hash, name, value);
} else if (overwrite) {
crm_trace("Setting %s=\"%s\" (overwriting old value \"%s\")",
name, value, old_value);
pcmk__insert_dup(hash, name, value);
}
}
}
}
typedef struct unpack_data_s {
gboolean overwrite;
void *hash;
crm_time_t *next_change;
const pe_rule_eval_data_t *rule_data;
xmlNode *top;
} unpack_data_t;
static void
unpack_attr_set(gpointer data, gpointer user_data)
{
sorted_set_t *pair = data;
unpack_data_t *unpack_data = user_data;
if (!pe_eval_rules(pair->attr_set, unpack_data->rule_data,
unpack_data->next_change)) {
return;
}
crm_trace("Adding attributes from %s (score %d) %s overwrite",
pair->name, pair->score,
(unpack_data->overwrite? "with" : "without"));
populate_hash(pair->attr_set, unpack_data->hash, unpack_data->overwrite, unpack_data->top);
}
/*!
* \internal
* \brief Create a sorted list of nvpair blocks
*
* \param[in,out] top XML document root (used to expand id-ref's)
* \param[in] xml_obj XML element containing blocks of nvpair elements
* \param[in] set_name If not NULL, only get blocks of this element
* \param[in] always_first If not NULL, sort block with this ID as first
*
* \return List of sorted_set_t entries for nvpair blocks
*/
static GList *
make_pairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name,
const char *always_first, gboolean overwrite)
{
GList *unsorted = NULL;
if (xml_obj == NULL) {
return NULL;
}
for (xmlNode *attr_set = pcmk__xe_first_child(xml_obj, NULL, NULL, NULL);
attr_set != NULL; attr_set = pcmk__xe_next(attr_set)) {
if ((set_name == NULL) || pcmk__xe_is(attr_set, set_name)) {
const char *score = NULL;
sorted_set_t *pair = NULL;
xmlNode *expanded_attr_set = expand_idref(attr_set, top);
if (expanded_attr_set == NULL) {
continue; // Not possible with schema validation enabled
}
pair = pcmk__assert_alloc(1, sizeof(sorted_set_t));
pair->name = pcmk__xe_id(expanded_attr_set);
pair->special_name = always_first;
pair->attr_set = expanded_attr_set;
pair->overwrite = overwrite;
score = crm_element_value(expanded_attr_set, PCMK_XA_SCORE);
pair->score = char2score(score);
unsorted = g_list_prepend(unsorted, pair);
}
}
return g_list_sort(unsorted, sort_pairs);
}
/*!
* \brief Extract nvpair blocks contained by an XML element into a hash table
*
* \param[in,out] top XML document root (used to expand id-ref's)
* \param[in] xml_obj XML element containing blocks of nvpair elements
* \param[in] set_name If not NULL, only use blocks of this element
* \param[in] rule_data Matching parameters to use when unpacking
* \param[out] hash Where to store extracted name/value pairs
* \param[in] always_first If not NULL, process block with this ID first
* \param[in] overwrite Whether to replace existing values with same name
* \param[out] next_change If not NULL, set to when evaluation will change
*/
void
pe_eval_nvpairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name,
const pe_rule_eval_data_t *rule_data, GHashTable *hash,
const char *always_first, gboolean overwrite,
crm_time_t *next_change)
{
GList *pairs = make_pairs(top, xml_obj, set_name, always_first, overwrite);
if (pairs) {
unpack_data_t data = {
.hash = hash,
.overwrite = overwrite,
.next_change = next_change,
.top = top,
.rule_data = rule_data
};
g_list_foreach(pairs, unpack_attr_set, &data);
g_list_free_full(pairs, free);
}
}
/*!
* \brief Extract nvpair blocks contained by an XML element into a hash table
*
* \param[in,out] top XML document root (used to expand id-ref's)
* \param[in] xml_obj XML element containing blocks of nvpair elements
* \param[in] set_name Element name to identify nvpair blocks
* \param[in] node_hash Node attributes to use when evaluating rules
* \param[out] hash Where to store extracted name/value pairs
* \param[in] always_first If not NULL, process block with this ID first
* \param[in] overwrite Whether to replace existing values with same name
* \param[in] now Time to use when evaluating rules
* \param[out] next_change If not NULL, set to when evaluation will change
*/
void
pe_unpack_nvpairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name,
GHashTable *node_hash, GHashTable *hash,
const char *always_first, gboolean overwrite,
crm_time_t *now, crm_time_t *next_change)
{
pe_rule_eval_data_t rule_data = {
.node_hash = node_hash,
.now = now,
.match_data = NULL,
.rsc_data = NULL,
.op_data = NULL
};
pe_eval_nvpairs(top, xml_obj, set_name, &rule_data, hash,
always_first, overwrite, next_change);
}
/*!
* \brief Evaluate rules
*
* \param[in,out] ruleset XML possibly containing rule sub-elements
* \param[in] rule_data
* \param[out] next_change If not NULL, set to when evaluation will change
*
* \return TRUE if there are no rules or
*/
gboolean
pe_eval_rules(xmlNode *ruleset, const pe_rule_eval_data_t *rule_data,
crm_time_t *next_change)
{
// If there are no rules, pass by default
gboolean ruleset_default = TRUE;
for (xmlNode *rule = pcmk__xe_first_child(ruleset, PCMK_XE_RULE, NULL,
NULL);
rule != NULL; rule = pcmk__xe_next_same(rule)) {
+ pcmk_rule_input_t rule_input = { NULL, };
+
+ map_rule_input(&rule_input, rule_data);
ruleset_default = FALSE;
- if (pe_eval_expr(rule, rule_data, next_change)) {
+ if (pcmk_evaluate_rule(rule, &rule_input, next_change) == pcmk_rc_ok) {
/* Only the deprecated PCMK__XE_LIFETIME element of location
* constraints may contain more than one rule at the top level --
* the schema limits a block of nvpairs to a single top-level rule.
* So, this effectively means that a lifetime is active if any rule
* it contains is active.
*/
return TRUE;
}
}
return ruleset_default;
}
-/*!
- * \brief Evaluate all of a rule's expressions
- *
- * \param[in,out] rule XML containing a rule definition or its id-ref
- * \param[in] rule_data Matching parameters to check against rule
- * \param[out] next_change If not NULL, set to when evaluation will change
- *
- * \return TRUE if \p rule_data passes \p rule, otherwise FALSE
- */
-gboolean
-pe_eval_expr(xmlNode *rule, const pe_rule_eval_data_t *rule_data,
- crm_time_t *next_change)
-{
- xmlNode *expr = NULL;
- gboolean test = TRUE;
- gboolean empty = TRUE;
- gboolean passed = TRUE;
- gboolean do_and = TRUE;
- const char *value = NULL;
-
- rule = expand_idref(rule, NULL);
- if (rule == NULL) {
- return FALSE; // Not possible with schema validation enabled
- }
-
- value = crm_element_value(rule, PCMK_XA_BOOLEAN_OP);
- if (pcmk__str_eq(value, PCMK_VALUE_OR, pcmk__str_casei)) {
- do_and = FALSE;
- passed = FALSE;
-
- } else if (!pcmk__str_eq(value, PCMK_VALUE_AND,
- pcmk__str_null_matches|pcmk__str_casei)) {
- pcmk__config_warn("Rule %s has invalid " PCMK_XA_BOOLEAN_OP
- " value '%s', using default ('" PCMK_VALUE_AND "')",
- pcmk__xe_id(rule), value);
- }
-
- crm_trace("Testing rule %s", pcmk__xe_id(rule));
- for (expr = pcmk__xe_first_child(rule, NULL, NULL, NULL); expr != NULL;
- expr = pcmk__xe_next(expr)) {
-
- test = pe_eval_subexpr(expr, rule_data, next_change);
- empty = FALSE;
-
- if (test && do_and == FALSE) {
- crm_trace("Expression %s/%s passed",
- pcmk__xe_id(rule), pcmk__xe_id(expr));
- return TRUE;
-
- } else if (test == FALSE && do_and) {
- crm_trace("Expression %s/%s failed",
- pcmk__xe_id(rule), pcmk__xe_id(expr));
- return FALSE;
- }
- }
-
- if (empty) {
- pcmk__config_err("Ignoring rule %s because it contains no expressions",
- pcmk__xe_id(rule));
- }
+// Deprecated functions kept only for backward API compatibility
+// LCOV_EXCL_START
- crm_trace("Rule %s %s", pcmk__xe_id(rule), passed ? "passed" : "failed");
- return passed;
-}
+#include <crm/pengine/rules_compat.h>
-/*!
- * \brief Evaluate a single rule expression, including any subexpressions
- *
- * \param[in,out] expr XML containing a rule expression
- * \param[in] rule_data Matching parameters to check against expression
- * \param[out] next_change If not NULL, set to when evaluation will change
- *
- * \return TRUE if \p rule_data passes \p expr, otherwise FALSE
- */
gboolean
-pe_eval_subexpr(xmlNode *expr, const pe_rule_eval_data_t *rule_data,
- crm_time_t *next_change)
+pe_test_rule(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role,
+ crm_time_t *now, crm_time_t *next_change,
+ pe_match_data_t *match_data)
{
- gboolean accept = FALSE;
- const char *uname = NULL;
-
- switch (pcmk__expression_type(expr)) {
- case pcmk__subexpr_rule:
- accept = pe_eval_expr(expr, rule_data, next_change);
- break;
- case pcmk__subexpr_attribute:
- case pcmk__subexpr_location:
- /* these expressions can never succeed if there is
- * no node to compare with
- */
- if (rule_data->node_hash != NULL) {
- accept = pe__eval_attr_expr(expr, rule_data);
- }
- break;
-
- case pcmk__subexpr_datetime:
- switch (pcmk__evaluate_date_expression(expr, rule_data->now,
- next_change)) {
- case pcmk_rc_within_range:
- case pcmk_rc_ok:
- accept = TRUE;
- break;
-
- default:
- accept = FALSE;
- break;
- }
- break;
-
- case pcmk__subexpr_resource:
- accept = pe__eval_rsc_expr(expr, rule_data);
- break;
-
- case pcmk__subexpr_operation:
- accept = pe__eval_op_expr(expr, rule_data);
- break;
+ pcmk_rule_input_t rule_input = {
+ .node_attrs = node_hash,
+ .now = now,
+ };
- default:
- CRM_CHECK(FALSE /* bad type */ , return FALSE);
- accept = FALSE;
- }
- if (rule_data->node_hash) {
- uname = g_hash_table_lookup(rule_data->node_hash, CRM_ATTR_UNAME);
+ if (match_data != NULL) {
+ rule_input.rsc_params = match_data->params;
+ rule_input.rsc_meta = match_data->meta;
+ if (match_data->re != NULL) {
+ rule_input.rsc_id = match_data->re->string;
+ rule_input.rsc_id_submatches = match_data->re->pmatch;
+ rule_input.rsc_id_nmatches = match_data->re->nregs;
+ }
}
-
- crm_trace("Expression %s %s on %s",
- pcmk__xe_id(expr), (accept? "passed" : "failed"),
- pcmk__s(uname, "all nodes"));
- return accept;
-}
-
-/*!
- * \internal
- * \brief Evaluate a node attribute expression based on #uname, #id, #kind,
- * or a generic node attribute
- *
- * \param[in] expr XML of rule expression
- * \param[in] rule_data The match_data and node_hash members are used
- *
- * \return TRUE if rule_data satisfies the expression, FALSE otherwise
- */
-gboolean
-pe__eval_attr_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data)
-{
- pcmk_rule_input_t rule_input = { NULL, };
-
- map_rule_input(&rule_input, rule_data);
- return pcmk__evaluate_attr_expression(expr, &rule_input) == pcmk_rc_ok;
-}
-
-gboolean
-pe__eval_op_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data)
-{
- pcmk_rule_input_t rule_input = { NULL, };
-
- map_rule_input(&rule_input, rule_data);
- return pcmk__evaluate_op_expression(expr, &rule_input) == pcmk_rc_ok;
-}
-
-gboolean
-pe__eval_rsc_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data)
-{
- pcmk_rule_input_t rule_input = { NULL, };
-
- map_rule_input(&rule_input, rule_data);
- return pcmk__evaluate_rsc_expression(expr, &rule_input) == pcmk_rc_ok;
+ return pcmk_evaluate_rule(rule, &rule_input, next_change) == pcmk_rc_ok;
}
-// Deprecated functions kept only for backward API compatibility
-// LCOV_EXCL_START
-
-#include <crm/pengine/rules_compat.h>
-
gboolean
test_ruleset(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now)
{
return pe_evaluate_rules(ruleset, node_hash, now, NULL);
}
gboolean
test_rule(xmlNode * rule, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now)
{
return pe_test_rule(rule, node_hash, role, now, NULL, NULL);
}
gboolean
pe_test_rule_re(xmlNode * rule, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now, pe_re_match_data_t * re_match_data)
{
pe_match_data_t match_data = {
.re = re_match_data,
.params = NULL,
.meta = NULL,
};
return pe_test_rule(rule, node_hash, role, now, NULL, &match_data);
}
gboolean
pe_test_rule_full(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role,
crm_time_t *now, pe_match_data_t *match_data)
{
return pe_test_rule(rule, node_hash, role, now, NULL, match_data);
}
+gboolean
+pe_test_expression(xmlNode *expr, GHashTable *node_hash, enum rsc_role_e role,
+ crm_time_t *now, crm_time_t *next_change,
+ pe_match_data_t *match_data)
+{
+ pcmk_rule_input_t rule_input = {
+ .now = now,
+ .node_attrs = node_hash,
+ };
+
+ if (match_data != NULL) {
+ rule_input.rsc_params = match_data->params;
+ rule_input.rsc_meta = match_data->meta;
+ if (match_data->re != NULL) {
+ rule_input.rsc_id = match_data->re->string;
+ rule_input.rsc_id_submatches = match_data->re->pmatch;
+ rule_input.rsc_id_nmatches = match_data->re->nregs;
+ }
+ }
+ return pcmk__evaluate_condition(expr, &rule_input,
+ next_change) == pcmk_rc_ok;
+}
+
gboolean
test_expression(xmlNode * expr, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now)
{
return pe_test_expression(expr, node_hash, role, now, NULL, NULL);
}
gboolean
pe_test_expression_re(xmlNode * expr, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now, pe_re_match_data_t * re_match_data)
{
pe_match_data_t match_data = {
.re = re_match_data,
.params = NULL,
.meta = NULL,
};
return pe_test_expression(expr, node_hash, role, now, NULL, &match_data);
}
gboolean
pe_test_expression_full(xmlNode *expr, GHashTable *node_hash,
enum rsc_role_e role, crm_time_t *now,
pe_match_data_t *match_data)
{
return pe_test_expression(expr, node_hash, role, now, NULL, match_data);
}
+gboolean
+pe_eval_expr(xmlNode *rule, const pe_rule_eval_data_t *rule_data,
+ crm_time_t *next_change)
+{
+ pcmk_rule_input_t rule_input = { NULL, };
+
+ map_rule_input(&rule_input, rule_data);
+ return pcmk_evaluate_rule(rule, &rule_input, next_change) == pcmk_rc_ok;
+}
+
+gboolean
+pe_eval_subexpr(xmlNode *expr, const pe_rule_eval_data_t *rule_data,
+ crm_time_t *next_change)
+{
+ pcmk_rule_input_t rule_input = { NULL, };
+
+ map_rule_input(&rule_input, rule_data);
+ return pcmk__evaluate_condition(expr, &rule_input,
+ next_change) == pcmk_rc_ok;
+}
+
void
unpack_instance_attributes(xmlNode *top, xmlNode *xml_obj, const char *set_name,
GHashTable *node_hash, GHashTable *hash,
const char *always_first, gboolean overwrite,
crm_time_t *now)
{
pe_rule_eval_data_t rule_data = {
.node_hash = node_hash,
.now = now,
.match_data = NULL,
.rsc_data = NULL,
.op_data = NULL
};
pe_eval_nvpairs(top, xml_obj, set_name, &rule_data, hash, always_first,
overwrite, NULL);
}
enum expression_type
find_expression_type(xmlNode *expr)
{
- return pcmk__expression_type(expr);
+ return pcmk__condition_type(expr);
}
char *
pe_expand_re_matches(const char *string, const pe_re_match_data_t *match_data)
{
if (match_data == NULL) {
return NULL;
}
return pcmk__replace_submatches(string, match_data->string,
match_data->pmatch, match_data->nregs);
}
// LCOV_EXCL_STOP
// End deprecated API

File Metadata

Mime Type
text/x-diff
Expires
Mon, Apr 21, 2:16 PM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1664904
Default Alt Text
(166 KB)

Event Timeline