Page Menu
Home
ClusterLabs Projects
Search
Configure Global Search
Log In
Files
F3686631
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
166 KB
Referenced Files
None
Subscribers
None
View Options
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 "&"
#define PCMK__XML_ENTITY_GT ">"
#define PCMK__XML_ENTITY_LT "<"
#define PCMK__XML_ENTITY_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
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Apr 21, 2:16 PM (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1664904
Default Alt Text
(166 KB)
Attached To
Mode
rP Pacemaker
Attached
Detach File
Event Timeline
Log In to Comment