diff --git a/include/crm/common/util.h b/include/crm/common/util.h
index 55e13e4165..8a5e9598eb 100644
--- a/include/crm/common/util.h
+++ b/include/crm/common/util.h
@@ -1,225 +1,225 @@
 /*
  * Copyright 2004-2020 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 CRM_COMMON_UTIL__H
 #  define CRM_COMMON_UTIL__H
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Utility functions
  * \ingroup core
  */
 
 #  include <sys/types.h>    // gid_t, mode_t, size_t, time_t, uid_t
 #  include <stdlib.h>
 #  include <stdbool.h>
 #  include <stdint.h>       // uint32_t
 #  include <limits.h>
 #  include <signal.h>
 #  include <glib.h>
 
 #  include <libxml/tree.h>
 
 #  include <crm/lrmd.h>
 #  include <crm/common/results.h>
 
 #  define ONLINESTATUS  "online"  // Status of an online client
 #  define OFFLINESTATUS "offline" // Status of an offline client
 
 // public name/value pair functions (from nvpair.c)
 int pcmk_scan_nvpair(const char *input, char **name, char **value);
 char *pcmk_format_nvpair(const char *name, const char *value, const char *units);
 char *pcmk_format_named_time(const char *name, time_t epoch_time);
 
 /* public Pacemaker Remote functions (from remote.c) */
 int crm_default_remote_port(void);
 
 /* public string functions (from strings.c) */
 char *crm_itoa_stack(int an_int, char *buf, size_t len);
 gboolean crm_is_true(const char *s);
 int crm_str_to_boolean(const char *s, int *ret);
 long long crm_parse_ll(const char *text, const char *default_text);
 int crm_parse_int(const char *text, const char *default_text);
 long long crm_get_msec(const char *input);
 char * crm_strip_trailing_newline(char *str);
 gboolean crm_str_eq(const char *a, const char *b, gboolean use_case);
 gboolean safe_str_neq(const char *a, const char *b);
 gboolean crm_strcase_equal(gconstpointer a, gconstpointer b);
 guint crm_strcase_hash(gconstpointer v);
 guint g_str_hash_traditional(gconstpointer v);
 char *crm_strdup_printf(char const *format, ...) __attribute__ ((__format__ (__printf__, 1, 2)));
-bool pcmk__split_range(const char *srcstring, char separator, char **name, char **value);
+int pcmk__split_range(const char *srcstring, char separator, char **start, char **end);
 
 #  define safe_str_eq(a, b) crm_str_eq(a, b, FALSE)
 #  define crm_str_hash g_str_hash_traditional
 
 static inline char *
 crm_itoa(int an_int)
 {
     return crm_strdup_printf("%d", an_int);
 }
 
 static inline char *
 crm_ftoa(double a_float)
 {
     return crm_strdup_printf("%f", a_float);
 }
 
 static inline char *
 crm_ttoa(time_t epoch_time)
 {
     return crm_strdup_printf("%lld", (long long) epoch_time);
 }
 
 /*!
  * \brief Create hash table with dynamically allocated string keys/values
  *
  * \return Newly allocated hash table
  * \note It is the caller's responsibility to free the result, using
  *       g_hash_table_destroy().
  */
 static inline GHashTable *
 crm_str_table_new()
 {
     return g_hash_table_new_full(crm_str_hash, g_str_equal, free, free);
 }
 
 /*!
  * \brief Create hash table with case-insensitive dynamically allocated string keys/values
  *
  * \return Newly allocated hash table
  * \note It is the caller's responsibility to free the result, using
  *       g_hash_table_destroy().
  */
 static inline GHashTable *
 crm_strcase_table_new()
 {
     return g_hash_table_new_full(crm_strcase_hash, crm_strcase_equal, free, free);
 }
 
 GHashTable *crm_str_table_dup(GHashTable *old_table);
 
 #  define crm_atoi(text, default_text) crm_parse_int(text, default_text)
 
 /* public I/O functions (from io.c) */
 void crm_build_path(const char *path_c, mode_t mode);
 
 guint crm_parse_interval_spec(const char *input);
 int char2score(const char *score);
 char *score2char(int score);
 char *score2char_stack(int score, char *buf, size_t len);
 
 /* public operation functions (from operations.c) */
 gboolean parse_op_key(const char *key, char **rsc_id, char **op_type,
                       guint *interval_ms);
 gboolean decode_transition_key(const char *key, char **uuid, int *action,
                                int *transition_id, int *target_rc);
 gboolean decode_transition_magic(const char *magic, char **uuid,
                                  int *transition_id, int *action_id,
                                  int *op_status, int *op_rc, int *target_rc);
 int rsc_op_expected_rc(lrmd_event_data_t *event);
 gboolean did_rsc_op_fail(lrmd_event_data_t *event, int target_rc);
 bool crm_op_needs_metadata(const char *rsc_class, const char *op);
 xmlNode *crm_create_op_xml(xmlNode *parent, const char *prefix,
                            const char *task, const char *interval_spec,
                            const char *timeout);
 #define CRM_DEFAULT_OP_TIMEOUT_S "20s"
 
 // Public resource agent functions (from agents.c)
 
 // Capabilities supported by a resource agent standard
 enum pcmk_ra_caps {
     pcmk_ra_cap_none         = 0x000,
     pcmk_ra_cap_provider     = 0x001, // Requires provider
     pcmk_ra_cap_status       = 0x002, // Supports status instead of monitor
     pcmk_ra_cap_params       = 0x004, // Supports parameters
     pcmk_ra_cap_unique       = 0x008, // Supports unique clones
     pcmk_ra_cap_promotable   = 0x010, // Supports promotable clones
 };
 
 uint32_t pcmk_get_ra_caps(const char *standard);
 char *crm_generate_ra_key(const char *standard, const char *provider,
                           const char *type);
 int crm_parse_agent_spec(const char *spec, char **standard, char **provider,
                          char **type);
 
 
 int compare_version(const char *version1, const char *version2);
 
 /* coverity[+kill] */
 void crm_abort(const char *file, const char *function, int line,
                const char *condition, gboolean do_core, gboolean do_fork);
 
 static inline gboolean
 is_not_set(long long word, long long bit)
 {
     return ((word & bit) == 0);
 }
 
 static inline gboolean
 is_set(long long word, long long bit)
 {
     return ((word & bit) == bit);
 }
 
 static inline gboolean
 is_set_any(long long word, long long bit)
 {
     return ((word & bit) != 0);
 }
 
 static inline guint
 crm_hash_table_size(GHashTable * hashtable)
 {
     if (hashtable == NULL) {
         return 0;
     }
     return g_hash_table_size(hashtable);
 }
 
 char *crm_meta_name(const char *field);
 const char *crm_meta_value(GHashTable * hash, const char *field);
 
 char *crm_md5sum(const char *buffer);
 
 char *crm_generate_uuid(void);
 bool crm_is_daemon_name(const char *name);
 
 int crm_user_lookup(const char *name, uid_t * uid, gid_t * gid);
 int pcmk_daemon_user(uid_t *uid, gid_t *gid);
 
 #ifdef HAVE_GNUTLS_GNUTLS_H
 void crm_gnutls_global_init(void);
 #endif
 
 bool pcmk_acl_required(const char *user);
 
 char *pcmk_hostname(void);
 
 #ifndef PCMK__NO_COMPAT
 /* Everything here is deprecated and kept only for public API backward
  * compatibility. It will be moved to compatibility.h when 2.1.0 is released.
  */
 
 //! \deprecated Use crm_parse_interval_spec() instead
 #define crm_get_interval crm_parse_interval_spec
 #endif
 
 //! \deprecated Use pcmk_get_ra_caps() instead
 bool crm_provider_required(const char *standard);
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif
diff --git a/lib/common/strings.c b/lib/common/strings.c
index bf78cea12e..2beeed3163 100644
--- a/lib/common/strings.c
+++ b/lib/common/strings.c
@@ -1,640 +1,640 @@
 /*
  * Copyright 2004-2020 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>
 
 #ifndef _GNU_SOURCE
 #  define _GNU_SOURCE
 #endif
 
 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
 #include <limits.h>
 #include <bzlib.h>
 #include <sys/types.h>
 
 char *
 crm_itoa_stack(int an_int, char *buffer, size_t len)
 {
     if (buffer != NULL) {
         snprintf(buffer, len, "%d", an_int);
     }
 
     return buffer;
 }
 
 /*!
  * \internal
  * \brief Scan a long long integer from a string
  *
  * \param[in]  text      String to scan
  * \param[out] result    If not NULL, where to store scanned value
  * \param[out] end_text  If not NULL, where to store pointer to just after value
  *
  * \return Standard Pacemaker return code (also set errno on error)
  */
 static int
 scan_ll(const char *text, long long *result, char **end_text)
 {
     long long local_result = -1;
     char *local_end_text = NULL;
     int rc = pcmk_rc_ok;
 
     errno = 0;
     if (text != NULL) {
 #ifdef ANSI_ONLY
         local_result = (long long) strtol(text, &local_end_text, 10);
 #else
         local_result = strtoll(text, &local_end_text, 10);
 #endif
         if (errno == ERANGE) {
             rc = errno;
             crm_warn("Integer parsed from %s was clipped to %lld",
                      text, local_result);
 
         } else if (errno != 0) {
             rc = errno;
             local_result = -1;
             crm_err("Could not parse integer from %s (using -1 instead): %s",
                     text, pcmk_rc_str(rc));
 
         } else if (local_end_text == text) {
             rc = EINVAL;
             local_result = -1;
             crm_err("Could not parse integer from %s (using -1 instead): "
                     "No digits found", text);
         }
 
         if ((end_text == NULL) && (local_end_text != NULL)
             && (local_end_text[0] != '\0')) {
             crm_warn("Characters left over after parsing '%s': '%s'",
                      text, local_end_text);
         }
         errno = rc;
     }
     if (end_text != NULL) {
         *end_text = local_end_text;
     }
     if (result != NULL) {
         *result = local_result;
     }
     return rc;
 }
 
 /*!
  * \brief Parse a long long integer value from a string
  *
  * \param[in] text          The string to parse
  * \param[in] default_text  Default string to parse if text is NULL
  *
  * \return Parsed value on success, -1 (and set errno) on error
  */
 long long
 crm_parse_ll(const char *text, const char *default_text)
 {
     long long result;
 
     if (text == NULL) {
         text = default_text;
         if (text == NULL) {
             crm_err("No default conversion value supplied");
             errno = EINVAL;
             return -1;
         }
     }
     scan_ll(text, &result, NULL);
     return result;
 }
 
 /*!
  * \brief Parse an integer value from a string
  *
  * \param[in] text          The string to parse
  * \param[in] default_text  Default string to parse if text is NULL
  *
  * \return Parsed value on success, INT_MIN or INT_MAX (and set errno to ERANGE)
  *         if parsed value is out of integer range, otherwise -1 (and set errno)
  */
 int
 crm_parse_int(const char *text, const char *default_text)
 {
     long long result = crm_parse_ll(text, default_text);
 
     if (result < INT_MIN) {
         // If errno is ERANGE, crm_parse_ll() has already logged a message
         if (errno != ERANGE) {
             crm_err("Conversion of %s was clipped: %lld", text, result);
             errno = ERANGE;
         }
         return INT_MIN;
 
     } else if (result > INT_MAX) {
         // If errno is ERANGE, crm_parse_ll() has already logged a message
         if (errno != ERANGE) {
             crm_err("Conversion of %s was clipped: %lld", text, result);
             errno = ERANGE;
         }
         return INT_MAX;
     }
 
     return (int) result;
 }
 
 /*!
  * \internal
  * \brief Parse a guint from a string stored in a hash table
  *
  * \param[in]  table        Hash table to search
  * \param[in]  key          Hash table key to use to retrieve string
  * \param[in]  default_val  What to use if key has no entry in table
  * \param[out] result       If not NULL, where to store parsed integer
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__guint_from_hash(GHashTable *table, const char *key, guint default_val,
                       guint *result)
 {
     const char *value;
     long long value_ll;
 
     CRM_CHECK((table != NULL) && (key != NULL), return EINVAL);
 
     value = g_hash_table_lookup(table, key);
     if (value == NULL) {
         if (result != NULL) {
             *result = default_val;
         }
         return pcmk_rc_ok;
     }
 
     errno = 0;
     value_ll = crm_parse_ll(value, NULL);
     if (errno != 0) {
         return errno; // Message already logged
     }
     if ((value_ll < 0) || (value_ll > G_MAXUINT)) {
         crm_warn("Could not parse non-negative integer from %s", value);
         return ERANGE;
     }
 
     if (result != NULL) {
         *result = (guint) value_ll;
     }
     return pcmk_rc_ok;
 }
 
 #ifndef NUMCHARS
 #  define	NUMCHARS	"0123456789."
 #endif
 
 #ifndef WHITESPACE
 #  define	WHITESPACE	" \t\n\r\f"
 #endif
 
 /*!
  * \brief Parse a time+units string and return milliseconds equivalent
  *
  * \param[in] input  String with a number and units (optionally with whitespace
  *                   before and/or after the number)
  *
  * \return Milliseconds corresponding to string expression, or -1 on error
  */
 long long
 crm_get_msec(const char *input)
 {
     const char *num_start = NULL;
     const char *units;
     long long multiplier = 1000;
     long long divisor = 1;
     long long msec = -1;
     size_t num_len = 0;
     char *end_text = NULL;
 
     if (input == NULL) {
         return -1;
     }
 
     num_start = input + strspn(input, WHITESPACE);
     num_len = strspn(num_start, NUMCHARS);
     if (num_len < 1) {
         return -1;
     }
     units = num_start + num_len;
     units += strspn(units, WHITESPACE);
 
     if (!strncasecmp(units, "ms", 2) || !strncasecmp(units, "msec", 4)) {
         multiplier = 1;
         divisor = 1;
     } else if (!strncasecmp(units, "us", 2) || !strncasecmp(units, "usec", 4)) {
         multiplier = 1;
         divisor = 1000;
     } else if (!strncasecmp(units, "s", 1) || !strncasecmp(units, "sec", 3)) {
         multiplier = 1000;
         divisor = 1;
     } else if (!strncasecmp(units, "m", 1) || !strncasecmp(units, "min", 3)) {
         multiplier = 60 * 1000;
         divisor = 1;
     } else if (!strncasecmp(units, "h", 1) || !strncasecmp(units, "hr", 2)) {
         multiplier = 60 * 60 * 1000;
         divisor = 1;
     } else if ((*units != EOS) && (*units != '\n') && (*units != '\r')) {
         return -1;
     }
 
     scan_ll(num_start, &msec, &end_text);
     if (msec > (LLONG_MAX / multiplier)) {
         // Arithmetics overflow while multiplier/divisor mutually exclusive
         return LLONG_MAX;
     }
     msec *= multiplier;
     msec /= divisor;
     return msec;
 }
 
 gboolean
 safe_str_neq(const char *a, const char *b)
 {
     if (a == b) {
         return FALSE;
 
     } else if (a == NULL || b == NULL) {
         return TRUE;
 
     } else if (strcasecmp(a, b) == 0) {
         return FALSE;
     }
     return TRUE;
 }
 
 gboolean
 crm_is_true(const char *s)
 {
     gboolean ret = FALSE;
 
     if (s != NULL) {
         crm_str_to_boolean(s, &ret);
     }
     return ret;
 }
 
 int
 crm_str_to_boolean(const char *s, int *ret)
 {
     if (s == NULL) {
         return -1;
 
     } else if (strcasecmp(s, "true") == 0
                || strcasecmp(s, "on") == 0
                || strcasecmp(s, "yes") == 0 || strcasecmp(s, "y") == 0 || strcasecmp(s, "1") == 0) {
         *ret = TRUE;
         return 1;
 
     } else if (strcasecmp(s, "false") == 0
                || strcasecmp(s, "off") == 0
                || strcasecmp(s, "no") == 0 || strcasecmp(s, "n") == 0 || strcasecmp(s, "0") == 0) {
         *ret = FALSE;
         return 1;
     }
     return -1;
 }
 
 char *
 crm_strip_trailing_newline(char *str)
 {
     int len;
 
     if (str == NULL) {
         return str;
     }
 
     for (len = strlen(str) - 1; len >= 0 && str[len] == '\n'; len--) {
         str[len] = '\0';
     }
 
     return str;
 }
 
 gboolean
 crm_str_eq(const char *a, const char *b, gboolean use_case)
 {
     if (use_case) {
         return g_strcmp0(a, b) == 0;
 
         /* TODO - Figure out which calls, if any, really need to be case independent */
     } else if (a == b) {
         return TRUE;
 
     } else if (a == NULL || b == NULL) {
         /* shouldn't be comparing NULLs */
         return FALSE;
 
     } else if (strcasecmp(a, b) == 0) {
         return TRUE;
     }
     return FALSE;
 }
 
 /*!
  * \brief Check whether a string starts with a certain sequence
  *
  * \param[in] str    String to check
  * \param[in] match  Sequence to match against beginning of \p str
  *
  * \return \c true if \p str begins with match, \c false otherwise
  * \note This is equivalent to !strncmp(s, prefix, strlen(prefix))
  *       but is likely less efficient when prefix is a string literal
  *       if the compiler optimizes away the strlen() at compile time,
  *       and more efficient otherwise.
  */
 bool
 pcmk__starts_with(const char *str, const char *prefix)
 {
     const char *s = str;
     const char *p = prefix;
 
     if (!s || !p) {
         return false;
     }
     while (*s && *p) {
         if (*s++ != *p++) {
             return false;
         }
     }
     return (*p == 0);
 }
 
 static inline bool
 ends_with(const char *s, const char *match, bool as_extension)
 {
     if ((match == NULL) || (match[0] == '\0')) {
         return true;
     } else if (s == NULL) {
         return false;
     } else {
         size_t slen, mlen;
 
         /* Besides as_extension, we could also check
            !strchr(&match[1], match[0]) but that would be inefficient.
          */
         if (as_extension) {
             s = strrchr(s, match[0]);
             return (s == NULL)? false : !strcmp(s, match);
         }
 
         mlen = strlen(match);
         slen = strlen(s);
         return ((slen >= mlen) && !strcmp(s + slen - mlen, match));
     }
 }
 
 /*!
  * \internal
  * \brief Check whether a string ends with a certain sequence
  *
  * \param[in] s      String to check
  * \param[in] match  Sequence to match against end of \p s
  *
  * \return \c true if \p s ends case-sensitively with match, \c false otherwise
  * \note pcmk__ends_with_ext() can be used if the first character of match
  *       does not recur in match.
  */
 bool
 pcmk__ends_with(const char *s, const char *match)
 {
     return ends_with(s, match, false);
 }
 
 /*!
  * \internal
  * \brief Check whether a string ends with a certain "extension"
  *
  * \param[in] s      String to check
  * \param[in] match  Extension to match against end of \p s, that is,
  *                   its first character must not occur anywhere
  *                   in the rest of that very sequence (example: file
  *                   extension where the last dot is its delimiter,
  *                   e.g., ".html"); incorrect results may be
  *                   returned otherwise.
  *
  * \return \c true if \p s ends (verbatim, i.e., case sensitively)
  *         with "extension" designated as \p match (including empty
  *         string), \c false otherwise
  *
  * \note Main incentive to prefer this function over \c pcmk__ends_with()
  *       where possible is the efficiency (at the cost of added
  *       restriction on \p match as stated; the complexity class
  *       remains the same, though: BigO(M+N) vs. BigO(M+2N)).
  */
 bool
 pcmk__ends_with_ext(const char *s, const char *match)
 {
     return ends_with(s, match, true);
 }
 
 /*
  * This re-implements g_str_hash as it was prior to glib2-2.28:
  *
  * https://gitlab.gnome.org/GNOME/glib/commit/354d655ba8a54b754cb5a3efb42767327775696c
  *
  * Note that the new g_str_hash is presumably a *better* hash (it's actually
  * a correct implementation of DJB's hash), but we need to preserve existing
  * behaviour, because the hash key ultimately determines the "sort" order
  * when iterating through GHashTables, which affects allocation of scores to
  * clone instances when iterating through rsc->allowed_nodes.  It (somehow)
  * also appears to have some minor impact on the ordering of a few
  * pseudo_event IDs in the transition graph.
  */
 guint
 g_str_hash_traditional(gconstpointer v)
 {
     const signed char *p;
     guint32 h = 0;
 
     for (p = v; *p != '\0'; p++)
         h = (h << 5) - h + *p;
 
     return h;
 }
 
 /* used with hash tables where case does not matter */
 gboolean
 crm_strcase_equal(gconstpointer a, gconstpointer b)
 {
     return crm_str_eq((const char *) a, (const char *) b, FALSE);
 }
 
 guint
 crm_strcase_hash(gconstpointer v)
 {
     const signed char *p;
     guint32 h = 0;
 
     for (p = v; *p != '\0'; p++)
         h = (h << 5) - h + g_ascii_tolower(*p);
 
     return h;
 }
 
 static void
 copy_str_table_entry(gpointer key, gpointer value, gpointer user_data)
 {
     if (key && value && user_data) {
         g_hash_table_insert((GHashTable*)user_data, strdup(key), strdup(value));
     }
 }
 
 GHashTable *
 crm_str_table_dup(GHashTable *old_table)
 {
     GHashTable *new_table = NULL;
 
     if (old_table) {
         new_table = crm_str_table_new();
         g_hash_table_foreach(old_table, copy_str_table_entry, new_table);
     }
     return new_table;
 }
 
 /*!
  * \internal
  * \brief Add a word to a space-separated string list
  *
  * \param[in,out] list  Pointer to beginning of list
  * \param[in]     word  Word to add to list
  *
  * \return (Potentially new) beginning of list
  * \note This dynamically reallocates list as needed.
  */
 char *
 pcmk__add_word(char *list, const char *word)
 {
     if (word != NULL) {
         size_t len = list? strlen(list) : 0;
 
         list = realloc_safe(list, len + strlen(word) + 2); // 2 = space + EOS
         sprintf(list + len, " %s", word);
     }
     return list;
 }
 
 /*!
  * \internal
  * \brief Compress data
  *
  * \param[in]  data        Data to compress
  * \param[in]  length      Number of characters of data to compress
  * \param[in]  max         Maximum size of compressed data (or 0 to estimate)
  * \param[out] result      Where to store newly allocated compressed result
  * \param[out] result_len  Where to store actual compressed length of result
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__compress(const char *data, unsigned int length, unsigned int max,
                char **result, unsigned int *result_len)
 {
     int rc;
     char *compressed = NULL;
     char *uncompressed = strdup(data);
 #ifdef CLOCK_MONOTONIC
     struct timespec after_t;
     struct timespec before_t;
 #endif
 
     if (max == 0) {
         max = (length * 1.01) + 601; // Size guaranteed to hold result
     }
 
 #ifdef CLOCK_MONOTONIC
     clock_gettime(CLOCK_MONOTONIC, &before_t);
 #endif
 
     compressed = calloc((size_t) max, sizeof(char));
     CRM_ASSERT(compressed);
 
     *result_len = max;
     rc = BZ2_bzBuffToBuffCompress(compressed, result_len, uncompressed, length,
                                   CRM_BZ2_BLOCKS, 0, CRM_BZ2_WORK);
     free(uncompressed);
     if (rc != BZ_OK) {
         crm_err("Compression of %d bytes failed: %s " CRM_XS " bzerror=%d",
                 length, bz2_strerror(rc), rc);
         free(compressed);
         return pcmk_rc_error;
     }
 
 #ifdef CLOCK_MONOTONIC
     clock_gettime(CLOCK_MONOTONIC, &after_t);
 
     crm_trace("Compressed %d bytes into %d (ratio %d:1) in %.0fms",
              length, *result_len, length / (*result_len),
              (after_t.tv_sec - before_t.tv_sec) * 1000 +
              (after_t.tv_nsec - before_t.tv_nsec) / 1e6);
 #else
     crm_trace("Compressed %d bytes into %d (ratio %d:1)",
              length, *result_len, length / (*result_len));
 #endif
 
     *result = compressed;
     return pcmk_rc_ok;
 }
 
 char *
 crm_strdup_printf(char const *format, ...)
 {
     va_list ap;
     int len = 0;
     char *string = NULL;
 
     va_start(ap, format);
     len = vasprintf (&string, format, ap);
     CRM_ASSERT(len > 0);
     va_end(ap);
     return string;
 }
 
-bool
+int
 pcmk__split_range(const char *srcstring, char separator, char **start, char **end)
 {
     const char *seploc = NULL;
 
     CRM_ASSERT(start != NULL && end != NULL);
     *start = NULL;
     *end = NULL;
 
     crm_trace("Attempting to decode: [%s]", srcstring);
-    if (srcstring == NULL) {
-        return false;
+    if (srcstring == NULL || strcmp(srcstring, "") == 0) {
+        return pcmk_rc_unknown_format;
     }
 
     seploc = strchr(srcstring, separator);
     if (!seploc) {
-        return false;
+        return pcmk_rc_ok;
     } else if (strlen(srcstring) == 1) {
         /* The source string contained only the separator. */
-        return false;
+        return pcmk_rc_unknown_format;
     } else if (seploc == srcstring && *(seploc + 1)) {
         /* Separator is the first character of the range, so this
          * range only has an end.
          */
         *end = strdup(seploc + 1);
     } else if (! *(seploc + 1)) {
         /* Separator is the last character of the range, so this
          * range only has a start.
          */
         *start = strndup(srcstring, seploc - srcstring);
     } else {
         *start = strndup(srcstring, seploc - srcstring);
         *end = strdup(seploc + 1);
     }
 
-    return true;
+    return pcmk_rc_ok;
 }
diff --git a/lib/common/tests/strings/pcmk__split_range.c b/lib/common/tests/strings/pcmk__split_range.c
index 972e75dfeb..014f72d55d 100644
--- a/lib/common/tests/strings/pcmk__split_range.c
+++ b/lib/common/tests/strings/pcmk__split_range.c
@@ -1,82 +1,82 @@
 #include <glib.h>
 
 #include <crm_internal.h>
 
 static void
 empty_input_string(void) {
     char *start = NULL;
     char *end = NULL;
 
-    g_assert(pcmk__split_range(NULL, '-', &start, &end) == false);
-    g_assert(pcmk__split_range("", '-', &start, &end) == false);
+    g_assert(pcmk__split_range(NULL, '-', &start, &end) == pcmk_rc_unknown_format);
+    g_assert(pcmk__split_range("", '-', &start, &end) == pcmk_rc_unknown_format);
 }
 
 static void
 missing_separator(void) {
     char *start = NULL;
     char *end = NULL;
 
-    g_assert(pcmk__split_range("1234", '-', &start, &end) == false);
+    g_assert(pcmk__split_range("1234", '-', &start, &end) == pcmk_rc_ok);
     g_assert(start == NULL);
     g_assert(end == NULL);
 }
 
 static void
 only_separator(void) {
     char *start = NULL;
     char *end = NULL;
 
-    g_assert(pcmk__split_range("-", '-', &start, &end) == false);
+    g_assert(pcmk__split_range("-", '-', &start, &end) == pcmk_rc_unknown_format);
     g_assert(start == NULL);
     g_assert(end == NULL);
 }
 
 static void
 no_range_end(void) {
     char *start = NULL;
     char *end = NULL;
 
-    g_assert(pcmk__split_range("2000-", '-', &start, &end) == true);
+    g_assert(pcmk__split_range("2000-", '-', &start, &end) == pcmk_rc_ok);
     g_assert_cmpstr(start, ==, "2000");
     g_assert(end == NULL);
 
     free(start);
 }
 
 static void
 no_range_start(void) {
     char *start = NULL;
     char *end = NULL;
 
-    g_assert(pcmk__split_range("-2020", '-', &start, &end) == true);
+    g_assert(pcmk__split_range("-2020", '-', &start, &end) == pcmk_rc_ok);
     g_assert(start == NULL);
     g_assert_cmpstr(end, ==, "2020");
 
     free(end);
 }
 
 static void
 range_start_and_end(void) {
     char *start = NULL;
     char *end = NULL;
 
-    g_assert(pcmk__split_range("2000-2020", '-', &start, &end) == true);
+    g_assert(pcmk__split_range("2000-2020", '-', &start, &end) == pcmk_rc_ok);
     g_assert_cmpstr(start, ==, "2000");
     g_assert_cmpstr(end, ==, "2020");
 
     free(start);
     free(end);
 }
 
 int main(int argc, char **argv) {
     g_test_init(&argc, &argv, NULL);
 
     g_test_add_func("/common/strings/range/empty", empty_input_string);
     g_test_add_func("/common/strings/range/no_sep", missing_separator);
     g_test_add_func("/common/strings/range/only_sep", only_separator);
     g_test_add_func("/common/strings/range/no_end", no_range_end);
     g_test_add_func("/common/strings/range/no_start", no_range_start);
     g_test_add_func("/common/strings/range/start_and_end", range_start_and_end);
 
     return g_test_run();
 }
diff --git a/lib/pengine/rules.c b/lib/pengine/rules.c
index 609aefcdc8..2b25d20c3d 100644
--- a/lib/pengine/rules.c
+++ b/lib/pengine/rules.c
@@ -1,1172 +1,1174 @@
 /*
  * Copyright 2004-2019 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/crm.h>
 #include <crm/msg_xml.h>
 #include <crm/common/xml.h>
 
 #include <glib.h>
 
 #include <crm/pengine/rules.h>
 #include <crm/pengine/rules_internal.h>
 #include <crm/pengine/internal.h>
 
 #include <sys/types.h>
 #include <regex.h>
 #include <ctype.h>
 
 CRM_TRACE_INIT_DATA(pe_rules);
 
 /*!
  * \brief Evaluate any rules contained by given XML element
  *
  * \param[in]  xml          XML element to check for rules
  * \param[in]  node_hash    Node attributes to use when evaluating 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)
 {
     // If there are no rules, pass by default
     gboolean ruleset_default = TRUE;
 
     for (xmlNode *rule = first_named_child(ruleset, XML_TAG_RULE);
          rule != NULL; rule = crm_next_same_xml(rule)) {
 
         ruleset_default = FALSE;
         if (pe_test_rule(rule, node_hash, RSC_ROLE_UNKNOWN, now, next_change,
                          NULL)) {
             /* Only the deprecated "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;
 }
 
 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)
 {
     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);
     value = crm_element_value(rule, XML_RULE_ATTR_BOOLEAN_OP);
     if (safe_str_eq(value, "or")) {
         do_and = FALSE;
         passed = FALSE;
     }
 
     crm_trace("Testing rule %s", ID(rule));
     for (expr = __xml_first_child_element(rule); expr != NULL;
          expr = __xml_next_element(expr)) {
 
         test = pe_test_expression(expr, node_hash, role, now, next_change,
                                   match_data);
         empty = FALSE;
 
         if (test && do_and == FALSE) {
             crm_trace("Expression %s/%s passed", ID(rule), ID(expr));
             return TRUE;
 
         } else if (test == FALSE && do_and) {
             crm_trace("Expression %s/%s failed", ID(rule), ID(expr));
             return FALSE;
         }
     }
 
     if (empty) {
         crm_err("Invalid Rule %s: rules must contain at least one expression", ID(rule));
     }
 
     crm_trace("Rule %s %s", ID(rule), passed ? "passed" : "failed");
     return passed;
 }
 
 /*!
  * \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]  expr         Rule subelement XML
  * \param[in]  node_hash    Node attributes to use when evaluating expression
  * \param[in]  role         Resource role to use when evaluating expression
  * \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)
 {
     gboolean accept = FALSE;
     const char *uname = NULL;
 
     switch (find_expression_type(expr)) {
         case nested_rule:
             accept = pe_test_rule(expr, node_hash, role, now, next_change,
                                   match_data);
             break;
         case attr_expr:
         case loc_expr:
             /* these expressions can never succeed if there is
              * no node to compare with
              */
             if (node_hash != NULL) {
                 accept = pe_test_attr_expression(expr, node_hash, now, match_data);
             }
             break;
 
         case time_expr:
             accept = pe_test_date_expression(expr, now, next_change);
             break;
 
         case role_expr:
             accept = pe_test_role_expression(expr, role, now);
             break;
 
 #if ENABLE_VERSIONED_ATTRS
         case version_expr:
             if (node_hash && g_hash_table_lookup_extended(node_hash,
                                                           CRM_ATTR_RA_VERSION,
                                                           NULL, NULL)) {
                 accept = pe_test_attr_expression(expr, node_hash, now, NULL);
             } else {
                 // we are going to test it when we have ra-version
                 accept = TRUE;
             }
             break;
 #endif
 
         default:
             CRM_CHECK(FALSE /* bad type */ , return FALSE);
             accept = FALSE;
     }
     if (node_hash) {
         uname = g_hash_table_lookup(node_hash, CRM_ATTR_UNAME);
     }
 
     crm_trace("Expression %s %s on %s",
               ID(expr), accept ? "passed" : "failed", uname ? uname : "all nodes");
     return accept;
 }
 
 enum expression_type
 find_expression_type(xmlNode * expr)
 {
     const char *tag = NULL;
     const char *attr = NULL;
 
     attr = crm_element_value(expr, XML_EXPR_ATTR_ATTRIBUTE);
     tag = crm_element_name(expr);
 
     if (safe_str_eq(tag, "date_expression")) {
         return time_expr;
 
     } else if (safe_str_eq(tag, XML_TAG_RULE)) {
         return nested_rule;
 
     } else if (safe_str_neq(tag, "expression")) {
         return not_expr;
 
     } else if (safe_str_eq(attr, CRM_ATTR_UNAME)
                || safe_str_eq(attr, CRM_ATTR_KIND)
                || safe_str_eq(attr, CRM_ATTR_ID)) {
         return loc_expr;
 
     } else if (safe_str_eq(attr, CRM_ATTR_ROLE)) {
         return role_expr;
 
 #if ENABLE_VERSIONED_ATTRS
     } else if (safe_str_eq(attr, CRM_ATTR_RA_VERSION)) {
         return version_expr;
 #endif
     }
 
     return attr_expr;
 }
 
 gboolean
 pe_test_role_expression(xmlNode * expr, enum rsc_role_e role, crm_time_t * now)
 {
     gboolean accept = FALSE;
     const char *op = NULL;
     const char *value = NULL;
 
     if (role == RSC_ROLE_UNKNOWN) {
         return accept;
     }
 
     value = crm_element_value(expr, XML_EXPR_ATTR_VALUE);
     op = crm_element_value(expr, XML_EXPR_ATTR_OPERATION);
 
     if (safe_str_eq(op, "defined")) {
         if (role > RSC_ROLE_STARTED) {
             accept = TRUE;
         }
 
     } else if (safe_str_eq(op, "not_defined")) {
         if (role < RSC_ROLE_SLAVE && role > RSC_ROLE_UNKNOWN) {
             accept = TRUE;
         }
 
     } else if (safe_str_eq(op, "eq")) {
         if (text2role(value) == role) {
             accept = TRUE;
         }
 
     } else if (safe_str_eq(op, "ne")) {
         // Test "ne" only with promotable clone roles
         if (role < RSC_ROLE_SLAVE && role > RSC_ROLE_UNKNOWN) {
             accept = FALSE;
 
         } else if (text2role(value) != role) {
             accept = TRUE;
         }
     }
     return accept;
 }
 
 gboolean
 pe_test_attr_expression(xmlNode *expr, GHashTable *hash, crm_time_t *now,
                         pe_match_data_t *match_data)
 {
     gboolean accept = FALSE;
     gboolean attr_allocated = FALSE;
     int cmp = 0;
     const char *h_val = NULL;
     GHashTable *table = NULL;
 
     const char *op = NULL;
     const char *type = NULL;
     const char *attr = NULL;
     const char *value = NULL;
     const char *value_source = NULL;
 
     attr = crm_element_value(expr, XML_EXPR_ATTR_ATTRIBUTE);
     op = crm_element_value(expr, XML_EXPR_ATTR_OPERATION);
     value = crm_element_value(expr, XML_EXPR_ATTR_VALUE);
     type = crm_element_value(expr, XML_EXPR_ATTR_TYPE);
     value_source = crm_element_value(expr, XML_EXPR_ATTR_VALUE_SOURCE);
 
     if (attr == NULL || op == NULL) {
         pe_err("Invalid attribute or operation in expression"
                " (\'%s\' \'%s\' \'%s\')", crm_str(attr), crm_str(op), crm_str(value));
         return FALSE;
     }
 
     if (match_data) {
         if (match_data->re) {
             char *resolved_attr = pe_expand_re_matches(attr, match_data->re);
 
             if (resolved_attr) {
                 attr = (const char *) resolved_attr;
                 attr_allocated = TRUE;
             }
         }
 
         if (safe_str_eq(value_source, "param")) {
             table = match_data->params;
         } else if (safe_str_eq(value_source, "meta")) {
             table = match_data->meta;
         }
     }
 
     if (table) {
         const char *param_name = value;
         const char *param_value = NULL;
 
         if (param_name && param_name[0]) {
             if ((param_value = (const char *)g_hash_table_lookup(table, param_name))) {
                 value = param_value;
             }
         }
     }
 
     if (hash != NULL) {
         h_val = (const char *)g_hash_table_lookup(hash, attr);
     }
 
     if (attr_allocated) {
         free((char *)attr);
         attr = NULL;
     }
 
     if (value != NULL && h_val != NULL) {
         if (type == NULL) {
             if (safe_str_eq(op, "lt")
                 || safe_str_eq(op, "lte")
                 || safe_str_eq(op, "gt")
                 || safe_str_eq(op, "gte")) {
                 type = "number";
 
             } else {
                 type = "string";
             }
             crm_trace("Defaulting to %s based comparison for '%s' op", type, op);
         }
 
         if (safe_str_eq(type, "string")) {
             cmp = strcasecmp(h_val, value);
 
         } else if (safe_str_eq(type, "number")) {
             int h_val_f = crm_parse_int(h_val, NULL);
             int value_f = crm_parse_int(value, NULL);
 
             if (h_val_f < value_f) {
                 cmp = -1;
             } else if (h_val_f > value_f) {
                 cmp = 1;
             } else {
                 cmp = 0;
             }
 
         } else if (safe_str_eq(type, "version")) {
             cmp = compare_version(h_val, value);
 
         }
 
     } else if (value == NULL && h_val == NULL) {
         cmp = 0;
     } else if (value == NULL) {
         cmp = 1;
     } else {
         cmp = -1;
     }
 
     if (safe_str_eq(op, "defined")) {
         if (h_val != NULL) {
             accept = TRUE;
         }
 
     } else if (safe_str_eq(op, "not_defined")) {
         if (h_val == NULL) {
             accept = TRUE;
         }
 
     } else if (safe_str_eq(op, "eq")) {
         if ((h_val == value) || cmp == 0) {
             accept = TRUE;
         }
 
     } else if (safe_str_eq(op, "ne")) {
         if ((h_val == NULL && value != NULL)
             || (h_val != NULL && value == NULL)
             || cmp != 0) {
             accept = TRUE;
         }
 
     } else if (value == NULL || h_val == NULL) {
         // The comparison is meaningless from this point on
         accept = FALSE;
 
     } else if (safe_str_eq(op, "lt")) {
         if (cmp < 0) {
             accept = TRUE;
         }
 
     } else if (safe_str_eq(op, "lte")) {
         if (cmp <= 0) {
             accept = TRUE;
         }
 
     } else if (safe_str_eq(op, "gt")) {
         if (cmp > 0) {
             accept = TRUE;
         }
 
     } else if (safe_str_eq(op, "gte")) {
         if (cmp >= 0) {
             accept = TRUE;
         }
     }
 
     return accept;
 }
 
 /* As per the nethack rules:
  *
  * moon period = 29.53058 days ~= 30, year = 365.2422 days
  * days moon phase advances on first day of year compared to preceding year
  *      = 365.2422 - 12*29.53058 ~= 11
  * years in Metonic cycle (time until same phases fall on the same days of
  *      the month) = 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
  * 177 ~= 8 reported phases * 22
  * + 11/22 for rounding
  *
  * 0-7, with 0: new, 4: full
  */
 
 static int
 phase_of_the_moon(crm_time_t * now)
 {
     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);
 }
 
 #define cron_check(xml_field, time_field)				\
     value = crm_element_value(cron_spec, xml_field);			\
     if(value != NULL) {							\
 	gboolean pass = TRUE;						\
-	gboolean rc = pcmk__split_range(value, '-', &value_low, &value_high); \
-        if (rc == false) {                                              \
+	int rc = pcmk__split_range(value, '-', &value_low, &value_high);\
+        if (rc == pcmk_rc_unknown_format) {                             \
+            return FALSE;                                               \
+        } else if (value_low == NULL && value_high == NULL) {           \
             value_low_i = crm_parse_int(value, NULL);                   \
             if (value_low_i != time_field) {                            \
                 pass = FALSE;                                           \
             }                                                           \
         } else if (value_low == NULL && value_high != NULL) {           \
             value_high_i = crm_parse_int(value_high, "-1");             \
             if (time_field > value_high_i) {                            \
                 pass = FALSE;                                           \
             }                                                           \
         } else if (value_low != NULL && value_high == NULL) {           \
             value_low_i = crm_parse_int(value_low, "0");                \
             if (value_low_i > time_field) {                             \
                 pass = FALSE;                                           \
             }                                                           \
 	} else {                                                        \
             value_low_i = crm_parse_int(value_low, "0");                \
             value_high_i = crm_parse_int(value_high, "-1");             \
             if (value_low_i > time_field || value_high_i < time_field) {\
                 pass = FALSE;                                           \
             }                                                           \
         }                                                               \
 	free(value_low);						\
 	free(value_high);						\
 	if(pass == FALSE) {						\
 	    crm_debug("Condition '%s' in %s: failed", value, xml_field); \
 	    return pass;						\
 	}								\
 	crm_debug("Condition '%s' in %s: passed", value, xml_field);	\
     }
 
 gboolean
 pe_cron_range_satisfied(crm_time_t * now, xmlNode * cron_spec)
 {
     const char *value = NULL;
     char *value_low = NULL;
     char *value_high = NULL;
 
     int value_low_i = 0;
     int value_high_i = 0;
 
     uint32_t h, m, s, y, d, w;
 
     CRM_CHECK(now != NULL, return FALSE);
 
     crm_time_get_timeofday(now, &h, &m, &s);
 
     cron_check("seconds", s);
     cron_check("minutes", m);
     cron_check("hours", h);
 
     crm_time_get_gregorian(now, &y, &m, &d);
 
     cron_check("monthdays", d);
     cron_check("months", m);
     cron_check("years", y);
 
     crm_time_get_ordinal(now, &y, &d);
 
     cron_check("yeardays", d);
 
     crm_time_get_isoweek(now, &y, &w, &d);
 
     cron_check("weekyears", y);
     cron_check("weeks", w);
     cron_check("weekdays", d);
 
     cron_check("moon", phase_of_the_moon(now));
 
     return TRUE;
 }
 
 #define update_field(xml_field, time_fn)			\
     value = crm_element_value(duration_spec, xml_field);	\
     if(value != NULL) {						\
 	int value_i = crm_parse_int(value, "0");		\
 	time_fn(end, value_i);					\
     }
 
 crm_time_t *
 pe_parse_xml_duration(crm_time_t * start, xmlNode * duration_spec)
 {
     crm_time_t *end = NULL;
     const char *value = NULL;
 
     end = crm_time_new(NULL);
     crm_time_set(end, start);
 
     update_field("years", crm_time_add_years);
     update_field("months", crm_time_add_months);
     update_field("weeks", crm_time_add_weeks);
     update_field("days", crm_time_add_days);
     update_field("hours", crm_time_add_hours);
     update_field("minutes", crm_time_add_minutes);
     update_field("seconds", crm_time_add_seconds);
 
     return end;
 }
 
 /*!
  * \internal
  * \brief Test a date expression (pass/fail) for a specific time
  *
  * \param[in]  time_expr    date_expression XML
  * \param[in]  now          Time for which to evaluate expression
  * \param[out] next_change  If not NULL, set to when evaluation will change
  *
  * \return TRUE if date expression is in effect at given time, FALSE otherwise
  */
 gboolean
 pe_test_date_expression(xmlNode *time_expr, crm_time_t *now,
                         crm_time_t *next_change)
 {
     switch (pe_eval_date_expression(time_expr, now, next_change)) {
         case pe_date_within_range:
         case pe_date_op_satisfied:
             return TRUE;
 
         default:
             return FALSE;
     }
 }
 
 // Set next_change to t if t is earlier
 static void
 crm_time_set_if_earlier(crm_time_t *next_change, crm_time_t *t)
 {
     if ((next_change != NULL) && (t != NULL)) {
         if (!crm_time_is_defined(next_change)
             || (crm_time_compare(t, next_change) < 0)) {
             crm_time_set(next_change, t);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Evaluate a date expression for a specific time
  *
  * \param[in]  time_expr    date_expression XML
  * \param[in]  now          Time for which to evaluate expression
  * \param[out] next_change  If not NULL, set to when evaluation will change
  *
  * \return Evaluation result
  */
 pe_eval_date_result_t
 pe_eval_date_expression(xmlNode *time_expr, crm_time_t *now,
                         crm_time_t *next_change)
 {
     crm_time_t *start = NULL;
     crm_time_t *end = NULL;
     const char *value = NULL;
     const char *op = crm_element_value(time_expr, "operation");
 
     xmlNode *duration_spec = NULL;
     xmlNode *date_spec = NULL;
 
     // "undetermined" will also be returned for parsing errors
     pe_eval_date_result_t rc = pe_date_result_undetermined;
 
     crm_trace("Testing expression: %s", ID(time_expr));
 
     duration_spec = first_named_child(time_expr, "duration");
     date_spec = first_named_child(time_expr, "date_spec");
 
     value = crm_element_value(time_expr, "start");
     if (value != NULL) {
         start = crm_time_new(value);
     }
     value = crm_element_value(time_expr, "end");
     if (value != NULL) {
         end = crm_time_new(value);
     }
 
     if (start != NULL && end == NULL && duration_spec != NULL) {
         end = pe_parse_xml_duration(start, duration_spec);
     }
 
     if ((op == NULL) || safe_str_eq(op, "in_range")) {
         if ((start == NULL) && (end == NULL)) {
             // in_range requires at least one of start or end
         } else if ((start != NULL) && (crm_time_compare(now, start) < 0)) {
             rc = pe_date_before_range;
             crm_time_set_if_earlier(next_change, start);
         } else if ((end != NULL) && (crm_time_compare(now, end) > 0)) {
             rc = pe_date_after_range;
         } else {
             rc = pe_date_within_range;
             if (end && next_change) {
                 // Evaluation doesn't change until second after end
                 crm_time_add_seconds(end, 1);
                 crm_time_set_if_earlier(next_change, end);
             }
         }
 
     } else if (safe_str_eq(op, "date_spec")) {
         rc = pe_cron_range_satisfied(now, date_spec) ? pe_date_op_satisfied
                                                      : pe_date_op_unsatisfied;
         // @TODO set next_change appropriately
 
     } else if (safe_str_eq(op, "gt")) {
         if (start == NULL) {
             // gt requires start
         } else if (crm_time_compare(now, start) > 0) {
             rc = pe_date_within_range;
         } else {
             rc = pe_date_before_range;
 
             // Evaluation doesn't change until second after start
             crm_time_add_seconds(start, 1);
             crm_time_set_if_earlier(next_change, start);
         }
 
     } else if (safe_str_eq(op, "lt")) {
         if (end == NULL) {
             // lt requires end
         } else if (crm_time_compare(now, end) < 0) {
             rc = pe_date_within_range;
             crm_time_set_if_earlier(next_change, end);
         } else {
             rc = pe_date_after_range;
         }
     }
 
     crm_time_free(start);
     crm_time_free(end);
     return rc;
 }
 
 // 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
 } 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 (safe_str_eq(pair_a->name, pair_a->special_name)) {
         return -1;
 
     } else if (safe_str_eq(pair_b->name, pair_a->special_name)) {
         return 1;
     }
 
     if (pair_a->score < pair_b->score) {
         return 1;
     } else if (pair_a->score > pair_b->score) {
         return -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;
 
     name = crm_element_name(list->children);
     if (safe_str_eq(XML_TAG_ATTRS, name)) {
         list = list->children;
     }
 
     for (an_attr = __xml_first_child_element(list); an_attr != NULL;
          an_attr = __xml_next_element(an_attr)) {
 
         if (crm_str_eq((const char *)an_attr->name, XML_CIB_TAG_NVPAIR, TRUE)) {
             xmlNode *ref_nvpair = expand_idref(an_attr, top);
 
             name = crm_element_value(an_attr, XML_NVPAIR_ATTR_NAME);
             if (name == NULL) {
                 name = crm_element_value(ref_nvpair, XML_NVPAIR_ATTR_NAME);
             }
 
             crm_trace("Setting attribute: %s", name);
             value = crm_element_value(an_attr, XML_NVPAIR_ATTR_VALUE);
             if (value == NULL) {
                 value = crm_element_value(ref_nvpair, XML_NVPAIR_ATTR_VALUE);
             }
 
             if (name == NULL || value == NULL) {
                 continue;
 
             }
 
             old_value = g_hash_table_lookup(hash, name);
 
             if (safe_str_eq(value, "#default")) {
                 if (old_value) {
                     crm_trace("Removing value for %s (%s)", name, value);
                     g_hash_table_remove(hash, name);
                 }
                 continue;
 
             } else if (old_value == NULL) {
                 g_hash_table_insert(hash, strdup(name), strdup(value));
 
             } else if (overwrite) {
                 crm_debug("Overwriting value of %s: %s -> %s", name, old_value, value);
                 g_hash_table_replace(hash, strdup(name), strdup(value));
             }
         }
     }
 }
 
 #if ENABLE_VERSIONED_ATTRS
 static xmlNode*
 get_versioned_rule(xmlNode * attr_set)
 {
     xmlNode * rule = NULL;
     xmlNode * expr = NULL;
 
     for (rule = __xml_first_child_element(attr_set); rule != NULL;
          rule = __xml_next_element(rule)) {
 
         if (crm_str_eq((const char *)rule->name, XML_TAG_RULE, TRUE)) {
             for (expr = __xml_first_child_element(rule); expr != NULL;
                  expr = __xml_next_element(expr)) {
 
                 if (find_expression_type(expr) == version_expr) {
                     return rule;
                 }
             }
         }
     }
 
     return NULL;
 }
 
 static void
 add_versioned_attributes(xmlNode * attr_set, xmlNode * versioned_attrs)
 {
     xmlNode *attr_set_copy = NULL;
     xmlNode *rule = NULL;
     xmlNode *expr = NULL;
 
     if (!attr_set || !versioned_attrs) {
         return;
     }
 
     attr_set_copy = copy_xml(attr_set);
 
     rule = get_versioned_rule(attr_set_copy);
     if (!rule) {
         free_xml(attr_set_copy);
         return;
     }
 
     expr = __xml_first_child_element(rule);
     while (expr != NULL) {
         if (find_expression_type(expr) != version_expr) {
             xmlNode *node = expr;
 
             expr = __xml_next_element(expr);
             free_xml(node);
         } else {
             expr = __xml_next_element(expr);
         }
     }
 
     add_node_nocopy(versioned_attrs, NULL, attr_set_copy);
 }
 #endif
 
 typedef struct unpack_data_s {
     gboolean overwrite;
     GHashTable *node_hash;
     void *hash;
     crm_time_t *now;
     crm_time_t *next_change;
     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_evaluate_rules(pair->attr_set, unpack_data->node_hash,
                            unpack_data->now, unpack_data->next_change)) {
         return;
     }
 
 #if ENABLE_VERSIONED_ATTRS
     if (get_versioned_rule(pair->attr_set) && !(unpack_data->node_hash &&
         g_hash_table_lookup_extended(unpack_data->node_hash,
                                      CRM_ATTR_RA_VERSION, NULL, NULL))) {
         // we haven't actually tested versioned expressions yet
         return;
     }
 #endif
 
     crm_trace("Adding attributes from %s", pair->name);
     populate_hash(pair->attr_set, unpack_data->hash, unpack_data->overwrite, unpack_data->top);
 }
 
 #if ENABLE_VERSIONED_ATTRS
 static void
 unpack_versioned_attr_set(gpointer data, gpointer user_data)
 {
     sorted_set_t *pair = data;
     unpack_data_t *unpack_data = user_data;
 
     if (pe_evaluate_rules(pair->attr_set, unpack_data->node_hash,
                           unpack_data->now, unpack_data->next_change)) {
         add_versioned_attributes(pair->attr_set, unpack_data->hash);
     }
 }
 #endif
 
 /*!
  * \internal
  * \brief Create a sorted list of nvpair blocks
  *
  * \param[in]  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 type
  * \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, xmlNode *xml_obj, const char *set_name,
            const char *always_first)
 {
     GListPtr unsorted = NULL;
     const char *score = NULL;
     sorted_set_t *pair = NULL;
     xmlNode *attr_set = NULL;
 
     if (xml_obj == NULL) {
         crm_trace("No instance attributes");
         return NULL;
     }
 
     crm_trace("Checking for attributes");
     for (attr_set = __xml_first_child_element(xml_obj); attr_set != NULL;
          attr_set = __xml_next_element(attr_set)) {
 
         /* Uncertain if set_name == NULL check is strictly necessary here */
         if (set_name == NULL || crm_str_eq((const char *)attr_set->name, set_name, TRUE)) {
             pair = NULL;
             attr_set = expand_idref(attr_set, top);
             if (attr_set == NULL) {
                 continue;
             }
 
             pair = calloc(1, sizeof(sorted_set_t));
             pair->name = ID(attr_set);
             pair->special_name = always_first;
             pair->attr_set = attr_set;
 
             score = crm_element_value(attr_set, XML_RULE_ATTR_SCORE);
             pair->score = char2score(score);
 
             unsorted = g_list_prepend(unsorted, pair);
         }
     }
     return g_list_sort(unsorted, sort_pairs);
 }
 
 /*!
  * \internal
  * \brief Extract nvpair blocks contained by an XML element into a hash table
  *
  * \param[in]  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 type
  * \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 rule evaluation will change
  * \param[in]  unpack_func   Function to call to unpack each block
  */
 static void
 unpack_nvpair_blocks(xmlNode *top, xmlNode *xml_obj, const char *set_name,
                      GHashTable *node_hash, void *hash,
                      const char *always_first, gboolean overwrite,
                      crm_time_t *now, crm_time_t *next_change,
                      GFunc unpack_func)
 {
     GList *pairs = make_pairs(top, xml_obj, set_name, always_first);
 
     if (pairs) {
         unpack_data_t data = {
             .hash = hash,
             .node_hash = node_hash,
             .now = now,
             .overwrite = overwrite,
             .next_change = next_change,
             .top = top,
         };
 
         g_list_foreach(pairs, unpack_func, &data);
         g_list_free_full(pairs, free);
     }
 }
 
 /*!
  * \brief Extract nvpair blocks contained by an XML element into a hash table
  *
  * \param[in]  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 rule evaluation will change
  */
 void
 pe_unpack_nvpairs(xmlNode *top, 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)
 {
     unpack_nvpair_blocks(top, xml_obj, set_name, node_hash, hash, always_first,
                          overwrite, now, next_change, unpack_attr_set);
 }
 
 #if ENABLE_VERSIONED_ATTRS
 void
 pe_unpack_versioned_attributes(xmlNode *top, xmlNode *xml_obj,
                                const char *set_name, GHashTable *node_hash,
                                xmlNode *hash, crm_time_t *now,
                                crm_time_t *next_change)
 {
     unpack_nvpair_blocks(top, xml_obj, set_name, node_hash, hash, NULL, FALSE,
                          now, next_change, unpack_versioned_attr_set);
 }
 #endif
 
 char *
 pe_expand_re_matches(const char *string, pe_re_match_data_t *match_data)
 {
     size_t len = 0;
     int i;
     const char *p, *last_match_index;
     char *p_dst, *result = NULL;
 
     if (!string || string[0] == '\0' || !match_data) {
         return NULL;
     }
 
     p = last_match_index = string;
 
     while (*p) {
         if (*p == '%' && *(p + 1) && isdigit(*(p + 1))) {
             i = *(p + 1) - '0';
             if (match_data->nregs >= i && match_data->pmatch[i].rm_so != -1 &&
                 match_data->pmatch[i].rm_eo > match_data->pmatch[i].rm_so) {
                 len += p - last_match_index + (match_data->pmatch[i].rm_eo - match_data->pmatch[i].rm_so);
                 last_match_index = p + 2;
             }
             p++;
         }
         p++;
     }
     len += p - last_match_index + 1;
 
     /* FIXME: Excessive? */
     if (len - 1 <= 0) {
         return NULL;
     }
 
     p_dst = result = calloc(1, len);
     p = string;
 
     while (*p) {
         if (*p == '%' && *(p + 1) && isdigit(*(p + 1))) {
             i = *(p + 1) - '0';
             if (match_data->nregs >= i && match_data->pmatch[i].rm_so != -1 &&
                 match_data->pmatch[i].rm_eo > match_data->pmatch[i].rm_so) {
                 /* rm_eo can be equal to rm_so, but then there is nothing to do */
                 int match_len = match_data->pmatch[i].rm_eo - match_data->pmatch[i].rm_so;
                 memcpy(p_dst, match_data->string + match_data->pmatch[i].rm_so, match_len);
                 p_dst += match_len;
             }
             p++;
         } else {
             *(p_dst) = *(p);
             p_dst++;
         }
         p++;
     }
 
     return result;
 }
 
 #if ENABLE_VERSIONED_ATTRS
 GHashTable*
 pe_unpack_versioned_parameters(xmlNode *versioned_params, const char *ra_version)
 {
     GHashTable *hash = crm_str_table_new();
 
     if (versioned_params && ra_version) {
         GHashTable *node_hash = crm_str_table_new();
         xmlNode *attr_set = __xml_first_child_element(versioned_params);
 
         if (attr_set) {
             g_hash_table_insert(node_hash, strdup(CRM_ATTR_RA_VERSION),
                                 strdup(ra_version));
             pe_unpack_nvpairs(NULL, versioned_params,
                               crm_element_name(attr_set), node_hash, hash, NULL,
                               FALSE, NULL, NULL);
         }
 
         g_hash_table_destroy(node_hash);
     }
 
     return hash;
 }
 #endif
 
 // Deprecated functions kept only for backward API compatibility
 gboolean test_ruleset(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now);
 gboolean test_rule(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role,
                    crm_time_t *now);
 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);
 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);
 gboolean test_expression(xmlNode *expr, GHashTable *node_hash,
                          enum rsc_role_e role, crm_time_t *now);
 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);
 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);
 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);
 
 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
 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);
 }
 
 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)
 {
     unpack_nvpair_blocks(top, xml_obj, set_name, node_hash, hash, always_first,
                          overwrite, now, NULL, unpack_attr_set);
 }