diff --git a/include/crm/common/util.h b/include/crm/common/util.h
index 93fb9767ee..d6a91508d1 100644
--- a/include/crm/common/util.h
+++ b/include/crm/common/util.h
@@ -1,229 +1,230 @@
 /*
  * 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/acl.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)));
 int pcmk__parse_ll_range(const char *srcstring, long long *start, long long *end);
+gboolean pcmk__str_in_list(GList *lst, const gchar *s);
 
 #  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 *transition_id,
                                int *action_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         = 0,
     pcmk_ra_cap_provider     = (1 << 0), // Requires provider
     pcmk_ra_cap_status       = (1 << 1), // Supports status instead of monitor
     pcmk_ra_cap_params       = (1 << 2), // Supports parameters
     pcmk_ra_cap_unique       = (1 << 3), // Supports unique clones
     pcmk_ra_cap_promotable   = (1 << 4), // Supports promotable clones
     pcmk_ra_cap_stdin        = (1 << 5), // Reads from standard input
 };
 
 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
 
 char *pcmk_hostname(void);
 
 bool pcmk_str_is_infinity(const char *s);
 bool pcmk_str_is_minus_infinity(const char *s);
 
 #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
 
 //! \deprecated Use pcmk_get_ra_caps() instead
 bool crm_provider_required(const char *standard);
 
 #endif // PCMK__NO_COMPAT
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif
diff --git a/lib/common/strings.c b/lib/common/strings.c
index cae60345ae..a2e17ae609 100644
--- a/lib/common/strings.c
+++ b/lib/common/strings.c
@@ -1,657 +1,671 @@
 /*
  * 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] prefix 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 (pcmk__str_empty(match)) {
         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;
 }
 
 int
 pcmk__parse_ll_range(const char *srcstring, long long *start, long long *end)
 {
     char *remainder = NULL;
 
     CRM_ASSERT(start != NULL && end != NULL);
 
     *start = -1;
     *end = -1;
 
     crm_trace("Attempting to decode: [%s]", srcstring);
     if (srcstring == NULL || strcmp(srcstring, "") == 0 || strcmp(srcstring, "-") == 0) {
         return pcmk_rc_unknown_format;
     }
 
     /* String starts with a dash, so this is either a range with
      * no beginning or garbage.
      * */
     if (*srcstring == '-') {
         int rc = scan_ll(srcstring+1, end, &remainder);
 
         if (rc != pcmk_rc_ok || *remainder != '\0') {
             return pcmk_rc_unknown_format;
         } else {
             return pcmk_rc_ok;
         }
     }
 
     if (scan_ll(srcstring, start, &remainder) != pcmk_rc_ok) {
         return pcmk_rc_unknown_format;
     }
 
     if (*remainder && *remainder == '-') {
         if (*(remainder+1)) {
             char *more_remainder = NULL;
             int rc = scan_ll(remainder+1, end, &more_remainder);
 
             if (rc != pcmk_rc_ok || *more_remainder != '\0') {
                 return pcmk_rc_unknown_format;
             }
         }
     } else if (*remainder && *remainder != '-') {
         *start = -1;
         return pcmk_rc_unknown_format;
     } else {
         /* The input string contained only one number.  Set start and end
          * to the same value and return pcmk_rc_ok.  This gives the caller
          * a way to tell this condition apart from a range with no end.
          */
         *end = *start;
     }
 
     return pcmk_rc_ok;
 }
+
+gboolean
+pcmk__str_in_list(GList *lst, const gchar *s)
+{
+    if (lst == NULL) {
+        return FALSE;
+    }
+
+    if (strcmp(lst->data, "*") == 0 && lst->next == NULL) {
+        return TRUE;
+    }
+
+    return g_list_find_custom(lst, s, (GCompareFunc) strcmp) != NULL;
+}