diff --git a/include/crm/common/xml.h b/include/crm/common/xml.h
index d47f78e01c..b00bf8d46a 100644
--- a/include/crm/common/xml.h
+++ b/include/crm/common/xml.h
@@ -1,196 +1,195 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_COMMON_XML__H
 #  define PCMK__CRM_COMMON_XML__H
 
 
 #  include <stdio.h>
 #  include <sys/types.h>
 #  include <unistd.h>
 
 #  include <stdlib.h>
 #  include <errno.h>
 #  include <fcntl.h>
 
 #  include <libxml/tree.h>
 #  include <libxml/xpath.h>
 
 #  include <crm/crm.h>
 #  include <crm/common/nvpair.h>
 #  include <crm/common/xml_io.h>
 #  include <crm/common/xml_names.h>
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Wrappers for and extensions to libxml2
  * \ingroup core
  */
 
 typedef const xmlChar *pcmkXmlStr;
 
 gboolean add_message_xml(xmlNode * msg, const char *field, xmlNode * xml);
 xmlNode *get_message_xml(const xmlNode *msg, const char *field);
 
 /*
  * \brief xmlCopyPropList ACLs-sensitive replacement expading i++ notation
  *
  * The gist is the same as with \c{xmlCopyPropList(target, src->properties)}.
  * The function exits prematurely when any attribute cannot be copied for
  * ACLs violation.  Even without bailing out, the result can possibly be
  * incosistent with expectations in that case, hence the caller shall,
  * aposteriori, verify that no document-level-tracked denial was indicated
  * with \c{xml_acl_denied(target)} and drop whole such intermediate object.
  *
  * \param[in,out] target  Element to receive attributes from #src element
  * \param[in]     src     Element carrying attributes to copy over to #target
  *
  * \note Original commit 1c632c506 sadly haven't stated which otherwise
  *       assumed behaviours of xmlCopyPropList were missing beyond otherwise
  *       custom extensions like said ACLs and "atomic increment" (that landed
  *       later on, anyway).
  */
 void copy_in_properties(xmlNode *target, const xmlNode *src);
 
 void expand_plus_plus(xmlNode * target, const char *name, const char *value);
 void fix_plus_plus_recursive(xmlNode * target);
 
 
 /*
  * Searching & Modifying
  */
 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_map_element_name(const xmlNode *xml)
 {
     if (xml == NULL) {
         return NULL;
     } else if (strcmp((const char *) xml->name, "master") == 0) {
         // Can't use PCMK__XE_PROMOTABLE_LEGACY because it's internal
         return PCMK_XE_CLONE;
     } else {
         return (const char *) xml->name;
     }
 }
 
 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);
 
 /* schema-related functions (from schemas.c) */
 gboolean validate_xml(xmlNode * xml_blob, const char *validation, gboolean to_logs);
 gboolean validate_xml_verbose(const xmlNode *xml_blob);
 
 /*!
  * \brief Update CIB XML to most recent schema version
  *
  * "Update" means either actively employ XSLT-based transformation(s)
  * (if intermediate product to transform valid per its declared schema version,
  * transformation available, proceeded successfully with a result valid per
  * expectated newer schema version), or just try to bump the marked validating
  * schema until all gradually rising schema versions attested or the first
  * such attempt subsequently fails to validate.   Which of the two styles will
  * be used depends on \p transform parameter (positive/negative, respectively).
  *
  * \param[in,out] xml_blob   XML tree representing CIB, may be swapped with
  *                           an "updated" one
  * \param[out]    best       The highest configuration version (per its index
  *                           in the global schemas table) it was possible to
  *                           reach during the update steps while ensuring
  *                           the validity of the result; if no validation
  *                           success was observed against possibly multiple
  *                           schemas, the value is less or equal the result
  *                           of \c get_schema_version applied on the input
  *                           \p xml_blob value (unless that function maps it
  *                           to -1, then 0 would be used instead)
  * \param[in]     max        When \p transform is positive, this allows to
  *                           set upper boundary schema (per its index in the
  *                           global schemas table) beyond which it's forbidden
  *                           to update by the means of XSLT transformation
  * \param[in]     transform  Whether to employ XSLT-based transformation so
  *                           as to allow overcoming possible incompatibilities
  *                           between major schema versions (see above)
  * \param[in]     to_logs    If true, output notable progress info to
  *                           internal log streams; if false, to stderr
  *
  * \return \c pcmk_ok if no non-recoverable error encountered (up to
  *         caller to evaluate if the update satisfies the requirements
  *         per returned \p best value), negative value carrying the reason
  *         otherwise
  */
 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);
 const char *xml_latest_schema(void);
 gboolean cli_config_update(xmlNode ** xml, int *best_version, gboolean to_logs);
 
 /*!
  * \brief Initialize the CRM XML subsystem
  *
  * This method sets global XML settings and loads pacemaker schemas into the cache.
  */
 void crm_xml_init(void);
 void crm_xml_cleanup(void);
 
 void pcmk_free_xml_subtree(xmlNode *xml);
 void free_xml(xmlNode * child);
 
 xmlNode *sorted_xml(xmlNode * input, xmlNode * parent, gboolean recursive);
 xmlXPathObjectPtr xpath_search(const xmlNode *xml_top, const char *path);
 void crm_foreach_xpath_result(xmlNode *xml, const char *xpath,
                               void (*helper)(xmlNode*, void*), void *user_data);
 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;
 }
 
 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_xml, xmlNode *new_xml);
 void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml);
 void xml_accept_changes(xmlNode * xml);
 bool xml_patch_versions(const 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 crm_xml_sanitize_id(char *id);
 void crm_xml_set_id(xmlNode *xml, const char *format, ...) G_GNUC_PRINTF(2, 3);
 
 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
 #include <crm/common/xml_compat.h>
 #endif
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif
diff --git a/include/crm/common/xml_compat.h b/include/crm/common/xml_compat.h
index 58a21e454c..1a9cdbbb31 100644
--- a/include/crm/common/xml_compat.h
+++ b/include/crm/common/xml_compat.h
@@ -1,139 +1,143 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_COMMON_XML_COMPAT__H
 #  define PCMK__CRM_COMMON_XML_COMPAT__H
 
 #include <glib.h>               // gboolean
 #include <libxml/tree.h>        // xmlNode
 
 #include <crm/common/nvpair.h>  // crm_xml_add()
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Deprecated Pacemaker XML API
  * \ingroup core
  * \deprecated Do not include this header directly. The XML APIs in this
  *             header, and the header itself, will be removed in a future
  *             release.
  */
 
 //! \deprecated Do not use (will be removed in a future release)
 #define XML_PARANOIA_CHECKS 0
 
 //! \deprecated This function will be removed in a future release
 xmlDoc *getDocPtr(xmlNode *node);
 
 //! \deprecated This function will be removed in a future release
 int add_node_nocopy(xmlNode * parent, const char *name, xmlNode * child);
 
 //! \deprecated This function will be removed in a future release
 xmlNode *find_entity(xmlNode *parent, const char *node_name, const char *id);
 
 //! \deprecated This function will be removed in a future release
 char *xml_get_path(const xmlNode *xml);
 
 //! \deprecated This function will be removed in a future release
 void xml_log_changes(uint8_t level, const char *function, const xmlNode *xml);
 
 //! \deprecated This function will be removed in a future release
 void xml_log_patchset(uint8_t level, const char *function, const xmlNode *xml);
 
 //!  \deprecated Use xml_apply_patchset() instead
 gboolean apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml);
 
 //! \deprecated Do not use (will be removed in a future release)
 void crm_destroy_xml(gpointer data);
 
 //! \deprecated Check children member directly
 gboolean xml_has_children(const xmlNode *root);
 
 //! \deprecated Use crm_xml_add() with "true" or "false" instead
 static inline const char *
 crm_xml_add_boolean(xmlNode *node, const char *name, gboolean value)
 {
     return crm_xml_add(node, name, (value? "true" : "false"));
 }
 
 //! \deprecated Use name member directly
 static inline const char *
 crm_element_name(const xmlNode *xml)
 {
     return (xml == NULL)? NULL : (const char *) xml->name;
 }
 
 //! \deprecated Do not use
 char *crm_xml_escape(const char *text);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *copy_xml(xmlNode *src_node);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *add_node_copy(xmlNode *new_parent, xmlNode *xml_node);
 
 //! \deprecated Do not use
 void purge_diff_markers(xmlNode *a_node);
 
 //! \deprecated Do not use
 xmlNode *diff_xml_object(xmlNode *left, xmlNode *right, gboolean suppress);
 
 //! \deprecated Do not use
 xmlNode *subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right,
                              gboolean full, gboolean *changed,
                              const char *marker);
 
 //! \deprecated Do not use
 gboolean can_prune_leaf(xmlNode *xml_node);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *create_xml_node(xmlNode *parent, const char *name);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *pcmk_create_xml_text_node(xmlNode *parent, const char *name,
                                    const char *content);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *pcmk_create_html_node(xmlNode *parent, const char *element_name,
                                const char *id, const char *class_name,
                                const char *text);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *first_named_child(const xmlNode *parent, const char *name);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *find_xml_node(const xmlNode *root, const char *search_path,
                        gboolean must_find);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *crm_next_same_xml(const xmlNode *sibling);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 void xml_remove_prop(xmlNode *obj, const char *name);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 gboolean replace_xml_child(xmlNode *parent, xmlNode *child, xmlNode *update,
                            gboolean delete_only);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 gboolean update_xml_child(xmlNode *child, xmlNode *to_update);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 int find_xml_children(xmlNode **children, xmlNode *root, const char *tag,
                       const char *field, const char *value,
                       gboolean search_matches);
 
+//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
+xmlNode *get_xpath_object_relative(const char *xpath, xmlNode *xml_obj,
+                                   int error_level);
+
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__CRM_COMMON_XML_COMPAT__H
diff --git a/lib/common/xpath.c b/lib/common/xpath.c
index 4c2c634c59..9fc95c5a5f 100644
--- a/lib/common/xpath.c
+++ b/lib/common/xpath.c
@@ -1,396 +1,396 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 #include <stdio.h>
 #include <string.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 #include "crmcommon_private.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(const xmlNode *xml_top, const char *path)
 {
     xmlXPathObjectPtr xpathObj = NULL;
     xmlXPathContextPtr xpathCtx = NULL;
     const xmlChar *xpathExpr = (pcmkXmlStr) path;
 
     CRM_CHECK(path != NULL, return NULL);
     CRM_CHECK(xml_top != NULL, return NULL);
     CRM_CHECK(strlen(path) > 0, return NULL);
 
     xpathCtx = xmlXPathNewContext(xml_top->doc);
     pcmk__mem_assert(xpathCtx);
 
     xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
     xmlXPathFreeContext(xpathCtx);
     return xpathObj;
 }
 
 /*!
  * \brief Run a supplied function for each result of an xpath search
  *
  * \param[in,out] 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)
-{
-    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);
-
-    xpath_full = crm_strdup_printf("%s%s", xpath_prefix, xpath);
-
-    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) {
         if (error_level < LOG_NEVER) {
             do_crm_log(error_level, "No match for %s in %s",
                        xpath, pcmk__s(nodePath, "unknown path"));
             crm_log_xml_explicit(xml_obj, "Unexpected Input");
         }
 
     } else if (max > 1) {
         if (error_level < LOG_NEVER) {
             int lpc = 0;
 
             do_crm_log(error_level, "Too many matches for %s in %s",
                        xpath, pcmk__s(nodePath, "unknown path"));
 
             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,
                                pcmk__s(matchNodePath, "unrecognizable match"));
                     free(matchNodePath);
                 }
             }
             crm_log_xml_explicit(xml_obj, "Bad Input");
         }
 
     } else {
         result = getXpathResult(xpathObj, 0);
     }
 
     freeXpathObject(xpathObj);
     free(nodePath);
 
     return result;
 }
 
 /*!
  * \internal
  * \brief Get an XPath string that matches an XML element as closely as possible
  *
  * \param[in] xml  The XML element for which to build an XPath string
  *
  * \return A \p GString that matches \p xml, or \p NULL if \p xml is \p NULL.
  *
  * \note The caller is responsible for freeing the string using
  *       \p g_string_free().
  */
 GString *
 pcmk__element_xpath(const xmlNode *xml)
 {
     const xmlNode *parent = NULL;
     GString *xpath = NULL;
     const char *id = NULL;
 
     if (xml == NULL) {
         return NULL;
     }
 
     parent = xml->parent;
     xpath = pcmk__element_xpath(parent);
     if (xpath == NULL) {
         xpath = g_string_sized_new(256);
     }
 
     // Build xpath like "/" -> "/cib" -> "/cib/configuration"
     if (parent == NULL) {
         g_string_append_c(xpath, '/');
     } else if (parent->parent == NULL) {
         g_string_append(xpath, (const gchar *) xml->name);
     } else {
         pcmk__g_strcat(xpath, "/", (const char *) xml->name, NULL);
     }
 
     id = pcmk__xe_id(xml);
     if (id != NULL) {
         pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", id, "']", NULL);
     }
 
     return xpath;
 }
 
 char *
 pcmk__xpath_node_id(const char *xpath, const char *node)
 {
     char *retval = NULL;
     char *patt = NULL;
     char *start = NULL;
     char *end = NULL;
 
     if (node == NULL || xpath == NULL) {
         return retval;
     }
 
     patt = crm_strdup_printf("/%s[@" PCMK_XA_ID "=", node);
     start = strstr(xpath, patt);
 
     if (!start) {
         free(patt);
         return retval;
     }
 
     start += strlen(patt);
     start++;
 
     end = strstr(start, "\'");
     CRM_ASSERT(end);
     retval = strndup(start, end-start);
 
     free(patt);
     return retval;
 }
 
 static int
 output_attr_child(xmlNode *child, void *userdata)
 {
     pcmk__output_t *out = userdata;
 
     out->info(out, "  Value: %s \t(id=%s)",
               crm_element_value(child, PCMK_XA_VALUE),
               pcmk__s(pcmk__xe_id(child), "<none>"));
     return pcmk_rc_ok;
 }
 
 void
 pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search,
                                  const char *name)
 {
     if (out == NULL || name == NULL || search == NULL ||
         search->children == NULL) {
         return;
     }
 
     out->info(out, "Multiple attributes match " PCMK_XA_NAME "=%s", name);
     pcmk__xe_foreach_child(search, NULL, output_attr_child, out);
 }
 
 // Deprecated functions kept only for backward API compatibility
 // LCOV_EXCL_START
 
 #include <crm/common/xml_compat.h>
 
 /*!
  * \deprecated This function will be removed in a future release
  * \brief Get an XPath string that matches an XML element as closely as possible
  *
  * \param[in] xml  The XML element for which to build an XPath string
  *
  * \return A string that matches \p xml, or \p NULL if \p xml is \p NULL.
  *
  * \note The caller is responsible for freeing the string using free().
  */
 char *
 xml_get_path(const xmlNode *xml)
 {
     char *path = NULL;
     GString *g_path = pcmk__element_xpath(xml);
 
     if (g_path == NULL) {
         return NULL;
     }
     path = pcmk__str_copy(g_path->str);
     g_string_free(g_path, TRUE);
     return path;
 }
 
+xmlNode *
+get_xpath_object_relative(const char *xpath, xmlNode *xml_obj, int error_level)
+{
+    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);
+
+    xpath_full = crm_strdup_printf("%s%s", xpath_prefix, xpath);
+
+    result = get_xpath_object(xpath_full, xml_obj, error_level);
+
+    free(xpath_prefix);
+    free(xpath_full);
+    return result;
+}
+
 // LCOV_EXCL_STOP
 // End deprecated API