diff --git a/include/crm/common/xml.h b/include/crm/common/xml.h
index 1ea46da0fc..a9fef3e4b2 100644
--- a/include/crm/common/xml.h
+++ b/include/crm/common/xml.h
@@ -1,296 +1,298 @@
 /*
  * Copyright (C) 2004 Andrew Beekhof <andrew@beekhof.net>
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public
  * License as published by the Free Software Foundation; either
  * version 2 of the License, or (at your option) any later version.
  *
  * This software is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  * General Public License for more details.
  *
  * You should have received a copy of the GNU General Public
  * License along with this library; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  */
 #ifndef CRM_COMMON_XML__H
 #  define CRM_COMMON_XML__H
 
 /**
  * \file
  * \brief Wrappers for and extensions to libxml2
  * \ingroup core
  */
 
 #  include <stdio.h>
 #  include <sys/types.h>
 #  include <unistd.h>
 
 #  include <stdlib.h>
 #  include <errno.h>
 #  include <fcntl.h>
 
 #  include <crm/crm.h>
 
 #  include <libxml/tree.h>
 #  include <libxml/xpath.h>
 
 /* Compression costs a LOT, don't do it unless we're hitting message limits
  *
  * For now, use 256k as the lower size, which means we can have 4 big data fields
  *  before we hit heartbeat's message limit
  *
  * The previous limit was 10k, compressing 184 of 1071 messages accounted for 23%
  *  of the total CPU used by the cib
  */
 #  define CRM_BZ2_BLOCKS		4
 #  define CRM_BZ2_WORK		20
 #  define CRM_BZ2_THRESHOLD	128 * 1024
 
 #  define XML_PARANOIA_CHECKS 0
 
 gboolean add_message_xml(xmlNode * msg, const char *field, xmlNode * xml);
 xmlNode *get_message_xml(xmlNode * msg, const char *field);
 GHashTable *xml2list(xmlNode * parent);
 
 void hash2nvpair(gpointer key, gpointer value, gpointer user_data);
 void hash2field(gpointer key, gpointer value, gpointer user_data);
 void hash2metafield(gpointer key, gpointer value, gpointer user_data);
 void hash2smartfield(gpointer key, gpointer value, gpointer user_data);
 
 xmlDoc *getDocPtr(xmlNode * node);
 
 /*
  * Replacement function for xmlCopyPropList which at the very least,
  * doesnt work the way *I* would expect it to.
  *
  * Copy all the attributes/properties from src into target.
  *
  * Not recursive, does not return anything.
  *
  */
 void copy_in_properties(xmlNode * target, xmlNode * src);
 void expand_plus_plus(xmlNode * target, const char *name, const char *value);
 void fix_plus_plus_recursive(xmlNode * target);
 
 /*
  * Create a node named "name" as a child of "parent"
  * If parent is NULL, creates an unconnected node.
  *
  * Returns the created node
  *
  */
 xmlNode *create_xml_node(xmlNode * parent, const char *name);
 
 /*
  * Make a copy of name and value and use the copied memory to create
  * an attribute for node.
  *
  * If node, name or value are NULL, nothing is done.
  *
  * If name or value are an empty string, nothing is done.
  *
  * Returns FALSE on failure and TRUE on success.
  *
  */
 const char *crm_xml_add(xmlNode * node, const char *name, const char *value);
 
 const char *crm_xml_replace(xmlNode * node, const char *name, const char *value);
 
 const char *crm_xml_add_int(xmlNode * node, const char *name, int value);
 
 /*
  * Unlink the node and set its doc pointer to NULL so free_xml()
  * will act appropriately
  */
 void unlink_xml_node(xmlNode * node);
 
 /*
  *
  */
 void purge_diff_markers(xmlNode * a_node);
 
 /*
  * Returns a deep copy of src_node
  *
  */
 xmlNode *copy_xml(xmlNode * src_node);
 
 /*
  * Add a copy of xml_node to new_parent
  */
 xmlNode *add_node_copy(xmlNode * new_parent, xmlNode * xml_node);
 
 int add_node_nocopy(xmlNode * parent, const char *name, xmlNode * child);
 
 /*
  * XML I/O Functions
  *
  * Whitespace between tags is discarded.
  */
 xmlNode *filename2xml(const char *filename);
 
 xmlNode *stdin2xml(void);
 
 xmlNode *string2xml(const char *input);
 
 int write_xml_fd(xmlNode * xml_node, const char *filename, int fd, gboolean compress);
 int write_xml_file(xmlNode * xml_node, const char *filename, gboolean compress);
 
 char *dump_xml_formatted(xmlNode * msg);
 
 char *dump_xml_unformatted(xmlNode * msg);
 
 /*
  * Diff related Functions
  */
 xmlNode *diff_xml_object(xmlNode * left, xmlNode * right, gboolean suppress);
 
 xmlNode *subtract_xml_object(xmlNode * parent, xmlNode * left, xmlNode * right,
                              gboolean full, gboolean * changed, const char *marker);
 
 gboolean can_prune_leaf(xmlNode * xml_node);
 
 void print_xml_diff(FILE * where, xmlNode * diff);
 
 gboolean apply_xml_diff(xmlNode * old, xmlNode * diff, xmlNode ** new);
 
 /*
  * Searching & Modifying
  */
 xmlNode *find_xml_node(xmlNode * cib, const char *node_path, gboolean must_find);
 
 xmlNode *find_entity(xmlNode * parent, const char *node_name, const char *id);
 
 void xml_remove_prop(xmlNode * obj, const char *name);
 
 gboolean replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update,
                            gboolean delete_only);
 
 gboolean update_xml_child(xmlNode * child, xmlNode * to_update);
 
 int find_xml_children(xmlNode ** children, xmlNode * root,
                       const char *tag, const char *field, const char *value,
                       gboolean search_matches);
 
 int crm_element_value_int(xmlNode * data, const char *name, int *dest);
 char *crm_element_value_copy(xmlNode * data, const char *name);
 int crm_element_value_const_int(const xmlNode * data, const char *name, int *dest);
 const char *crm_element_value_const(const xmlNode * data, const char *name);
 xmlNode *get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level);
 xmlNode *get_xpath_object_relative(const char *xpath, xmlNode * xml_obj, int error_level);
 
 static inline const char *
 crm_element_name(xmlNode *xml)
 {
     return xml? (const char *)(xml->name) : NULL;
 }
 
 const char *crm_element_value(xmlNode * data, const char *name);
 
 void xml_validate(const xmlNode * root);
 
 gboolean xml_has_children(const xmlNode * root);
 
 char *calculate_on_disk_digest(xmlNode * local_cib);
 char *calculate_operation_digest(xmlNode * local_cib, const char *version);
 char *calculate_xml_versioned_digest(xmlNode * input, gboolean sort, gboolean do_filter,
                                      const char *version);
 
 gboolean validate_xml(xmlNode * xml_blob, const char *validation, gboolean to_logs);
 gboolean validate_xml_verbose(xmlNode * xml_blob);
 int update_validation(xmlNode ** xml_blob, int *best, int max, gboolean transform, gboolean to_logs);
 int get_schema_version(const char *name);
 const char *get_schema_name(int version);
 
 void crm_xml_init(void);
 void crm_xml_cleanup(void);
 
 static inline xmlNode *
 __xml_first_child(xmlNode * parent)
 {
     xmlNode *child = NULL;
 
     if (parent) {
         child = parent->children;
         while (child && child->type == XML_TEXT_NODE) {
             child = child->next;
         }
     }
     return child;
 }
 
 static inline xmlNode *
 __xml_next(xmlNode * child)
 {
     if (child) {
         child = child->next;
         while (child && child->type == XML_TEXT_NODE) {
             child = child->next;
         }
     }
     return child;
 }
 
 static inline xmlNode *
 __xml_next_element(xmlNode * child)
 {
     if (child) {
         child = child->next;
         while (child && child->type != XML_ELEMENT_NODE) {
             child = child->next;
         }
     }
     return child;
 }
 
 void free_xml(xmlNode * child);
 
 xmlNode *first_named_child(xmlNode * parent, const char *name);
 
 xmlNode *sorted_xml(xmlNode * input, xmlNode * parent, gboolean recursive);
 xmlXPathObjectPtr xpath_search(xmlNode * xml_top, const char *path);
+void crm_foreach_xpath_result(xmlNode *xml, const char *xpath,
+                              void (*helper)(xmlNode*, void*), void *user_data);
 gboolean cli_config_update(xmlNode ** xml, int *best_version, gboolean to_logs);
 xmlNode *expand_idref(xmlNode * input, xmlNode * top);
 
 void freeXpathObject(xmlXPathObjectPtr xpathObj);
 xmlNode *getXpathResult(xmlXPathObjectPtr xpathObj, int index);
 void dedupXpathResults(xmlXPathObjectPtr xpathObj);
 
 static inline int numXpathResults(xmlXPathObjectPtr xpathObj)
 {
     if(xpathObj == NULL || xpathObj->nodesetval == NULL) {
         return 0;
     }
     return xpathObj->nodesetval->nodeNr;
 }
 
 const char *xml_latest_schema(void);
 
 bool xml_acl_enabled(xmlNode *xml);
 void xml_acl_disable(xmlNode *xml);
 bool xml_acl_denied(xmlNode *xml); /* Part or all of a change was rejected */
 bool xml_acl_filtered_copy(const char *user, xmlNode* acl_source, xmlNode *xml, xmlNode ** result);
 
 bool xml_tracking_changes(xmlNode * xml);
 bool xml_document_dirty(xmlNode *xml);
 void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls);
 void xml_calculate_changes(xmlNode * old, xmlNode * new); /* For comparing two documents after the fact */
 void xml_accept_changes(xmlNode * xml);
 void xml_log_changes(uint8_t level, const char *function, xmlNode *xml);
 void xml_log_patchset(uint8_t level, const char *function, xmlNode *xml);
 bool xml_patch_versions(xmlNode *patchset, int add[3], int del[3]);
 
 xmlNode *xml_create_patchset(
     int format, xmlNode *source, xmlNode *target, bool *config, bool manage_version);
 int xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version);
 
 void patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, bool with_digest);
 
 void save_xml_to_file(xmlNode * xml, const char *desc, const char *filename);
 char *xml_get_path(xmlNode *xml);
 
 char * crm_xml_escape(const char *text);
 #endif
diff --git a/lib/common/xpath.c b/lib/common/xpath.c
index fc84edda95..9a058192b2 100644
--- a/lib/common/xpath.c
+++ b/lib/common/xpath.c
@@ -1,239 +1,270 @@
 /*
  * Copyright (C) 2004 Andrew Beekhof <andrew@beekhof.net>
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
  * License as published by the Free Software Foundation; either
  * version 2.1 of the License, or (at your option) any later version.
  *
  * This library is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  * Lesser General Public License for more details.
  *
  * You should have received a copy of the GNU Lesser General Public
  * License along with this library; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  */
 
 #include <crm_internal.h>
 #include <stdio.h>
 #include <string.h>
 
 /*
  * From xpath2.c
  *
  * All the elements returned by an XPath query are pointers to
  * elements from the tree *except* namespace nodes where the XPath
  * semantic is different from the implementation in libxml2 tree.
  * As a result when a returned node set is freed when
  * xmlXPathFreeObject() is called, that routine must check the
  * element type. But node from the returned set may have been removed
  * by xmlNodeSetContent() resulting in access to freed data.
  *
  * This can be exercised by running
  *       valgrind xpath2 test3.xml '//discarded' discarded
  *
  * There is 2 ways around it:
  *   - make a copy of the pointers to the nodes from the result set
  *     then call xmlXPathFreeObject() and then modify the nodes
  * or
  * - remove the references from the node set, if they are not
        namespace nodes, before calling xmlXPathFreeObject().
  */
 void
 freeXpathObject(xmlXPathObjectPtr xpathObj)
 {
     int lpc, max = numXpathResults(xpathObj);
 
     if (xpathObj == NULL) {
         return;
     }
 
     for (lpc = 0; lpc < max; lpc++) {
         if (xpathObj->nodesetval->nodeTab[lpc] && xpathObj->nodesetval->nodeTab[lpc]->type != XML_NAMESPACE_DECL) {
             xpathObj->nodesetval->nodeTab[lpc] = NULL;
         }
     }
 
     /* _Now_ it's safe to free it */
     xmlXPathFreeObject(xpathObj);
 }
 
 xmlNode *
 getXpathResult(xmlXPathObjectPtr xpathObj, int index)
 {
     xmlNode *match = NULL;
     int max = numXpathResults(xpathObj);
 
     CRM_CHECK(index >= 0, return NULL);
     CRM_CHECK(xpathObj != NULL, return NULL);
 
     if (index >= max) {
         crm_err("Requested index %d of only %d items", index, max);
         return NULL;
 
     } else if(xpathObj->nodesetval->nodeTab[index] == NULL) {
         /* Previously requested */
         return NULL;
     }
 
     match = xpathObj->nodesetval->nodeTab[index];
     CRM_CHECK(match != NULL, return NULL);
 
     if (xpathObj->nodesetval->nodeTab[index]->type != XML_NAMESPACE_DECL) {
         /* See the comment for freeXpathObject() */
         xpathObj->nodesetval->nodeTab[index] = NULL;
     }
 
     if (match->type == XML_DOCUMENT_NODE) {
         /* Will happen if section = '/' */
         match = match->children;
 
     } else if (match->type != XML_ELEMENT_NODE
                && match->parent && match->parent->type == XML_ELEMENT_NODE) {
         /* Return the parent instead */
         match = match->parent;
 
     } else if (match->type != XML_ELEMENT_NODE) {
         /* We only support searching nodes */
         crm_err("We only support %d not %d", XML_ELEMENT_NODE, match->type);
         match = NULL;
     }
     return match;
 }
 
 void
 dedupXpathResults(xmlXPathObjectPtr xpathObj)
 {
     int lpc, max = numXpathResults(xpathObj);
 
     if (xpathObj == NULL) {
         return;
     }
 
     for (lpc = 0; lpc < max; lpc++) {
         xmlNode *xml = NULL;
         gboolean dedup = FALSE;
 
         if (xpathObj->nodesetval->nodeTab[lpc] == NULL) {
             continue;
         }
 
         xml = xpathObj->nodesetval->nodeTab[lpc]->parent;
 
         for (; xml; xml = xml->parent) {
             int lpc2 = 0;
 
             for (lpc2 = 0; lpc2 < max; lpc2++) {
                 if (xpathObj->nodesetval->nodeTab[lpc2] == xml) {
                     xpathObj->nodesetval->nodeTab[lpc] = NULL;
                     dedup = TRUE;
                     break;
                 }
             }
 
             if (dedup) {
                 break;
             }
         }
     }
 }
 
 /* the caller needs to check if the result contains a xmlDocPtr or xmlNodePtr */
 xmlXPathObjectPtr
 xpath_search(xmlNode * xml_top, const char *path)
 {
     xmlDocPtr doc = NULL;
     xmlXPathObjectPtr xpathObj = NULL;
     xmlXPathContextPtr xpathCtx = NULL;
     const xmlChar *xpathExpr = (const xmlChar *)path;
 
     CRM_CHECK(path != NULL, return NULL);
     CRM_CHECK(xml_top != NULL, return NULL);
     CRM_CHECK(strlen(path) > 0, return NULL);
 
     doc = getDocPtr(xml_top);
 
     xpathCtx = xmlXPathNewContext(doc);
     CRM_ASSERT(xpathCtx != NULL);
 
     xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
     xmlXPathFreeContext(xpathCtx);
     return xpathObj;
 }
 
+/*!
+ * \brief Run a supplied function for each result of an xpath search
+ *
+ * \param[in] xml            XML to search
+ * \param[in] xpath          XPath search string
+ * \param[in] helper         Function to call for each result
+ * \param[in/out] user_data  Data to pass to supplied function
+ *
+ * \note The helper function will be passed the XML node of the result,
+ *       and the supplied user_data. This function does not otherwise
+ *       use user_data.
+ */
+void
+crm_foreach_xpath_result(xmlNode *xml, const char *xpath,
+                         void (*helper)(xmlNode*, void*), void *user_data)
+{
+    xmlXPathObjectPtr xpathObj = xpath_search(xml, xpath);
+    int nresults = numXpathResults(xpathObj);
+    int i;
+
+    for (i = 0; i < nresults; i++) {
+        xmlNode *result = getXpathResult(xpathObj, i);
+
+        CRM_LOG_ASSERT(result != NULL);
+        if (result) {
+            (*helper)(result, user_data);
+        }
+    }
+    freeXpathObject(xpathObj);
+}
+
 xmlNode *
 get_xpath_object_relative(const char *xpath, xmlNode * xml_obj, int error_level)
 {
     int len = 0;
     xmlNode *result = NULL;
     char *xpath_full = NULL;
     char *xpath_prefix = NULL;
 
     if (xml_obj == NULL || xpath == NULL) {
         return NULL;
     }
 
     xpath_prefix = (char *)xmlGetNodePath(xml_obj);
     len += strlen(xpath_prefix);
     len += strlen(xpath);
 
     xpath_full = strdup(xpath_prefix);
     xpath_full = realloc_safe(xpath_full, len + 1);
     strncat(xpath_full, xpath, len);
 
     result = get_xpath_object(xpath_full, xml_obj, error_level);
 
     free(xpath_prefix);
     free(xpath_full);
     return result;
 }
 
 xmlNode *
 get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level)
 {
     int max;
     xmlNode *result = NULL;
     xmlXPathObjectPtr xpathObj = NULL;
     char *nodePath = NULL;
     char *matchNodePath = NULL;
 
     if (xpath == NULL) {
         return xml_obj;         /* or return NULL? */
     }
 
     xpathObj = xpath_search(xml_obj, xpath);
     nodePath = (char *)xmlGetNodePath(xml_obj);
     max = numXpathResults(xpathObj);
 
     if (max < 1) {
         do_crm_log(error_level, "No match for %s in %s", xpath, crm_str(nodePath));
         crm_log_xml_explicit(xml_obj, "Unexpected Input");
 
     } else if (max > 1) {
         int lpc = 0;
 
         do_crm_log(error_level, "Too many matches for %s in %s", xpath, crm_str(nodePath));
 
         for (lpc = 0; lpc < max; lpc++) {
             xmlNode *match = getXpathResult(xpathObj, lpc);
 
             CRM_LOG_ASSERT(match != NULL);
             if(match != NULL) {
                 matchNodePath = (char *)xmlGetNodePath(match);
                 do_crm_log(error_level, "%s[%d] = %s", xpath, lpc, crm_str(matchNodePath));
                 free(matchNodePath);
             }
         }
         crm_log_xml_explicit(xml_obj, "Bad Input");
 
     } else {
         result = getXpathResult(xpathObj, 0);
     }
 
     freeXpathObject(xpathObj);
     free(nodePath);
 
     return result;
 }