diff --git a/daemons/schedulerd/sched_notif.c b/daemons/schedulerd/sched_notif.c
index 700f78f09b..8ffdb9597b 100644
--- a/daemons/schedulerd/sched_notif.c
+++ b/daemons/schedulerd/sched_notif.c
@@ -1,815 +1,837 @@
 /*
- * Copyright 2004-2018 Andrew Beekhof <andrew@beekhof.net>
+ * Copyright 2004-2019 Andrew Beekhof <andrew@beekhof.net>
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 #include <crm/msg_xml.h>
 #include <sched_allocate.h>
 #include <sched_notif.h>
 #include <sched_utils.h>
 
 typedef struct notify_entry_s {
     resource_t *rsc;
     node_t *node;
 } notify_entry_t;
 
 static gint
 sort_notify_entries(gconstpointer a, gconstpointer b)
 {
     int tmp;
     const notify_entry_t *entry_a = a;
     const notify_entry_t *entry_b = b;
 
     if (entry_a == NULL && entry_b == NULL) {
         return 0;
     }
     if (entry_a == NULL) {
         return 1;
     }
     if (entry_b == NULL) {
         return -1;
     }
 
     if (entry_a->rsc == NULL && entry_b->rsc == NULL) {
         return 0;
     }
     if (entry_a->rsc == NULL) {
         return 1;
     }
     if (entry_b->rsc == NULL) {
         return -1;
     }
 
     tmp = strcmp(entry_a->rsc->id, entry_b->rsc->id);
     if (tmp != 0) {
         return tmp;
     }
 
     if (entry_a->node == NULL && entry_b->node == NULL) {
         return 0;
     }
     if (entry_a->node == NULL) {
         return 1;
     }
     if (entry_b->node == NULL) {
         return -1;
     }
 
     return strcmp(entry_a->node->details->id, entry_b->node->details->id);
 }
 
 static notify_entry_t *dup_notify_entry(notify_entry_t *entry)
 {
     notify_entry_t *dup = malloc(sizeof(notify_entry_t));
 
     CRM_ASSERT(dup != NULL);
     dup->rsc = entry->rsc;
     dup->node = entry->node;
     return dup;
 }
 
 static void
 expand_node_list(GListPtr list, char **uname, char **metal)
 {
     GListPtr gIter = NULL;
     char *node_list = NULL;
     char *metal_list = NULL;
 
     CRM_ASSERT(uname != NULL);
     if (list == NULL) {
         *uname = strdup(" ");
         if(metal) {
             *metal = strdup(" ");
         }
         return;
     }
 
     for (gIter = list; gIter != NULL; gIter = gIter->next) {
         int len = 0;
         int existing_len = 0;
         node_t *node = (node_t *) gIter->data;
 
         if (node->details->uname == NULL) {
             continue;
         }
         len = 2 + strlen(node->details->uname);
 
         if(node_list) {
             existing_len = strlen(node_list);
         }
 //            crm_trace("Adding %s (%dc) at offset %d", node->details->uname, len - 2, existing_len);
         node_list = realloc_safe(node_list, len + existing_len);
         sprintf(node_list + existing_len, "%s%s", existing_len == 0 ? "":" ", node->details->uname);
 
         if(metal) {
             existing_len = 0;
             if(metal_list) {
                 existing_len = strlen(metal_list);
             }
 
             if(node->details->remote_rsc
                && node->details->remote_rsc->container
                && node->details->remote_rsc->container->running_on) {
                 node = pe__current_node(node->details->remote_rsc->container);
             }
 
             if (node->details->uname == NULL) {
                 continue;
             }
             len = 2 + strlen(node->details->uname);
             metal_list = realloc_safe(metal_list, len + existing_len);
             sprintf(metal_list + existing_len, "%s%s", existing_len == 0 ? "":" ", node->details->uname);
         }
     }
 
     *uname = node_list;
     if(metal) {
         *metal = metal_list;
     }
 }
 
 static void
 expand_list(GListPtr list, char **rsc_list, char **node_list)
 {
     GListPtr gIter = NULL;
     const char *uname = NULL;
     const char *rsc_id = NULL;
     const char *last_rsc_id = NULL;
 
     if (rsc_list) {
         *rsc_list = NULL;
     }
 
     if (list == NULL) {
         if (rsc_list) {
             *rsc_list = strdup(" ");
         }
         if (node_list) {
             *node_list = strdup(" ");
         }
         return;
     }
 
     if (node_list) {
         *node_list = NULL;
     }
 
     for (gIter = list; gIter != NULL; gIter = gIter->next) {
         notify_entry_t *entry = (notify_entry_t *) gIter->data;
 
         CRM_LOG_ASSERT(entry != NULL);
         CRM_LOG_ASSERT(entry && entry->rsc != NULL);
 
         if(entry == NULL || entry->rsc == NULL) {
             continue;
         }
 
         /* Uh, why? */
         CRM_LOG_ASSERT(node_list == NULL || entry->node != NULL);
         if(node_list != NULL && entry->node == NULL) {
             continue;
         }
 
         uname = NULL;
         rsc_id = entry->rsc->id;
         CRM_ASSERT(rsc_id != NULL);
 
         /* filter dups */
         if (safe_str_eq(rsc_id, last_rsc_id)) {
             continue;
         }
         last_rsc_id = rsc_id;
 
         if (rsc_list != NULL) {
             int existing_len = 0;
             int len = 2 + strlen(rsc_id);       /* +1 space, +1 EOS */
 
             if (*rsc_list) {
                 existing_len = strlen(*rsc_list);
             }
 
             crm_trace("Adding %s (%dc) at offset %d", rsc_id, len - 2, existing_len);
             *rsc_list = realloc_safe(*rsc_list, len + existing_len);
             sprintf(*rsc_list + existing_len, "%s%s", existing_len == 0 ? "":" ", rsc_id);
         }
 
         if (entry->node != NULL) {
             uname = entry->node->details->uname;
         }
 
         if (node_list != NULL && uname) {
             int existing_len = 0;
             int len = 2 + strlen(uname);
 
             if (*node_list) {
                 existing_len = strlen(*node_list);
             }
 
             crm_trace("Adding %s (%dc) at offset %d", uname, len - 2, existing_len);
             *node_list = realloc_safe(*node_list, len + existing_len);
             sprintf(*node_list + existing_len, "%s%s", existing_len == 0 ? "":" ", uname);
         }
     }
 
 }
 
 static void
 dup_attr(gpointer key, gpointer value, gpointer user_data)
 {
     add_hash_param(user_data, key, value);
 }
 
+static void
+add_notify_data_to_action_meta(notify_data_t *n_data, pe_action_t *action)
+{
+    for (GList *item = n_data->keys; item; item = item->next) {
+        pcmk_nvpair_t *nvpair = item->data;
+
+        add_hash_param(action->meta, nvpair->name, nvpair->value);
+    }
+}
+
 static action_t *
 pe_notify(resource_t * rsc, node_t * node, action_t * op, action_t * confirm,
           notify_data_t * n_data, pe_working_set_t * data_set)
 {
     char *key = NULL;
     action_t *trigger = NULL;
     const char *value = NULL;
     const char *task = NULL;
 
     if (op == NULL || confirm == NULL) {
         pe_rsc_trace(rsc, "Op=%p confirm=%p", op, confirm);
         return NULL;
     }
 
     CRM_CHECK(rsc != NULL, return NULL);
     CRM_CHECK(node != NULL, return NULL);
 
     if (node->details->online == FALSE) {
         pe_rsc_trace(rsc, "Skipping notification for %s: node offline", rsc->id);
         return NULL;
     } else if (is_set(op->flags, pe_action_runnable) == FALSE) {
         pe_rsc_trace(rsc, "Skipping notification for %s: not runnable", op->uuid);
         return NULL;
     }
 
     value = g_hash_table_lookup(op->meta, "notify_type");
     task = g_hash_table_lookup(op->meta, "notify_operation");
 
     pe_rsc_trace(rsc, "Creating notify actions for %s: %s (%s-%s)", op->uuid, rsc->id, value, task);
 
     key = generate_notify_key(rsc->id, value, task);
     trigger = custom_action(rsc, key, op->task, node,
                             is_set(op->flags, pe_action_optional), TRUE, data_set);
     g_hash_table_foreach(op->meta, dup_attr, trigger->meta);
-    g_hash_table_foreach(n_data->keys, dup_attr, trigger->meta);
+    add_notify_data_to_action_meta(n_data, trigger);
 
     /* pseudo_notify before notify */
     pe_rsc_trace(rsc, "Ordering %s before %s (%d->%d)", op->uuid, trigger->uuid, trigger->id,
                  op->id);
 
     order_actions(op, trigger, pe_order_optional);
     order_actions(trigger, confirm, pe_order_optional);
     return trigger;
 }
 
 static void
 pe_post_notify(resource_t * rsc, node_t * node, notify_data_t * n_data, pe_working_set_t * data_set)
 {
     action_t *notify = NULL;
 
     CRM_CHECK(rsc != NULL, return);
 
     if (n_data->post == NULL) {
         return;                 /* Nothing to do */
     }
 
     notify = pe_notify(rsc, node, n_data->post, n_data->post_done, n_data, data_set);
 
     if (notify != NULL) {
         notify->priority = INFINITY;
     }
 
     if (n_data->post_done) {
         GListPtr gIter = rsc->actions;
 
         for (; gIter != NULL; gIter = gIter->next) {
             action_t *mon = (action_t *) gIter->data;
             const char *interval_ms_s = g_hash_table_lookup(mon->meta,
                                                             XML_LRM_ATTR_INTERVAL_MS);
 
             if ((interval_ms_s == NULL) || safe_str_eq(interval_ms_s, "0")) {
                 pe_rsc_trace(rsc, "Skipping %s: interval", mon->uuid);
                 continue;
             } else if (safe_str_eq(mon->task, RSC_CANCEL)) {
                 pe_rsc_trace(rsc, "Skipping %s: cancel", mon->uuid);
                 continue;
             }
 
             order_actions(n_data->post_done, mon, pe_order_optional);
         }
     }
 }
 
 notify_data_t *
 create_notification_boundaries(resource_t * rsc, const char *action, action_t * start,
                                action_t * end, pe_working_set_t * data_set)
 {
     /* Create the pseudo ops that precede and follow the actual notifications */
 
     /*
      * Creates two sequences (conditional on start and end being supplied):
      *   pre_notify -> pre_notify_complete -> start, and
      *   end -> post_notify -> post_notify_complete
      *
      * 'start' and 'end' may be the same event or ${X} and ${X}ed as per clones
      */
     char *key = NULL;
     notify_data_t *n_data = NULL;
 
     if (is_not_set(rsc->flags, pe_rsc_notify)) {
         return NULL;
     }
 
     n_data = calloc(1, sizeof(notify_data_t));
     n_data->action = action;
-    n_data->keys = crm_str_table_new();
 
     if (start) {
         /* create pre-event notification wrappers */
         key = generate_notify_key(rsc->id, "pre", start->task);
         n_data->pre =
             custom_action(rsc, key, RSC_NOTIFY, NULL, is_set(start->flags, pe_action_optional),
                           TRUE, data_set);
 
         update_action_flags(n_data->pre, pe_action_pseudo, __FUNCTION__, __LINE__);
         update_action_flags(n_data->pre, pe_action_runnable, __FUNCTION__, __LINE__);
 
         add_hash_param(n_data->pre->meta, "notify_type", "pre");
         add_hash_param(n_data->pre->meta, "notify_operation", n_data->action);
 
         add_hash_param(n_data->pre->meta, "notify_key_type", "pre");
         add_hash_param(n_data->pre->meta, "notify_key_operation", start->task);
 
         /* create pre_notify_complete */
         key = generate_notify_key(rsc->id, "confirmed-pre", start->task);
         n_data->pre_done =
             custom_action(rsc, key, RSC_NOTIFIED, NULL, is_set(start->flags, pe_action_optional),
                           TRUE, data_set);
 
         update_action_flags(n_data->pre_done, pe_action_pseudo, __FUNCTION__, __LINE__);
         update_action_flags(n_data->pre_done, pe_action_runnable, __FUNCTION__, __LINE__);
 
         add_hash_param(n_data->pre_done->meta, "notify_type", "pre");
         add_hash_param(n_data->pre_done->meta, "notify_operation", n_data->action);
 
         add_hash_param(n_data->pre_done->meta, "notify_key_type", "confirmed-pre");
         add_hash_param(n_data->pre_done->meta, "notify_key_operation", start->task);
 
         order_actions(n_data->pre_done, start, pe_order_optional);
         order_actions(n_data->pre, n_data->pre_done, pe_order_optional);
     }
 
     if (end) {
         /* create post-event notification wrappers */
         key = generate_notify_key(rsc->id, "post", end->task);
         n_data->post =
             custom_action(rsc, key, RSC_NOTIFY, NULL, is_set(end->flags, pe_action_optional), TRUE,
                           data_set);
 
         n_data->post->priority = INFINITY;
         update_action_flags(n_data->post, pe_action_pseudo, __FUNCTION__, __LINE__);
         if (is_set(end->flags, pe_action_runnable)) {
             update_action_flags(n_data->post, pe_action_runnable, __FUNCTION__, __LINE__);
         } else {
             update_action_flags(n_data->post, pe_action_runnable | pe_action_clear, __FUNCTION__, __LINE__);
         }
 
         add_hash_param(n_data->post->meta, "notify_type", "post");
         add_hash_param(n_data->post->meta, "notify_operation", n_data->action);
 
         add_hash_param(n_data->post->meta, "notify_key_type", "post");
         add_hash_param(n_data->post->meta, "notify_key_operation", end->task);
 
         /* create post_notify_complete */
         key = generate_notify_key(rsc->id, "confirmed-post", end->task);
         n_data->post_done =
             custom_action(rsc, key, RSC_NOTIFIED, NULL, is_set(end->flags, pe_action_optional),
                           TRUE, data_set);
 
         n_data->post_done->priority = INFINITY;
         update_action_flags(n_data->post_done, pe_action_pseudo, __FUNCTION__, __LINE__);
         if (is_set(end->flags, pe_action_runnable)) {
             update_action_flags(n_data->post_done, pe_action_runnable, __FUNCTION__, __LINE__);
         } else {
             update_action_flags(n_data->post_done, pe_action_runnable | pe_action_clear, __FUNCTION__, __LINE__);
         }
 
         add_hash_param(n_data->post_done->meta, "notify_type", "post");
         add_hash_param(n_data->post_done->meta, "notify_operation", n_data->action);
 
         add_hash_param(n_data->post_done->meta, "notify_key_type", "confirmed-post");
         add_hash_param(n_data->post_done->meta, "notify_key_operation", end->task);
 
         order_actions(end, n_data->post, pe_order_implies_then);
         order_actions(n_data->post, n_data->post_done, pe_order_implies_then);
     }
 
     if (start && end) {
         order_actions(n_data->pre_done, n_data->post, pe_order_optional);
     }
     return n_data;
 }
 
 void
 collect_notification_data(resource_t * rsc, gboolean state, gboolean activity,
                           notify_data_t * n_data)
 {
 
     if(n_data->allowed_nodes == NULL) {
         n_data->allowed_nodes = rsc->allowed_nodes;
     }
 
     if (rsc->children) {
         GListPtr gIter = rsc->children;
 
         for (; gIter != NULL; gIter = gIter->next) {
             resource_t *child = (resource_t *) gIter->data;
 
             collect_notification_data(child, state, activity, n_data);
         }
         return;
     }
 
     if (state) {
         notify_entry_t *entry = NULL;
 
         entry = calloc(1, sizeof(notify_entry_t));
         entry->rsc = rsc;
         if (rsc->running_on) {
             /* we only take the first one */
             entry->node = rsc->running_on->data;
         }
 
         pe_rsc_trace(rsc, "%s state: %s", rsc->id, role2text(rsc->role));
 
         switch (rsc->role) {
             case RSC_ROLE_STOPPED:
                 n_data->inactive = g_list_prepend(n_data->inactive, entry);
                 break;
             case RSC_ROLE_STARTED:
                 n_data->active = g_list_prepend(n_data->active, entry);
                 break;
             case RSC_ROLE_SLAVE:
                 n_data->slave = g_list_prepend(n_data->slave, entry);
                 n_data->active = g_list_prepend(n_data->active,
                                                 dup_notify_entry(entry));
                 break;
             case RSC_ROLE_MASTER:
                 n_data->master = g_list_prepend(n_data->master, entry);
                 n_data->active = g_list_prepend(n_data->active,
                                                 dup_notify_entry(entry));
                 break;
             default:
                 crm_err("Unsupported notify role");
                 free(entry);
                 break;
         }
     }
 
     if (activity) {
         notify_entry_t *entry = NULL;
         enum action_tasks task;
 
         GListPtr gIter = rsc->actions;
 
         for (; gIter != NULL; gIter = gIter->next) {
             action_t *op = (action_t *) gIter->data;
 
             if (is_set(op->flags, pe_action_optional) == FALSE && op->node != NULL) {
                 task = text2task(op->task);
 
                 if(task == stop_rsc && op->node->details->unclean) {
                     // Create anyway (additional noise if node can't be fenced)
                 } else if(is_not_set(op->flags, pe_action_runnable)) {
                     continue;
                 }
 
                 entry = calloc(1, sizeof(notify_entry_t));
                 entry->node = op->node;
                 entry->rsc = rsc;
 
                 switch (task) {
                     case start_rsc:
                         n_data->start = g_list_prepend(n_data->start, entry);
                         break;
                     case stop_rsc:
                         n_data->stop = g_list_prepend(n_data->stop, entry);
                         break;
                     case action_promote:
                         n_data->promote = g_list_prepend(n_data->promote, entry);
                         break;
                     case action_demote:
                         n_data->demote = g_list_prepend(n_data->demote, entry);
                         break;
                     default:
                         free(entry);
                         break;
                 }
             }
         }
     }
 }
 
+#define add_notify_env(n_data, key, value) do {                         \
+         n_data->keys = pcmk_prepend_nvpair(n_data->keys, key, value);  \
+    } while (0)
+
+#define add_notify_env_free(n_data, key, value) do {                    \
+         n_data->keys = pcmk_prepend_nvpair(n_data->keys, key, value);  \
+         free(value); value = NULL;                                     \
+    } while (0)
+
 gboolean
 expand_notification_data(resource_t *rsc, notify_data_t * n_data, pe_working_set_t * data_set)
 {
     /* Expand the notification entries into a key=value hashtable
      * This hashtable is later used in action2xml()
      */
     gboolean required = FALSE;
     char *rsc_list = NULL;
     char *node_list = NULL;
     char *metal_list = NULL;
     const char *source = NULL;
     GListPtr nodes = NULL;
 
     if (n_data->stop) {
         n_data->stop = g_list_sort(n_data->stop, sort_notify_entries);
     }
     expand_list(n_data->stop, &rsc_list, &node_list);
     if (rsc_list != NULL && safe_str_neq(" ", rsc_list)) {
         if (safe_str_eq(n_data->action, RSC_STOP)) {
             required = TRUE;
         }
     }
-    g_hash_table_insert(n_data->keys, strdup("notify_stop_resource"), rsc_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_stop_uname"), node_list);
+    add_notify_env_free(n_data, "notify_stop_resource", rsc_list);
+    add_notify_env_free(n_data, "notify_stop_uname", node_list);
 
     if (n_data->start) {
         n_data->start = g_list_sort(n_data->start, sort_notify_entries);
         if (rsc_list && safe_str_eq(n_data->action, RSC_START)) {
             required = TRUE;
         }
     }
     expand_list(n_data->start, &rsc_list, &node_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_start_resource"), rsc_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_start_uname"), node_list);
+    add_notify_env_free(n_data, "notify_start_resource", rsc_list);
+    add_notify_env_free(n_data, "notify_start_uname", node_list);
 
     if (n_data->demote) {
         n_data->demote = g_list_sort(n_data->demote, sort_notify_entries);
         if (safe_str_eq(n_data->action, RSC_DEMOTE)) {
             required = TRUE;
         }
     }
 
     expand_list(n_data->demote, &rsc_list, &node_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_demote_resource"), rsc_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_demote_uname"), node_list);
+    add_notify_env_free(n_data, "notify_demote_resource", rsc_list);
+    add_notify_env_free(n_data, "notify_demote_uname", node_list);
 
     if (n_data->promote) {
         n_data->promote = g_list_sort(n_data->promote, sort_notify_entries);
         if (safe_str_eq(n_data->action, RSC_PROMOTE)) {
             required = TRUE;
         }
     }
     expand_list(n_data->promote, &rsc_list, &node_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_promote_resource"), rsc_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_promote_uname"), node_list);
+    add_notify_env_free(n_data, "notify_promote_resource", rsc_list);
+    add_notify_env_free(n_data, "notify_promote_uname", node_list);
 
     if (n_data->active) {
         n_data->active = g_list_sort(n_data->active, sort_notify_entries);
     }
     expand_list(n_data->active, &rsc_list, &node_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_active_resource"), rsc_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_active_uname"), node_list);
+    add_notify_env_free(n_data, "notify_active_resource", rsc_list);
+    add_notify_env_free(n_data, "notify_active_uname", node_list);
 
     if (n_data->slave) {
         n_data->slave = g_list_sort(n_data->slave, sort_notify_entries);
     }
     expand_list(n_data->slave, &rsc_list, &node_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_slave_resource"), rsc_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_slave_uname"), node_list);
+    add_notify_env_free(n_data, "notify_slave_resource", rsc_list);
+    add_notify_env_free(n_data, "notify_slave_uname", node_list);
 
     if (n_data->master) {
         n_data->master = g_list_sort(n_data->master, sort_notify_entries);
     }
     expand_list(n_data->master, &rsc_list, &node_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_master_resource"), rsc_list);
-    g_hash_table_insert(n_data->keys, strdup("notify_master_uname"), node_list);
+    add_notify_env_free(n_data, "notify_master_resource", rsc_list);
+    add_notify_env_free(n_data, "notify_master_uname", node_list);
 
     if (n_data->inactive) {
         n_data->inactive = g_list_sort(n_data->inactive, sort_notify_entries);
     }
     expand_list(n_data->inactive, &rsc_list, NULL);
-    g_hash_table_insert(n_data->keys, strdup("notify_inactive_resource"), rsc_list);
+    add_notify_env_free(n_data, "notify_inactive_resource", rsc_list);
 
     nodes = g_hash_table_get_values(n_data->allowed_nodes);
+    if (is_set(data_set->flags, pe_flag_stdout)) {
+        /* If printing to stdout, sort the node list, for consistent
+         * regression test output (while avoiding the performance hit
+         * for the live cluster).
+         */
+        nodes = g_list_sort(nodes, sort_node_uname);
+    }
     expand_node_list(nodes, &node_list, NULL);
-    g_hash_table_insert(n_data->keys, strdup("notify_available_uname"), node_list);
+    add_notify_env_free(n_data, "notify_available_uname", node_list);
     g_list_free(nodes);
 
     source = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET);
     if (safe_str_eq("host", source)) {
         expand_node_list(data_set->nodes, &node_list, &metal_list);
-        g_hash_table_insert(n_data->keys, strdup("notify_all_hosts"),
-                            metal_list);
+        add_notify_env_free(n_data, "notify_all_hosts", metal_list);
     } else {
         expand_node_list(data_set->nodes, &node_list, NULL);
     }
-    g_hash_table_insert(n_data->keys, strdup("notify_all_uname"), node_list);
+    add_notify_env_free(n_data, "notify_all_uname", node_list);
 
     if (required && n_data->pre) {
         update_action_flags(n_data->pre, pe_action_optional | pe_action_clear, __FUNCTION__, __LINE__);
         update_action_flags(n_data->pre_done, pe_action_optional | pe_action_clear, __FUNCTION__, __LINE__);
     }
 
     if (required && n_data->post) {
         update_action_flags(n_data->post, pe_action_optional | pe_action_clear, __FUNCTION__, __LINE__);
         update_action_flags(n_data->post_done, pe_action_optional | pe_action_clear, __FUNCTION__, __LINE__);
     }
     return required;
 }
 
 /*
  * \internal
  * \brief Find any remote connection start relevant to an action
  *
  * \param[in] action  Action to chek
  *
  * \return If action is behind a remote connection, connection's start
  */
 static pe_action_t *
 find_remote_start(pe_action_t *action)
 {
     if (action && action->node) {
         pe_resource_t *remote_rsc = action->node->details->remote_rsc;
 
         if (remote_rsc) {
             return find_first_action(remote_rsc->actions, NULL, RSC_START,
                                      NULL);
         }
     }
     return NULL;
 }
 
 void
 create_notifications(resource_t * rsc, notify_data_t * n_data, pe_working_set_t * data_set)
 {
     GListPtr gIter = NULL;
     action_t *stop = NULL;
     action_t *start = NULL;
     enum action_tasks task = text2task(n_data->action);
 
     if (rsc->children) {
         gIter = rsc->children;
         for (; gIter != NULL; gIter = gIter->next) {
             resource_t *child = (resource_t *) gIter->data;
 
             create_notifications(child, n_data, data_set);
         }
         return;
     }
 
     /* Copy notification details into standard ops */
 
     for (gIter = rsc->actions; gIter != NULL; gIter = gIter->next) {
         action_t *op = (action_t *) gIter->data;
 
         if (is_set(op->flags, pe_action_optional) == FALSE && op->node != NULL) {
             enum action_tasks t = text2task(op->task);
 
             switch (t) {
                 case start_rsc:
                 case stop_rsc:
                 case action_promote:
                 case action_demote:
-                    g_hash_table_foreach(n_data->keys, dup_attr, op->meta);
+                    add_notify_data_to_action_meta(n_data, op);
                     break;
                 default:
                     break;
             }
         }
     }
 
     switch (task) {
         case start_rsc:
             if(g_list_length(n_data->start) == 0) {
                 pe_rsc_trace(rsc, "Skipping empty notification for: %s.%s (%s->%s)",
                              n_data->action, rsc->id, role2text(rsc->role), role2text(rsc->next_role));
                 return;
             }
             break;
         case action_promote:
             if(g_list_length(n_data->promote) == 0) {
                 pe_rsc_trace(rsc, "Skipping empty notification for: %s.%s (%s->%s)",
                              n_data->action, rsc->id, role2text(rsc->role), role2text(rsc->next_role));
                 return;
             }
             break;
         case action_demote:
             if(g_list_length(n_data->demote) == 0) {
                 pe_rsc_trace(rsc, "Skipping empty notification for: %s.%s (%s->%s)",
                              n_data->action, rsc->id, role2text(rsc->role), role2text(rsc->next_role));
                 return;
             }
             break;
         default:
             /* We cannot do the same for stop_rsc/n_data->stop at it
              * might be implied by fencing
              */
             break;
     }
 
     pe_rsc_trace(rsc, "Creating notifications for: %s.%s (%s->%s)",
                  n_data->action, rsc->id, role2text(rsc->role), role2text(rsc->next_role));
 
     stop = find_first_action(rsc->actions, NULL, RSC_STOP, NULL);
     start = find_first_action(rsc->actions, NULL, RSC_START, NULL);
 
     /* stop / demote */
     if (rsc->role != RSC_ROLE_STOPPED) {
         if (task == stop_rsc || task == action_demote) {
             gIter = rsc->running_on;
             for (; gIter != NULL; gIter = gIter->next) {
                 node_t *current_node = (node_t *) gIter->data;
 
                 /* if this stop action is a pseudo action as a result of the current
                  * node being fenced, this stop action is implied by the fencing 
                  * action. There's no reason to send the fenced node a stop notification */ 
                 if (stop &&
                     is_set(stop->flags, pe_action_pseudo) &&
                     (current_node->details->unclean || current_node->details->remote_requires_reset) ) {
 
                     continue;
                 }
 
                 pe_notify(rsc, current_node, n_data->pre, n_data->pre_done, n_data, data_set);
                 if (task == action_demote || stop == NULL
                     || is_set(stop->flags, pe_action_optional)) {
                     pe_post_notify(rsc, current_node, n_data, data_set);
                 }
             }
         }
     }
 
     /* start / promote */
     if (rsc->next_role != RSC_ROLE_STOPPED) {
         if (rsc->allocated_to == NULL) {
             pe_proc_err("Next role '%s' but %s is not allocated", role2text(rsc->next_role),
                         rsc->id);
 
         } else if (task == start_rsc || task == action_promote) {
 
             if (start) {
                 pe_action_t *remote_start = find_remote_start(start);
 
                 if (remote_start
                     && is_not_set(remote_start->flags, pe_action_runnable)) {
                     /* Start and promote actions for a clone instance behind
                      * a Pacemaker Remote connection happen after the
                      * connection starts. If the connection start is blocked, do
                      * not schedule notifications for these actions.
                      */
                     return;
                 }
             }
             if (task != start_rsc || start == NULL || is_set(start->flags, pe_action_optional)) {
                 pe_notify(rsc, rsc->allocated_to, n_data->pre, n_data->pre_done, n_data, data_set);
             }
             pe_post_notify(rsc, rsc->allocated_to, n_data, data_set);
         }
     }
 }
 
 void
 free_notification_data(notify_data_t * n_data)
 {
     if (n_data == NULL) {
         return;
     }
 
     g_list_free_full(n_data->stop, free);
     g_list_free_full(n_data->start, free);
     g_list_free_full(n_data->demote, free);
     g_list_free_full(n_data->promote, free);
     g_list_free_full(n_data->master, free);
     g_list_free_full(n_data->slave, free);
     g_list_free_full(n_data->active, free);
     g_list_free_full(n_data->inactive, free);
-    g_hash_table_destroy(n_data->keys);
+    pcmk_free_nvpairs(n_data->keys);
     free(n_data);
 }
 
 void
 create_secondary_notification(pe_action_t *action, resource_t *rsc,
                               pe_action_t *stonith_op,
                               pe_working_set_t *data_set)
 {
     notify_data_t *n_data;
 
     crm_info("Creating secondary notification for %s", action->uuid);
     n_data = create_notification_boundaries(rsc, RSC_STOP, NULL, stonith_op,
                                             data_set);
     collect_notification_data(rsc, TRUE, FALSE, n_data);
-    g_hash_table_insert(n_data->keys, strdup("notify_stop_resource"),
-                        strdup(rsc->id));
-    g_hash_table_insert(n_data->keys, strdup("notify_stop_uname"),
-                        strdup(action->node->details->uname));
+    add_notify_env(n_data, "notify_stop_resource", rsc->id);
+    add_notify_env(n_data, "notify_stop_uname", action->node->details->uname);
     create_notifications(uber_parent(rsc), n_data, data_set);
     free_notification_data(n_data);
 }
diff --git a/include/crm/pengine/internal.h b/include/crm/pengine/internal.h
index 1913d90e00..e9b7c55d91 100644
--- a/include/crm/pengine/internal.h
+++ b/include/crm/pengine/internal.h
@@ -1,353 +1,353 @@
 /*
- * Copyright 2004-2018 Andrew Beekhof <andrew@beekhof.net>
+ * Copyright 2004-2019 Andrew Beekhof <andrew@beekhof.net>
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PE_INTERNAL__H
 #  define PE_INTERNAL__H
 #  include <string.h>
 #  include <crm/pengine/status.h>
 #  include <crm/pengine/remote.h>
 
 #  define pe_rsc_info(rsc, fmt, args...)  crm_log_tag(LOG_INFO,  rsc ? rsc->id : "<NULL>", fmt, ##args)
 #  define pe_rsc_debug(rsc, fmt, args...) crm_log_tag(LOG_DEBUG, rsc ? rsc->id : "<NULL>", fmt, ##args)
 #  define pe_rsc_trace(rsc, fmt, args...) crm_log_tag(LOG_TRACE, rsc ? rsc->id : "<NULL>", fmt, ##args)
 
 #  define pe_err(fmt...) { was_processing_error = TRUE; crm_config_error = TRUE; crm_err(fmt); }
 #  define pe_warn(fmt...) { was_processing_warning = TRUE; crm_config_warning = TRUE; crm_warn(fmt); }
 #  define pe_proc_err(fmt...) { was_processing_error = TRUE; crm_err(fmt); }
 #  define pe_proc_warn(fmt...) { was_processing_warning = TRUE; crm_warn(fmt); }
 #  define pe_set_action_bit(action, bit) action->flags = crm_set_bit(__FUNCTION__, __LINE__, action->uuid, action->flags, bit)
 #  define pe_clear_action_bit(action, bit) action->flags = crm_clear_bit(__FUNCTION__, __LINE__, action->uuid, action->flags, bit)
 
 typedef struct pe__location_constraint_s {
     char *id;                           // Constraint XML ID
     pe_resource_t *rsc_lh;              // Resource being located
     enum rsc_role_e role_filter;        // Role to locate
     enum pe_discover_e discover_mode;   // Resource discovery
     GListPtr node_list_rh;              // List of pe_node_t*
 } pe__location_t;
 
 typedef struct pe__order_constraint_s {
     int id;
     enum pe_ordering type;
 
     void *lh_opaque;
     resource_t *lh_rsc;
     action_t *lh_action;
     char *lh_action_task;
 
     void *rh_opaque;
     resource_t *rh_rsc;
     action_t *rh_action;
     char *rh_action_task;
 } pe__ordering_t;
 
 typedef struct notify_data_s {
-    GHashTable *keys;
+    GList *keys;                // Environment variable name/value pairs
 
     const char *action;
 
     action_t *pre;
     action_t *post;
     action_t *pre_done;
     action_t *post_done;
 
     GListPtr active;            /* notify_entry_t*  */
     GListPtr inactive;          /* notify_entry_t*  */
     GListPtr start;             /* notify_entry_t*  */
     GListPtr stop;              /* notify_entry_t*  */
     GListPtr demote;            /* notify_entry_t*  */
     GListPtr promote;           /* notify_entry_t*  */
     GListPtr master;            /* notify_entry_t*  */
     GListPtr slave;             /* notify_entry_t*  */
     GHashTable *allowed_nodes;
 
 } notify_data_t;
 
 bool pe_can_fence(pe_working_set_t *data_set, node_t *node);
 
 int merge_weights(int w1, int w2);
 void add_hash_param(GHashTable * hash, const char *name, const char *value);
 
 char *native_parameter(resource_t * rsc, node_t * node, gboolean create, const char *name,
                        pe_working_set_t * data_set);
 pe_node_t *native_location(const pe_resource_t *rsc, GList **list, int current);
 
 void pe_metadata(void);
 void verify_pe_options(GHashTable * options);
 
 void common_update_score(resource_t * rsc, const char *id, int score);
 void native_add_running(resource_t * rsc, node_t * node, pe_working_set_t * data_set);
 
 gboolean native_unpack(resource_t * rsc, pe_working_set_t * data_set);
 gboolean group_unpack(resource_t * rsc, pe_working_set_t * data_set);
 gboolean clone_unpack(resource_t * rsc, pe_working_set_t * data_set);
 gboolean container_unpack(resource_t * rsc, pe_working_set_t * data_set);
 
 resource_t *native_find_rsc(resource_t *rsc, const char *id, const node_t *node,
                             int flags);
 
 gboolean native_active(resource_t * rsc, gboolean all);
 gboolean group_active(resource_t * rsc, gboolean all);
 gboolean clone_active(resource_t * rsc, gboolean all);
 gboolean container_active(resource_t * rsc, gboolean all);
 
 void native_print(resource_t * rsc, const char *pre_text, long options, void *print_data);
 void group_print(resource_t * rsc, const char *pre_text, long options, void *print_data);
 void clone_print(resource_t * rsc, const char *pre_text, long options, void *print_data);
 void container_print(resource_t * rsc, const char *pre_text, long options, void *print_data);
 
 void native_free(resource_t * rsc);
 void group_free(resource_t * rsc);
 void clone_free(resource_t * rsc);
 void container_free(resource_t * rsc);
 
 enum rsc_role_e native_resource_state(const resource_t * rsc, gboolean current);
 enum rsc_role_e group_resource_state(const resource_t * rsc, gboolean current);
 enum rsc_role_e clone_resource_state(const resource_t * rsc, gboolean current);
 enum rsc_role_e container_resource_state(const resource_t * rsc, gboolean current);
 
 gboolean common_unpack(xmlNode * xml_obj, resource_t ** rsc, resource_t * parent,
                        pe_working_set_t * data_set);
 void common_free(resource_t * rsc);
 
 extern node_t *node_copy(const node_t *this_node);
 extern time_t get_effective_time(pe_working_set_t * data_set);
 
 /* Failure handling utilities (from failcounts.c) */
 
 // bit flags for fail count handling options
 enum pe_fc_flags_e {
     pe_fc_default   = 0x00,
     pe_fc_effective = 0x01, // don't count expired failures
     pe_fc_fillers   = 0x02, // if container, include filler failures in count
 };
 
 int pe_get_failcount(node_t *node, resource_t *rsc, time_t *last_failure,
                      uint32_t flags, xmlNode *xml_op,
                      pe_working_set_t *data_set);
 
 pe_action_t *pe__clear_failcount(pe_resource_t *rsc, pe_node_t *node,
                                  const char *reason,
                                  pe_working_set_t *data_set);
 
 /* Functions for finding/counting a resource's active nodes */
 
 pe_node_t *pe__find_active_on(const pe_resource_t *rsc,
                               unsigned int *count_all,
                               unsigned int *count_clean);
 pe_node_t *pe__find_active_requires(const pe_resource_t *rsc,
                                     unsigned int *count);
 
 static inline pe_node_t *
 pe__current_node(const pe_resource_t *rsc)
 {
     return pe__find_active_on(rsc, NULL, NULL);
 }
 
 
 /* Binary like operators for lists of nodes */
 extern void node_list_exclude(GHashTable * list, GListPtr list2, gboolean merge_scores);
 extern GListPtr node_list_dup(GListPtr list, gboolean reset, gboolean filter);
 
 extern GHashTable *node_hash_from_list(GListPtr list);
 static inline gpointer
 pe_hash_table_lookup(GHashTable * hash, gconstpointer key)
 {
     if (hash) {
         return g_hash_table_lookup(hash, key);
     }
     return NULL;
 }
 
 extern action_t *get_pseudo_op(const char *name, pe_working_set_t * data_set);
 extern gboolean order_actions(action_t * lh_action, action_t * rh_action, enum pe_ordering order);
 
 GHashTable *node_hash_dup(GHashTable * hash);
 
 /* Printing functions for debug */
 extern void print_node(const char *pre_text, node_t * node, gboolean details);
 
 extern void print_resource(int log_level, const char *pre_text, resource_t * rsc, gboolean details);
 
 extern void dump_node_scores_worker(int level, const char *file, const char *function, int line,
                                     resource_t * rsc, const char *comment, GHashTable * nodes);
 
 extern void dump_node_capacity(int level, const char *comment, node_t * node);
 extern void dump_rsc_utilization(int level, const char *comment, resource_t * rsc, node_t * node);
 
 #  define dump_node_scores(level, rsc, text, nodes) do {		\
         dump_node_scores_worker(level, __FILE__, __FUNCTION__, __LINE__, rsc, text, nodes); \
     } while(0)
 
 /* Sorting functions */
 extern gint sort_rsc_priority(gconstpointer a, gconstpointer b);
 extern gint sort_rsc_index(gconstpointer a, gconstpointer b);
 
 extern xmlNode *find_rsc_op_entry(resource_t * rsc, const char *key);
 
 extern action_t *custom_action(resource_t * rsc, char *key, const char *task, node_t * on_node,
                                gboolean optional, gboolean foo, pe_working_set_t * data_set);
 
 #  define delete_key(rsc) generate_op_key(rsc->id, CRMD_ACTION_DELETE, 0)
 #  define delete_action(rsc, node, optional) custom_action(		\
 		rsc, delete_key(rsc), CRMD_ACTION_DELETE, node,		\
 		optional, TRUE, data_set);
 
 #  define stopped_key(rsc) generate_op_key(rsc->id, CRMD_ACTION_STOPPED, 0)
 #  define stopped_action(rsc, node, optional) custom_action(		\
 		rsc, stopped_key(rsc), CRMD_ACTION_STOPPED, node,	\
 		optional, TRUE, data_set);
 
 #  define stop_key(rsc) generate_op_key(rsc->id, CRMD_ACTION_STOP, 0)
 #  define stop_action(rsc, node, optional) custom_action(			\
 		rsc, stop_key(rsc), CRMD_ACTION_STOP, node,		\
 		optional, TRUE, data_set);
 
 #  define reload_key(rsc) generate_op_key(rsc->id, CRMD_ACTION_RELOAD, 0)
 #  define start_key(rsc) generate_op_key(rsc->id, CRMD_ACTION_START, 0)
 #  define start_action(rsc, node, optional) custom_action(		\
 		rsc, start_key(rsc), CRMD_ACTION_START, node,		\
 		optional, TRUE, data_set)
 
 #  define started_key(rsc) generate_op_key(rsc->id, CRMD_ACTION_STARTED, 0)
 #  define started_action(rsc, node, optional) custom_action(		\
 		rsc, started_key(rsc), CRMD_ACTION_STARTED, node,	\
 		optional, TRUE, data_set)
 
 #  define promote_key(rsc) generate_op_key(rsc->id, CRMD_ACTION_PROMOTE, 0)
 #  define promote_action(rsc, node, optional) custom_action(		\
 		rsc, promote_key(rsc), CRMD_ACTION_PROMOTE, node,	\
 		optional, TRUE, data_set)
 
 #  define promoted_key(rsc) generate_op_key(rsc->id, CRMD_ACTION_PROMOTED, 0)
 #  define promoted_action(rsc, node, optional) custom_action(		\
 		rsc, promoted_key(rsc), CRMD_ACTION_PROMOTED, node,	\
 		optional, TRUE, data_set)
 
 #  define demote_key(rsc) generate_op_key(rsc->id, CRMD_ACTION_DEMOTE, 0)
 #  define demote_action(rsc, node, optional) custom_action(		\
 		rsc, demote_key(rsc), CRMD_ACTION_DEMOTE, node,		\
 		optional, TRUE, data_set)
 
 #  define demoted_key(rsc) generate_op_key(rsc->id, CRMD_ACTION_DEMOTED, 0)
 #  define demoted_action(rsc, node, optional) custom_action(		\
 		rsc, demoted_key(rsc), CRMD_ACTION_DEMOTED, node,	\
 		optional, TRUE, data_set)
 
 extern int pe_get_configured_timeout(resource_t *rsc, const char *action,
                                      pe_working_set_t *data_set);
 
 extern action_t *find_first_action(GListPtr input, const char *uuid, const char *task,
                                    node_t * on_node);
 extern enum action_tasks get_complex_task(resource_t * rsc, const char *name,
                                           gboolean allow_non_atomic);
 
 extern GListPtr find_actions(GListPtr input, const char *key, const node_t *on_node);
 extern GListPtr find_actions_exact(GListPtr input, const char *key, node_t * on_node);
 extern GListPtr find_recurring_actions(GListPtr input, node_t * not_on_node);
 
 extern void pe_free_action(action_t * action);
 
 extern void resource_location(resource_t * rsc, node_t * node, int score, const char *tag,
                               pe_working_set_t * data_set);
 
 extern gint sort_op_by_callid(gconstpointer a, gconstpointer b);
 extern gboolean get_target_role(resource_t * rsc, enum rsc_role_e *role);
 
 extern resource_t *find_clone_instance(resource_t * rsc, const char *sub_id,
                                        pe_working_set_t * data_set);
 
 extern void destroy_ticket(gpointer data);
 extern ticket_t *ticket_new(const char *ticket_id, pe_working_set_t * data_set);
 
 // Resources for manipulating resource names
 const char *pe_base_name_end(const char *id);
 char *clone_strip(const char *last_rsc_id);
 char *clone_zero(const char *last_rsc_id);
 
 static inline bool
 pe_base_name_eq(resource_t *rsc, const char *id)
 {
     if (id && rsc && rsc->id) {
         // Number of characters in rsc->id before any clone suffix
         size_t base_len = pe_base_name_end(rsc->id) - rsc->id + 1;
 
         return (strlen(id) == base_len) && !strncmp(id, rsc->id, base_len);
     }
     return FALSE;
 }
 
 int get_target_rc(xmlNode * xml_op);
 
 gint sort_node_uname(gconstpointer a, gconstpointer b);
 bool is_set_recursive(resource_t * rsc, long long flag, bool any);
 
 enum rsc_digest_cmp_val {
     /*! Digests are the same */
     RSC_DIGEST_MATCH = 0,
     /*! Params that require a restart changed */
     RSC_DIGEST_RESTART,
     /*! Some parameter changed.  */
     RSC_DIGEST_ALL,
     /*! rsc op didn't have a digest associated with it, so
      *  it is unknown if parameters changed or not. */
     RSC_DIGEST_UNKNOWN,
 };
 
 typedef struct op_digest_cache_s {
     enum rsc_digest_cmp_val rc;
     xmlNode *params_all;
     xmlNode *params_secure;
     xmlNode *params_restart;
     char *digest_all_calc;
     char *digest_secure_calc;
     char *digest_restart_calc;
 } op_digest_cache_t;
 
 op_digest_cache_t *rsc_action_digest_cmp(resource_t * rsc, xmlNode * xml_op, node_t * node,
                                          pe_working_set_t * data_set);
 
 action_t *pe_fence_op(node_t * node, const char *op, bool optional, const char *reason, pe_working_set_t * data_set);
 void trigger_unfencing(
     resource_t * rsc, node_t *node, const char *reason, action_t *dependency, pe_working_set_t * data_set);
 
 void pe_action_set_reason(pe_action_t *action, const char *reason, bool overwrite);
 void pe_action_set_flag_reason(const char *function, long line, pe_action_t *action, pe_action_t *reason, const char *text, enum pe_action_flags flags, bool overwrite);
 
 #define pe_action_required(action, reason, text) pe_action_set_flag_reason(__FUNCTION__, __LINE__, action, reason, text, pe_action_optional, FALSE)
 #define pe_action_implies(action, reason, flag) pe_action_set_flag_reason(__FUNCTION__, __LINE__, action, reason, NULL, flag, FALSE)
 
 void set_bit_recursive(resource_t * rsc, unsigned long long flag);
 void clear_bit_recursive(resource_t * rsc, unsigned long long flag);
 
 gboolean add_tag_ref(GHashTable * tags, const char * tag_name,  const char * obj_ref);
 
 void print_rscs_brief(GListPtr rsc_list, const char * pre_text, long options,
                       void * print_data, gboolean print_all);
 void pe_fence_node(pe_working_set_t * data_set, node_t * node, const char *reason);
 
 node_t *pe_create_node(const char *id, const char *uname, const char *type,
                        const char *score, pe_working_set_t * data_set);
 bool remote_id_conflict(const char *remote_name, pe_working_set_t *data);
 void common_print(resource_t * rsc, const char *pre_text, const char *name, node_t *node, long options, void *print_data);
 resource_t *find_container_child(const resource_t *bundle, const node_t *node);
 bool container_fix_remote_addr(resource_t *rsc);
 const char *container_fix_remote_addr_in(resource_t *rsc, xmlNode *xml, const char *field);
 const char *pe_node_attribute_calculated(const pe_node_t *node,
                                          const char *name,
                                          const resource_t *rsc);
 const char *pe_node_attribute_raw(pe_node_t *node, const char *name);
 bool pe__is_universal_clone(pe_resource_t *rsc,
                             pe_working_set_t *data_set);
 void pe__add_param_check(xmlNode *rsc_op, pe_resource_t *rsc, pe_node_t *node,
                          enum pe_check_parameters, pe_working_set_t *data_set);
 void pe__foreach_param_check(pe_working_set_t *data_set,
                              void (*cb)(pe_resource_t*, pe_node_t*, xmlNode*,
                                         enum pe_check_parameters,
                                         pe_working_set_t*));
 void pe__free_param_checks(pe_working_set_t *data_set);
 #endif