Blob Blame History Raw
From ea636bc7b290325a8d11f56c4ca461d4d010643d Mon Sep 17 00:00:00 2001
From: Ken Gaillot <kgaillot@redhat.com>
Date: Mon, 14 Sep 2020 16:29:19 -0500
Subject: [PATCH 1/6] Refactor: tools: restructure crm_resource command-line
 resource configuration

... to allow (future) options other than --validate to use the command-line
resource configuration options.

I had planned to use this for a project but went in different direction, so
nothing more is expected to use it for now, but I think it's still worthwhile
to help isolate different parts of code.
---
 tools/crm_resource.c | 189 ++++++++++++++++++++++++++-------------------------
 1 file changed, 98 insertions(+), 91 deletions(-)

diff --git a/tools/crm_resource.c b/tools/crm_resource.c
index 2fc9a86..1dcb0f0 100644
--- a/tools/crm_resource.c
+++ b/tools/crm_resource.c
@@ -1,5 +1,5 @@
 /*
- * Copyright 2004-2020 the Pacemaker project contributors
+ * Copyright 2004-2021 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
@@ -66,39 +66,46 @@ enum rsc_command {
 };
 
 struct {
-    enum rsc_command rsc_cmd;   // The crm_resource command to perform
-    const char *attr_set_type;
-    int cib_options;
-    gboolean clear_expired;
-    int find_flags;             /* Flags to use when searching for resource */
-    gboolean force;
-    gchar *host_uname;
-    gchar *interval_spec;
-    gchar *move_lifetime;
-    gchar *operation;
-    GHashTable *override_params;
-    gchar *prop_id;
-    char *prop_name;
-    gchar *prop_set;
-    gchar *prop_value;
-    gboolean recursive;
-    gchar **remainder;
-    gboolean require_cib;           // Whether command requires CIB connection
-    gboolean require_crmd;          /* whether command requires controller connection */
-    gboolean require_dataset;       /* whether command requires populated dataset instance */
-    gboolean require_resource;      /* whether command requires that resource be specified */
-    int resource_verbose;
-    gchar *rsc_id;
-    gchar *rsc_type;
-    gboolean promoted_role_only;
-    int timeout_ms;
-    char *agent_spec;               // Standard and/or provider and/or agent
-    char *v_agent;
-    char *v_class;
-    char *v_provider;
-    gboolean validate_cmdline;      /* whether we are just validating based on command line options */
-    GHashTable *validate_options;
-    gchar *xml_file;
+    enum rsc_command rsc_cmd;     // crm_resource command to perform
+
+    // Infrastructure that given command needs to work
+    gboolean require_cib;         // Whether command requires CIB IPC
+    int cib_options;              // Options to use with CIB IPC calls
+    gboolean require_crmd;        // Whether command requires controller IPC
+    gboolean require_dataset;     // Whether command requires populated data set
+    gboolean require_resource;    // Whether command requires resource specified
+    int find_flags;               // Flags to use when searching for resource
+
+    // Command-line option values
+    gchar *rsc_id;                // Value of --resource
+    gchar *rsc_type;              // Value of --resource-type
+    gboolean force;               // --force was given
+    gboolean clear_expired;       // --expired was given
+    gboolean recursive;           // --recursive was given
+    gboolean promoted_role_only;  // --master was given
+    gchar *host_uname;            // Value of --node
+    gchar *interval_spec;         // Value of --interval
+    gchar *move_lifetime;         // Value of --lifetime
+    gchar *operation;             // Value of --operation
+    const char *attr_set_type;    // Instance, meta, or utilization attribute
+    gchar *prop_id;               // --nvpair (attribute XML ID)
+    char *prop_name;              // Attribute name
+    gchar *prop_set;              // --set-name (attribute block XML ID)
+    gchar *prop_value;            // --parameter-value (attribute value)
+    int timeout_ms;               // Parsed from --timeout value
+    char *agent_spec;             // Standard and/or provider and/or agent
+    gchar *xml_file;              // Value of (deprecated) --xml-file
+
+    // Resource configuration specified via command-line arguments
+    gboolean cmdline_config;      // Resource configuration was via arguments
+    char *v_agent;                // Value of --agent
+    char *v_class;                // Value of --class
+    char *v_provider;             // Value of --provider
+    GHashTable *cmdline_params;   // Resource parameters specified
+
+    // Positional command-line arguments
+    gchar **remainder;            // Positional arguments as given
+    GHashTable *override_params;  // Resource parameter values that override config
 } options = {
     .attr_set_type = XML_TAG_ATTR_SETS,
     .cib_options = cib_sync_call,
@@ -533,28 +540,6 @@ static GOptionEntry advanced_entries[] = {
     { NULL }
 };
 
-static GOptionEntry validate_entries[] = {
-    { "class", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, class_cb,
-      "The standard the resource agent confirms to (for example, ocf).\n"
-      INDENT "Use with --agent, --provider, --option, and --validate.",
-      "CLASS" },
-    { "agent", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, agent_provider_cb,
-      "The agent to use (for example, IPaddr). Use with --class,\n"
-      INDENT "--provider, --option, and --validate.",
-      "AGENT" },
-    { "provider", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, agent_provider_cb,
-      "The vendor that supplies the resource agent (for example,\n"
-      INDENT "heartbeat). Use with --class, --agent, --option, and --validate.",
-      "PROVIDER" },
-    { "option", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, option_cb,
-      "Specify a device configuration parameter as NAME=VALUE (may be\n"
-      INDENT "specified multiple times). Use with --validate and without the\n"
-      INDENT "-r option.",
-      "PARAM" },
-
-    { NULL }
-};
-
 static GOptionEntry addl_entries[] = {
     { "node", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.host_uname,
       "Node name",
@@ -582,6 +567,23 @@ static GOptionEntry addl_entries[] = {
     { "interval", 'I', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.interval_spec,
       "Interval of operation to clear (default 0) (with -C -r -n)",
       "N" },
+    { "class", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, class_cb,
+      "The standard the resource agent conforms to (for example, ocf).\n"
+      INDENT "Use with --agent, --provider, --option, and --validate.",
+      "CLASS" },
+    { "agent", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, agent_provider_cb,
+      "The agent to use (for example, IPaddr). Use with --class,\n"
+      INDENT "--provider, --option, and --validate.",
+      "AGENT" },
+    { "provider", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, agent_provider_cb,
+      "The vendor that supplies the resource agent (for example,\n"
+      INDENT "heartbeat). Use with --class, --agent, --option, and --validate.",
+      "PROVIDER" },
+    { "option", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, option_cb,
+      "Specify a device configuration parameter as NAME=VALUE (may be\n"
+      INDENT "specified multiple times). Use with --validate and without the\n"
+      INDENT "-r option.",
+      "PARAM" },
     { "set-name", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.prop_set,
       "(Advanced) XML ID of attributes element to use (with -p, -d)",
       "ID" },
@@ -608,7 +610,7 @@ static GOptionEntry addl_entries[] = {
 
 gboolean
 agent_provider_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
-    options.validate_cmdline = TRUE;
+    options.cmdline_config = TRUE;
     options.require_resource = FALSE;
 
     if (pcmk__str_eq(option_name, "--provider", pcmk__str_casei)) {
@@ -654,7 +656,7 @@ class_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **
         options.v_class = strdup(optarg);
     }
 
-    options.validate_cmdline = TRUE;
+    options.cmdline_config = TRUE;
     options.require_resource = FALSE;
     return TRUE;
 }
@@ -762,10 +764,10 @@ option_cb(const gchar *option_name, const gchar *optarg, gpointer data,
     if (pcmk_scan_nvpair(optarg, &name, &value) != 2) {
         return FALSE;
     }
-    if (options.validate_options == NULL) {
-        options.validate_options = crm_str_table_new();
+    if (options.cmdline_params == NULL) {
+        options.cmdline_params = crm_str_table_new();
     }
-    g_hash_table_replace(options.validate_options, name, value);
+    g_hash_table_replace(options.cmdline_params, name, value);
     return TRUE;
 }
 
@@ -1365,17 +1367,18 @@ show_metadata(pcmk__output_t *out, const char *agent_spec, crm_exit_t *exit_code
 }
 
 static void
-validate_cmdline(crm_exit_t *exit_code)
+validate_cmdline_config(void)
 {
-    // -r cannot be used with any of --class, --agent, or --provider
+    // Cannot use both --resource and command-line resource configuration
     if (options.rsc_id != NULL) {
         g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
                     "--resource cannot be used with --class, --agent, and --provider");
 
-    // If --class, --agent, or --provider are given, --validate must also be given.
+    // Not all commands support command-line resource configuration
     } else if (options.rsc_cmd != cmd_execute_agent) {
         g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
-                    "--class, --agent, and --provider require --validate");
+                    "--class, --agent, and --provider can only be used with "
+                    "--validate");
 
     // Not all of --class, --agent, and --provider need to be given.  Not all
     // classes support the concept of a provider.  Check that what we were given
@@ -1398,15 +1401,16 @@ validate_cmdline(crm_exit_t *exit_code)
                     options.v_agent ? options.v_agent : "");
     }
 
-    if (error == NULL) {
-        if (options.validate_options == NULL) {
-            options.validate_options = crm_str_table_new();
-        }
-        *exit_code = cli_resource_execute_from_params(out, "test", options.v_class, options.v_provider, options.v_agent,
-                                                      "validate-all", options.validate_options,
-                                                      options.override_params, options.timeout_ms,
-                                                      options.resource_verbose, options.force);
+    if (error != NULL) {
+        return;
+    }
+
+    if (options.cmdline_params == NULL) {
+        options.cmdline_params = crm_str_table_new();
     }
+    options.require_resource = FALSE;
+    options.require_dataset = FALSE;
+    options.require_cib = FALSE;
 }
 
 static GOptionContext *
@@ -1467,8 +1471,6 @@ build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
                         "Show command help", command_entries);
     pcmk__add_arg_group(context, "locations", "Locations:",
                         "Show location help", location_entries);
-    pcmk__add_arg_group(context, "validate", "Validate:",
-                        "Show validate help", validate_entries);
     pcmk__add_arg_group(context, "advanced", "Advanced:",
                         "Show advanced option help", advanced_entries);
     pcmk__add_arg_group(context, "additional", "Additional Options:",
@@ -1512,7 +1514,6 @@ main(int argc, char **argv)
         goto done;
     }
 
-    options.resource_verbose = args->verbosity;
     out->quiet = args->quiet;
 
     crm_log_args(argc, argv);
@@ -1628,15 +1629,15 @@ main(int argc, char **argv)
         goto done;
     }
 
-    // Sanity check validating from command line parameters.  If everything checks out,
-    // go ahead and run the validation.  This way we don't need a CIB connection.
-    if (options.validate_cmdline) {
-        validate_cmdline(&exit_code);
-        goto done;
-    } else if (options.validate_options != NULL) {
+    if (options.cmdline_config) {
+        /* A resource configuration was given on the command line. Sanity-check
+         * the values and set error if they don't make sense.
+         */
+        validate_cmdline_config();
+    } else if (options.cmdline_params != NULL) {
         // @COMPAT @TODO error out here when we can break backward compatibility
-        g_hash_table_destroy(options.validate_options);
-        options.validate_options = NULL;
+        g_hash_table_destroy(options.cmdline_params);
+        options.cmdline_params = NULL;
     }
 
     if (error != NULL) {
@@ -1773,12 +1774,18 @@ main(int argc, char **argv)
             break;
 
         case cmd_execute_agent:
-            exit_code = cli_resource_execute(out, rsc, options.rsc_id,
-                                             options.operation,
-                                             options.override_params,
-                                             options.timeout_ms, cib_conn,
-                                             data_set, options.resource_verbose,
-                                             options.force);
+            if (options.cmdline_config) {
+                exit_code = cli_resource_execute_from_params(out, "test",
+                    options.v_class, options.v_provider, options.v_agent,
+                    "validate-all", options.cmdline_params,
+                    options.override_params, options.timeout_ms,
+                    args->verbosity, options.force);
+            } else {
+                exit_code = cli_resource_execute(out, rsc, options.rsc_id,
+                    options.operation, options.override_params,
+                    options.timeout_ms, cib_conn, data_set,
+                    args->verbosity, options.force);
+            }
             break;
 
         case cmd_colocations:
@@ -2038,7 +2045,7 @@ done:
         g_hash_table_destroy(options.override_params);
     }
 
-    /* options.validate_options does not need to be destroyed here.  See the
+    /* options.cmdline_params does not need to be destroyed here.  See the
      * comments in cli_resource_execute_from_params.
      */
 
-- 
1.8.3.1


From e140bd1bc35a20f027f054b4575808bd0ef547fc Mon Sep 17 00:00:00 2001
From: Ken Gaillot <kgaillot@redhat.com>
Date: Wed, 16 Sep 2020 15:40:16 -0500
Subject: [PATCH 2/6] Low: tools: handle required node names better in
 crm_resource

Currently, --fail is the only option that requires a node name to be specified,
but generalize the handling so future options can reuse it.

This also makes the error handling closer to what's done for required resource
names, both in error message and exit status.
---
 tools/crm_resource.c         | 87 ++++++++++++++++++++++++++++----------------
 tools/crm_resource_runtime.c |  8 +---
 2 files changed, 57 insertions(+), 38 deletions(-)

diff --git a/tools/crm_resource.c b/tools/crm_resource.c
index 1dcb0f0..2717a62 100644
--- a/tools/crm_resource.c
+++ b/tools/crm_resource.c
@@ -74,6 +74,7 @@ struct {
     gboolean require_crmd;        // Whether command requires controller IPC
     gboolean require_dataset;     // Whether command requires populated data set
     gboolean require_resource;    // Whether command requires resource specified
+    gboolean require_node;        // Whether command requires node specified
     int find_flags;               // Flags to use when searching for resource
 
     // Command-line option values
@@ -774,6 +775,7 @@ option_cb(const gchar *option_name, const gchar *optarg, gpointer data,
 gboolean
 fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.require_crmd = TRUE;
+    options.require_node = TRUE;
     SET_COMMAND(cmd_fail);
     return TRUE;
 }
@@ -1483,9 +1485,13 @@ main(int argc, char **argv)
 {
     xmlNode *cib_xml_copy = NULL;
     pe_resource_t *rsc = NULL;
-
+    pe_node_t *node = NULL;
     int rc = pcmk_rc_ok;
 
+    /*
+     * Parse command line arguments
+     */
+
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     GOptionContext *context = NULL;
     GOptionGroup *output_group = NULL;
@@ -1502,6 +1508,10 @@ main(int argc, char **argv)
         goto done;
     }
 
+    /*
+     * Set verbosity
+     */
+
     for (int i = 0; i < args->verbosity; i++) {
         crm_bump_log_level(argc, argv);
     }
@@ -1518,9 +1528,9 @@ main(int argc, char **argv)
 
     crm_log_args(argc, argv);
 
-    if (options.host_uname) {
-        crm_trace("Option host => %s", options.host_uname);
-    }
+    /*
+     * Validate option combinations
+     */
 
     // If the user didn't explicitly specify a command, list resources
     if (options.rsc_cmd == cmd_none) {
@@ -1634,30 +1644,42 @@ main(int argc, char **argv)
          * the values and set error if they don't make sense.
          */
         validate_cmdline_config();
+        if (error != NULL) {
+            exit_code = CRM_EX_USAGE;
+            goto done;
+        }
+
     } else if (options.cmdline_params != NULL) {
         // @COMPAT @TODO error out here when we can break backward compatibility
         g_hash_table_destroy(options.cmdline_params);
         options.cmdline_params = NULL;
     }
 
-    if (error != NULL) {
+    if (options.require_resource && (options.rsc_id == NULL)) {
+        rc = ENXIO;
+        exit_code = CRM_EX_USAGE;
+        g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+                    "Must supply a resource id with -r");
+        goto done;
+    }
+    if (options.require_node && (options.host_uname == NULL)) {
+        rc = ENXIO;
         exit_code = CRM_EX_USAGE;
+        g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+                    "Must supply a node name with -N");
         goto done;
     }
 
+    /*
+     * Set up necessary connections
+     */
+
     if (options.force) {
         crm_debug("Forcing...");
         cib__set_call_options(options.cib_options, crm_system_name,
                               cib_quorum_override);
     }
 
-    if (options.require_resource && !options.rsc_id) {
-        rc = ENXIO;
-        g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
-                    "Must supply a resource id with -r");
-        goto done;
-    }
-
     if (options.find_flags && options.rsc_id) {
         options.require_dataset = TRUE;
     }
@@ -1700,6 +1722,11 @@ main(int argc, char **argv)
         }
     }
 
+    // If user supplied a node name, check whether it exists
+    if ((options.host_uname != NULL) && (data_set != NULL)) {
+        node = pe_find_node(data_set->nodes, options.host_uname);
+    }
+
     // Establish a connection to the controller if needed
     if (options.require_crmd) {
         rc = pcmk_new_ipc_api(&controld_api, pcmk_ipc_controld);
@@ -1718,6 +1745,10 @@ main(int argc, char **argv)
         }
     }
 
+    /*
+     * Handle requested command
+     */
+
     switch (options.rsc_cmd) {
         case cmd_list_resources: {
             GListPtr all = NULL;
@@ -1844,18 +1875,11 @@ main(int argc, char **argv)
             break;
 
         case cmd_why:
-            {
-                pe_node_t *dest = NULL;
-
-                if (options.host_uname) {
-                    dest = pe_find_node(data_set->nodes, options.host_uname);
-                    if (dest == NULL) {
-                        rc = pcmk_rc_node_unknown;
-                        goto done;
-                    }
-                }
-                out->message(out, "resource-reasons-list", cib_conn, data_set->resources, rsc, dest);
-                rc = pcmk_rc_ok;
+            if ((options.host_uname != NULL) && (node == NULL)) {
+                rc = pcmk_rc_node_unknown;
+            } else {
+                rc = out->message(out, "resource-reasons-list", cib_conn,
+                                  data_set->resources, rsc, node);
             }
             break;
 
@@ -1878,15 +1902,10 @@ main(int argc, char **argv)
         case cmd_ban:
             if (options.host_uname == NULL) {
                 rc = ban_or_move(out, rsc, options.move_lifetime, &exit_code);
+            } else if (node == NULL) {
+                rc = pcmk_rc_node_unknown;
             } else {
-                pe_node_t *dest = pe_find_node(data_set->nodes,
-                                               options.host_uname);
-
-                if (dest == NULL) {
-                    rc = pcmk_rc_node_unknown;
-                    goto done;
-                }
-                rc = cli_resource_ban(out, options.rsc_id, dest->details->uname,
+                rc = cli_resource_ban(out, options.rsc_id, node->details->uname,
                                       options.move_lifetime, NULL, cib_conn,
                                       options.cib_options,
                                       options.promoted_role_only);
@@ -2002,6 +2021,10 @@ main(int argc, char **argv)
             break;
     }
 
+    /*
+     * Clean up and exit
+     */
+
 done:
     if (rc != pcmk_rc_ok) {
         if (rc == pcmk_rc_no_quorum) {
diff --git a/tools/crm_resource_runtime.c b/tools/crm_resource_runtime.c
index 3f28c7b..de5e807 100644
--- a/tools/crm_resource_runtime.c
+++ b/tools/crm_resource_runtime.c
@@ -1,5 +1,5 @@
 /*
- * Copyright 2004-2020 the Pacemaker project contributors
+ * Copyright 2004-2021 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
@@ -511,11 +511,7 @@ send_lrm_rsc_op(pcmk__output_t *out, pcmk_ipc_api_t *controld_api, bool do_fail_
         return EINVAL;
     }
 
-    if (host_uname == NULL) {
-        out->err(out, "Please specify a node name");
-        return EINVAL;
-
-    } else {
+    {
         pe_node_t *node = pe_find_node(data_set->nodes, host_uname);
 
         if (node == NULL) {
-- 
1.8.3.1


From 31bda91470487790d6e17b6f2cbed282bafd11d0 Mon Sep 17 00:00:00 2001
From: Ken Gaillot <kgaillot@redhat.com>
Date: Tue, 15 Sep 2020 15:00:53 -0500
Subject: [PATCH 3/6] Refactor: libpacemaker: add files for resource-related
 API

---
 include/pacemaker-internal.h   |  3 ++-
 include/pcmki/Makefile.am      |  3 ++-
 include/pcmki/pcmki_resource.h | 14 ++++++++++++++
 lib/pacemaker/Makefile.am      |  3 ++-
 lib/pacemaker/pcmk_resource.c  | 21 +++++++++++++++++++++
 5 files changed, 41 insertions(+), 3 deletions(-)
 create mode 100644 include/pcmki/pcmki_resource.h
 create mode 100644 lib/pacemaker/pcmk_resource.c

diff --git a/include/pacemaker-internal.h b/include/pacemaker-internal.h
index 2e75d09..bf33f3e 100644
--- a/include/pacemaker-internal.h
+++ b/include/pacemaker-internal.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 the Pacemaker project contributors
+ * Copyright 2019-2021 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
@@ -14,6 +14,7 @@
 #  include <pcmki/pcmki_cluster_queries.h>
 #  include <pcmki/pcmki_fence.h>
 #  include <pcmki/pcmki_output.h>
+#  include <pcmki/pcmki_resource.h>
 #  include <pcmki/pcmki_sched_allocate.h>
 #  include <pcmki/pcmki_sched_notif.h>
 #  include <pcmki/pcmki_sched_utils.h>
diff --git a/include/pcmki/Makefile.am b/include/pcmki/Makefile.am
index 7aa64c7..446c801 100644
--- a/include/pcmki/Makefile.am
+++ b/include/pcmki/Makefile.am
@@ -1,5 +1,5 @@
 #
-# Copyright 2019 the Pacemaker project contributors
+# Copyright 2019-2021 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
@@ -13,6 +13,7 @@ noinst_HEADERS		= pcmki_error.h \
 			  pcmki_cluster_queries.h \
 			  pcmki_fence.h \
 			  pcmki_output.h \
+			  pcmki_resource.h \
 			  pcmki_sched_allocate.h \
 			  pcmki_sched_notif.h \
 			  pcmki_sched_utils.h \
diff --git a/include/pcmki/pcmki_resource.h b/include/pcmki/pcmki_resource.h
new file mode 100644
index 0000000..effa945
--- /dev/null
+++ b/include/pcmki/pcmki_resource.h
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2021 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 PCMKI_RESOURCE__H
+#define PCMKI_RESOURCE__H
+
+#include <crm/common/output_internal.h>
+
+#endif /* PCMK_RESOURCE__H */
diff --git a/lib/pacemaker/Makefile.am b/lib/pacemaker/Makefile.am
index 4129ade..760c04a 100644
--- a/lib/pacemaker/Makefile.am
+++ b/lib/pacemaker/Makefile.am
@@ -1,5 +1,5 @@
 #
-# Copyright 2004-2019 the Pacemaker project contributors
+# Copyright 2004-2021 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
@@ -31,6 +31,7 @@ libpacemaker_la_SOURCES	=
 libpacemaker_la_SOURCES += pcmk_cluster_queries.c
 libpacemaker_la_SOURCES += pcmk_fence.c
 libpacemaker_la_SOURCES += pcmk_output.c
+libpacemaker_la_SOURCES += pcmk_resource.c
 libpacemaker_la_SOURCES	+= pcmk_sched_allocate.c
 libpacemaker_la_SOURCES += pcmk_sched_bundle.c
 libpacemaker_la_SOURCES += pcmk_sched_clone.c
diff --git a/lib/pacemaker/pcmk_resource.c b/lib/pacemaker/pcmk_resource.c
new file mode 100644
index 0000000..05614fc
--- /dev/null
+++ b/lib/pacemaker/pcmk_resource.c
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2021 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <glib.h>
+#include <libxml/tree.h>
+
+#include <crm/common/mainloop.h>
+#include <crm/common/results.h>
+#include <crm/common/output_internal.h>
+#include <crm/pengine/internal.h>
+
+#include <pacemaker.h>
+#include <pacemaker-internal.h>
-- 
1.8.3.1


From e45fe95cc6f526ab67ab6f718aa1364861ca525b Mon Sep 17 00:00:00 2001
From: Ken Gaillot <kgaillot@redhat.com>
Date: Tue, 15 Sep 2020 15:32:25 -0500
Subject: [PATCH 4/6] API: libpacemaker: new API pcmk_resource_digests()

---
 include/pacemaker.h            |  22 +++++++-
 include/pcmki/pcmki_resource.h |   7 +++
 lib/pacemaker/pcmk_output.c    | 107 +++++++++++++++++++++++++++++++++++-
 lib/pacemaker/pcmk_resource.c  | 119 +++++++++++++++++++++++++++++++++++++++++
 xml/Makefile.am                |  10 +++-
 xml/api/digests-2.6.rng        |  33 ++++++++++++
 6 files changed, 293 insertions(+), 5 deletions(-)
 create mode 100644 xml/api/digests-2.6.rng

diff --git a/include/pacemaker.h b/include/pacemaker.h
index b2a73cd..51bf585 100644
--- a/include/pacemaker.h
+++ b/include/pacemaker.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 the Pacemaker project contributors
+ * Copyright 2019-2021 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
@@ -20,8 +20,11 @@ extern "C" {
  * \ingroup pacemaker
  */
 
-#  include <crm/stonith-ng.h>
+#  include <glib.h>
 #  include <libxml/tree.h>
+#  include <crm/pengine/pe_types.h>
+
+#  include <crm/stonith-ng.h>
 
 /*!
  * \brief Get controller status
@@ -55,6 +58,21 @@ int pcmk_designated_controller(xmlNodePtr *xml, unsigned int message_timeout_ms)
  */
 int pcmk_pacemakerd_status(xmlNodePtr *xml, char *ipc_name, unsigned int message_timeout_ms);
 
+/*!
+ * \brief Calculate and output resource operation digests
+ *
+ * \param[out] xml        Where to store XML with result
+ * \param[in]  rsc        Resource to calculate digests for
+ * \param[in]  node       Node whose operation history should be used
+ * \param[in]  overrides  Hash table of configuration parameters to override
+ * \param[in]  data_set   Cluster working set (with status)
+ *
+ * \return Standard Pacemaker return code
+ */
+int pcmk_resource_digests(xmlNodePtr *xml, pe_resource_t *rsc,
+                          pe_node_t *node, GHashTable *overrides,
+                          pe_working_set_t *data_set);
+
 #ifdef BUILD_PUBLIC_LIBPACEMAKER
 
 /*!
diff --git a/include/pcmki/pcmki_resource.h b/include/pcmki/pcmki_resource.h
index effa945..9d2afb5 100644
--- a/include/pcmki/pcmki_resource.h
+++ b/include/pcmki/pcmki_resource.h
@@ -9,6 +9,13 @@
 #ifndef PCMKI_RESOURCE__H
 #define PCMKI_RESOURCE__H
 
+#include <glib.h>
+
 #include <crm/common/output_internal.h>
+#include <crm/pengine/pe_types.h>
+
+int pcmk__resource_digests(pcmk__output_t *out, pe_resource_t *rsc,
+                           pe_node_t *node, GHashTable *overrides,
+                           pe_working_set_t *data_set);
 
 #endif /* PCMK_RESOURCE__H */
diff --git a/lib/pacemaker/pcmk_output.c b/lib/pacemaker/pcmk_output.c
index 500afd1..bc4b91a 100644
--- a/lib/pacemaker/pcmk_output.c
+++ b/lib/pacemaker/pcmk_output.c
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2020 the Pacemaker project contributors
+ * Copyright 2019-2021 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
@@ -12,6 +12,7 @@
 #include <crm/common/output_internal.h>
 #include <crm/stonith-ng.h>
 #include <crm/fencing/internal.h>
+#include <crm/pengine/internal.h>
 #include <libxml/tree.h>
 #include <pacemaker-internal.h>
 
@@ -539,6 +540,108 @@ crmadmin_node_xml(pcmk__output_t *out, va_list args)
     return pcmk_rc_ok;
 }
 
+PCMK__OUTPUT_ARGS("digests", "pe_resource_t *", "pe_node_t *", "const char *",
+                  "guint", "op_digest_cache_t *")
+static int
+digests_text(pcmk__output_t *out, va_list args)
+{
+    pe_resource_t *rsc = va_arg(args, pe_resource_t *);
+    pe_node_t *node = va_arg(args, pe_node_t *);
+    const char *task = va_arg(args, const char *);
+    guint interval_ms = va_arg(args, guint);
+    op_digest_cache_t *digests = va_arg(args, op_digest_cache_t *);
+
+    char *action_desc = NULL;
+    const char *rsc_desc = "unknown resource";
+    const char *node_desc = "unknown node";
+
+    if (interval_ms != 0) {
+        action_desc = crm_strdup_printf("%ums-interval %s action", interval_ms,
+                                        ((task == NULL)? "unknown" : task));
+    } else if (pcmk__str_eq(task, "monitor", pcmk__str_none)) {
+        action_desc = strdup("probe action");
+    } else {
+        action_desc = crm_strdup_printf("%s action",
+                                        ((task == NULL)? "unknown" : task));
+    }
+    if ((rsc != NULL) && (rsc->id != NULL)) {
+        rsc_desc = rsc->id;
+    }
+    if ((node != NULL) && (node->details->uname != NULL)) {
+        node_desc = node->details->uname;
+    }
+    out->begin_list(out, NULL, NULL, "Digests for %s %s on %s",
+                    rsc_desc, action_desc, node_desc);
+    free(action_desc);
+
+    if (digests == NULL) {
+        out->list_item(out, NULL, "none");
+        out->end_list(out);
+        return pcmk_rc_ok;
+    }
+    if (digests->digest_all_calc != NULL) {
+        out->list_item(out, NULL, "%s (all parameters)",
+                       digests->digest_all_calc);
+    }
+    if (digests->digest_secure_calc != NULL) {
+        out->list_item(out, NULL, "%s (non-private parameters)",
+                       digests->digest_secure_calc);
+    }
+    if (digests->digest_restart_calc != NULL) {
+        out->list_item(out, NULL, "%s (non-reloadable parameters)",
+                       digests->digest_restart_calc);
+    }
+    out->end_list(out);
+    return pcmk_rc_ok;
+}
+
+static void
+add_digest_xml(xmlNode *parent, const char *type, const char *digest,
+               xmlNode *digest_source)
+{
+    if (digest != NULL) {
+        xmlNodePtr digest_xml = create_xml_node(parent, "digest");
+
+        crm_xml_add(digest_xml, "type", ((type == NULL)? "unspecified" : type));
+        crm_xml_add(digest_xml, "hash", digest);
+        if (digest_source != NULL) {
+            add_node_copy(digest_xml, digest_source);
+        }
+    }
+}
+
+PCMK__OUTPUT_ARGS("digests", "pe_resource_t *", "pe_node_t *", "const char *",
+                  "guint", "op_digest_cache_t *")
+static int
+digests_xml(pcmk__output_t *out, va_list args)
+{
+    pe_resource_t *rsc = va_arg(args, pe_resource_t *);
+    pe_node_t *node = va_arg(args, pe_node_t *);
+    const char *task = va_arg(args, const char *);
+    guint interval_ms = va_arg(args, guint);
+    op_digest_cache_t *digests = va_arg(args, op_digest_cache_t *);
+
+    char *interval_s = crm_strdup_printf("%ums", interval_ms);
+    xmlNode *xml = NULL;
+
+    xml = pcmk__output_create_xml_node(out, "digests",
+                                       "resource", crm_str(rsc->id),
+                                       "node", crm_str(node->details->uname),
+                                       "task", crm_str(task),
+                                       "interval", interval_s,
+                                       NULL);
+    free(interval_s);
+    if (digests != NULL) {
+        add_digest_xml(xml, "all", digests->digest_all_calc,
+                       digests->params_all);
+        add_digest_xml(xml, "nonprivate", digests->digest_secure_calc,
+                       digests->params_secure);
+        add_digest_xml(xml, "nonreloadable", digests->digest_restart_calc,
+                       digests->params_restart);
+    }
+    return pcmk_rc_ok;
+}
+
 static pcmk__message_entry_t fmt_functions[] = {
     { "rsc-is-colocated-with-list", "default", rsc_is_colocated_with_list },
     { "rsc-is-colocated-with-list", "xml", rsc_is_colocated_with_list_xml },
@@ -557,6 +660,8 @@ static pcmk__message_entry_t fmt_functions[] = {
     { "crmadmin-node-list", "default", crmadmin_node_list },
     { "crmadmin-node", "default", crmadmin_node_text },
     { "crmadmin-node", "xml", crmadmin_node_xml },
+    { "digests", "default", digests_text },
+    { "digests", "xml", digests_xml },
 
     { NULL, NULL, NULL }
 };
diff --git a/lib/pacemaker/pcmk_resource.c b/lib/pacemaker/pcmk_resource.c
index 05614fc..197edf8 100644
--- a/lib/pacemaker/pcmk_resource.c
+++ b/lib/pacemaker/pcmk_resource.c
@@ -9,6 +9,7 @@
 
 #include <crm_internal.h>
 
+#include <errno.h>
 #include <glib.h>
 #include <libxml/tree.h>
 
@@ -19,3 +20,121 @@
 
 #include <pacemaker.h>
 #include <pacemaker-internal.h>
+
+// Search path for resource operation history (takes node name and resource ID)
+#define XPATH_OP_HISTORY "//" XML_CIB_TAG_STATUS                            \
+                         "/" XML_CIB_TAG_STATE "[@" XML_ATTR_UNAME "='%s']" \
+                         "/" XML_CIB_TAG_LRM "/" XML_LRM_TAG_RESOURCES      \
+                         "/" XML_LRM_TAG_RESOURCE "[@" XML_ATTR_ID "='%s']"
+
+static xmlNode *
+best_op(pe_resource_t *rsc, pe_node_t *node, pe_working_set_t *data_set)
+{
+    char *xpath = NULL;
+    xmlNode *history = NULL;
+    xmlNode *best = NULL;
+
+    // Find node's resource history
+    xpath = crm_strdup_printf(XPATH_OP_HISTORY, node->details->uname, rsc->id);
+    history = get_xpath_object(xpath, data_set->input, LOG_NEVER);
+    free(xpath);
+
+    // Examine each history entry
+    for (xmlNode *lrm_rsc_op = first_named_child(history, XML_LRM_TAG_RSC_OP);
+         lrm_rsc_op != NULL; lrm_rsc_op = crm_next_same_xml(lrm_rsc_op)) {
+
+        const char *digest = crm_element_value(lrm_rsc_op,
+                                               XML_LRM_ATTR_RESTART_DIGEST);
+        guint interval_ms = 0;
+
+        crm_element_value_ms(lrm_rsc_op, XML_LRM_ATTR_INTERVAL, &interval_ms);
+
+        if (pcmk__ends_with(ID(lrm_rsc_op), "_last_failure_0")
+            || (interval_ms != 0)) {
+
+            // Only use last failure or recurring op if nothing else available
+            if (best == NULL) {
+                best = lrm_rsc_op;
+            }
+            continue;
+        }
+
+        best = lrm_rsc_op;
+        if (digest != NULL) {
+            // Any non-recurring action with a restart digest is sufficient
+            break;
+        }
+    }
+    return best;
+}
+
+/*!
+ * \internal
+ * \brief Calculate and output resource operation digests
+ *
+ * \param[in]  out        Output object
+ * \param[in]  rsc        Resource to calculate digests for
+ * \param[in]  node       Node whose operation history should be used
+ * \param[in]  overrides  Hash table of configuration parameters to override
+ * \param[in]  data_set   Cluster working set (with status)
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+pcmk__resource_digests(pcmk__output_t *out, pe_resource_t *rsc,
+                       pe_node_t *node, GHashTable *overrides,
+                       pe_working_set_t *data_set)
+{
+    const char *task = NULL;
+    xmlNode *xml_op = NULL;
+    op_digest_cache_t *digests = NULL;
+    guint interval_ms = 0;
+    int rc = pcmk_rc_ok;
+
+    if ((out == NULL) || (rsc == NULL) || (node == NULL) || (data_set == NULL)) {
+        return EINVAL;
+    }
+    if (rsc->variant != pe_native) {
+        // Only primitives get operation digests
+        return EOPNOTSUPP;
+    }
+
+    // Find XML of operation history to use
+    xml_op = best_op(rsc, node, data_set);
+
+    // Generate an operation key
+    if (xml_op != NULL) {
+        task = crm_element_value(xml_op, XML_LRM_ATTR_TASK);
+        crm_element_value_ms(xml_op, XML_LRM_ATTR_INTERVAL_MS, &interval_ms);
+    }
+    if (task == NULL) { // Assume start if no history is available
+        task = RSC_START;
+        interval_ms = 0;
+    }
+
+    // Calculate and show digests
+    digests = pe__calculate_digests(rsc, task, &interval_ms, node, xml_op,
+                                    overrides, true, data_set);
+    rc = out->message(out, "digests", rsc, node, task, interval_ms, digests);
+
+    pe__free_digests(digests);
+    return rc;
+}
+
+int
+pcmk_resource_digests(xmlNodePtr *xml, pe_resource_t *rsc,
+                      pe_node_t *node, GHashTable *overrides,
+                      pe_working_set_t *data_set)
+{
+    pcmk__output_t *out = NULL;
+    int rc = pcmk_rc_ok;
+
+    rc = pcmk__out_prologue(&out, xml);
+    if (rc != pcmk_rc_ok) {
+        return rc;
+    }
+    pcmk__register_lib_messages(out);
+    rc = pcmk__resource_digests(out, rsc, node, overrides, data_set);
+    pcmk__out_epilogue(out, xml, rc);
+    return rc;
+}
diff --git a/xml/Makefile.am b/xml/Makefile.am
index e7b9a51..cb6cfa0 100644
--- a/xml/Makefile.am
+++ b/xml/Makefile.am
@@ -1,5 +1,5 @@
 #
-# Copyright 2004-2019 the Pacemaker project contributors
+# Copyright 2004-2021 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
@@ -50,7 +50,13 @@ version_pairs_last = $(wordlist \
 # problems.
 
 # Names of API schemas that form the choices for pacemaker-result content
-API_request_base	= command-output crm_mon crm_resource crmadmin stonith_admin version
+API_request_base	= command-output	\
+			  crm_mon		\
+			  crm_resource		\
+			  crmadmin		\
+			  digests		\
+			  stonith_admin		\
+			  version
 
 # Names of CIB schemas that form the choices for cib/configuration content
 CIB_cfg_base		= options nodes resources constraints fencing acls tags alerts
diff --git a/xml/api/digests-2.6.rng b/xml/api/digests-2.6.rng
new file mode 100644
index 0000000..7e843d4
--- /dev/null
+++ b/xml/api/digests-2.6.rng
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0"
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+
+    <start>
+        <ref name="element-digests"/>
+    </start>
+
+    <define name="element-digests">
+        <attribute name="resource"> <text/> </attribute>
+        <attribute name="node"> <text/> </attribute>
+        <attribute name="task"> <text/> </attribute>
+        <attribute name="interval"> <text/> </attribute>
+        <zeroOrMore>
+            <ref name="element-digest"/>
+        </zeroOrMore>
+    </define>
+
+    <define name="element-digest">
+        <attribute name="type"> <text/> </attribute>
+        <attribute name="hash"> <text/> </attribute>
+        <optional>
+            <element name="parameters">
+                <zeroOrMore>
+                    <attribute>
+                      <anyName/>
+                      <text/>
+                    </attribute>
+                </zeroOrMore>
+            </element>
+        </optional>
+    </define>
+</grammar>
-- 
1.8.3.1


From 4e726eb67c8eed255ee83706ed13cd7ea31f9864 Mon Sep 17 00:00:00 2001
From: Ken Gaillot <kgaillot@redhat.com>
Date: Mon, 14 Sep 2020 16:29:41 -0500
Subject: [PATCH 5/6] Feature: tools: add crm_resource --digests option

This is not particularly useful for end users but can help during development,
and can be used by higher-level tools to bypass Pacemaker's configuration
change detection (with obvious risks).
---
 tools/crm_resource.c | 39 ++++++++++++++++++++++++++++++++++++++-
 1 file changed, 38 insertions(+), 1 deletion(-)

diff --git a/tools/crm_resource.c b/tools/crm_resource.c
index 2717a62..8c7247a 100644
--- a/tools/crm_resource.c
+++ b/tools/crm_resource.c
@@ -40,6 +40,7 @@ enum rsc_command {
     cmd_cts,
     cmd_delete,
     cmd_delete_param,
+    cmd_digests,
     cmd_execute_agent,
     cmd_fail,
     cmd_get_param,
@@ -158,6 +159,8 @@ gboolean validate_or_force_cb(const gchar *option_name, const gchar *optarg,
                               gpointer data, GError **error);
 gboolean restart_cb(const gchar *option_name, const gchar *optarg,
                     gpointer data, GError **error);
+gboolean digests_cb(const gchar *option_name, const gchar *optarg,
+                    gpointer data, GError **error);
 gboolean wait_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean why_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 
@@ -507,6 +510,14 @@ static GOptionEntry advanced_entries[] = {
     { "wait", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, wait_cb,
       "(Advanced) Wait until the cluster settles into a stable state",
       NULL },
+    { "digests", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, digests_cb,
+      "(Advanced) Show parameter hashes that Pacemaker uses to detect\n"
+      INDENT "configuration changes (only accurate if there is resource\n"
+      INDENT "history on the specified node). Required: --resource, --node.\n"
+      INDENT "Optional: any NAME=VALUE parameters will be used to override\n"
+      INDENT "the configuration (to see what the hash would be with those\n"
+      INDENT "changes).",
+      NULL },
     { "force-demote", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
       validate_or_force_cb,
       "(Advanced) Bypass the cluster and demote a resource on the local\n"
@@ -893,7 +904,9 @@ validate_or_force_cb(const gchar *option_name, const gchar *optarg,
     }
     options.operation = g_strdup(option_name + 2); // skip "--"
     options.find_flags = pe_find_renamed|pe_find_anon;
-    options.override_params = crm_str_table_new();
+    if (options.override_params == NULL) {
+        options.override_params = crm_str_table_new();
+    }
     return TRUE;
 }
 
@@ -907,6 +920,20 @@ restart_cb(const gchar *option_name, const gchar *optarg, gpointer data,
 }
 
 gboolean
+digests_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+           GError **error)
+{
+    SET_COMMAND(cmd_digests);
+    options.find_flags = pe_find_renamed|pe_find_anon;
+    if (options.override_params == NULL) {
+        options.override_params = crm_str_table_new();
+    }
+    options.require_node = TRUE;
+    options.require_dataset = TRUE;
+    return TRUE;
+}
+
+gboolean
 wait_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     SET_COMMAND(cmd_wait);
     options.require_resource = FALSE;
@@ -1819,6 +1846,16 @@ main(int argc, char **argv)
             }
             break;
 
+        case cmd_digests:
+            node = pe_find_node(data_set->nodes, options.host_uname);
+            if (node == NULL) {
+                rc = pcmk_rc_node_unknown;
+            } else {
+                rc = pcmk__resource_digests(out, rsc, node,
+                                            options.override_params, data_set);
+            }
+            break;
+
         case cmd_colocations:
             rc = out->message(out, "stacks-constraints", rsc, data_set, false);
             break;
-- 
1.8.3.1


From bb34d07013bed2e71ac9cedb4b1631ad5e2825bf Mon Sep 17 00:00:00 2001
From: Ken Gaillot <kgaillot@redhat.com>
Date: Mon, 23 Nov 2020 12:17:31 -0600
Subject: [PATCH 6/6] Test: cts-cli: add regression tests for crm_resource
 --digests

---
 cts/Makefile.am                  |   3 +-
 cts/cli/crm_resource_digests.xml | 143 +++++++++++++++++++++++++++++++++++++++
 cts/cli/regression.tools.exp     |  34 ++++++++++
 cts/cts-cli.in                   |  14 +++-
 4 files changed, 192 insertions(+), 2 deletions(-)
 create mode 100644 cts/cli/crm_resource_digests.xml

diff --git a/cts/Makefile.am b/cts/Makefile.am
index 5666a9f..de02aed 100644
--- a/cts/Makefile.am
+++ b/cts/Makefile.am
@@ -1,5 +1,5 @@
 #
-# Copyright 2001-2019 the Pacemaker project contributors
+# Copyright 2001-2021 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
@@ -65,6 +65,7 @@ dist_cli_DATA	= cli/constraints.xml \
 		  cli/crm_diff_old.xml		\
 		  cli/crm_mon.xml		\
 		  cli/crm_mon-partial.xml	\
+		  cli/crm_resource_digests.xml	\
 		  cli/regression.acls.exp	\
 		  cli/regression.crm_mon.exp	\
 		  cli/regression.dates.exp	\
diff --git a/cts/cli/crm_resource_digests.xml b/cts/cli/crm_resource_digests.xml
new file mode 100644
index 0000000..074ca3d
--- /dev/null
+++ b/cts/cli/crm_resource_digests.xml
@@ -0,0 +1,143 @@
+<cib crm_feature_set="3.6.3" validate-with="pacemaker-3.0" epoch="253" num_updates="20" admin_epoch="0" cib-last-written="Sun Nov 22 14:45:16 2020" update-origin="node2" update-client="cibadmin" update-user="root" have-quorum="1" dc-uuid="1">
+  <configuration>
+    <crm_config>
+      <cluster_property_set id="cib-bootstrap-options">
+        <nvpair id="cts-stonith-enabled" name="stonith-enabled" value="1"/>
+        <nvpair id="cib-bootstrap-options-have-watchdog" name="have-watchdog" value="false"/>
+        <nvpair id="cib-bootstrap-options-dc-version" name="dc-version" value="2.0.5"/>
+        <nvpair id="cib-bootstrap-options-cluster-infrastructure" name="cluster-infrastructure" value="corosync"/>
+      </cluster_property_set>
+    </crm_config>
+    <nodes>
+      <node id="1" uname="node1"/>
+      <node id="2" uname="node2"/>
+      <node id="3" uname="node3"/>
+      <node id="4" uname="node4"/>
+      <node id="5" uname="node5"/>
+    </nodes>
+    <resources>
+      <primitive class="stonith" id="Fencing" type="fence_xvm">
+        <meta_attributes id="Fencing-meta">
+          <nvpair id="Fencing-migration-threshold" name="migration-threshold" value="5"/>
+        </meta_attributes>
+        <instance_attributes id="Fencing-params">
+          <nvpair id="Fencing-key_file" name="key_file" value="/etc/pacemaker/fence_xvm.key"/>
+          <nvpair id="Fencing-multicast_address" name="multicast_address" value="239.255.100.100"/>
+          <nvpair id="Fencing-pcmk_host_list" name="pcmk_host_list" value="node1 node2 node3 node4 node5"/>
+        </instance_attributes>
+        <operations>
+          <op id="Fencing-monitor-120s" interval="120s" name="monitor" timeout="120s"/>
+          <op id="Fencing-stop-0" interval="0" name="stop" timeout="60s"/>
+          <op id="Fencing-start-0" interval="0" name="start" timeout="60s"/>
+        </operations>
+      </primitive>
+      <primitive class="ocf" id="rsc1" provider="pacemaker" type="Dummy">
+        <instance_attributes id="rsc1-instance_attributes">
+          <nvpair id="rsc1-instance_attributes-fake" name="fake" value="1"/>
+          <nvpair id="rsc1-instance_attributes-passwd" name="passwd" value="secret"/>
+        </instance_attributes>
+        <instance_attributes id="rsc1-instance_attributes-node1">
+          <nvpair id="rsc1-instance_attributes-fake-node1" name="fake" value="0"/>
+          <rule id="rsc1-rule1" score="INFINITY">
+            <expression attribute="#uname" id="rsc1-rule1-expr1" operation="eq" value="node1"/>
+          </rule>
+        </instance_attributes>
+        <operations>
+          <op id="rsc1-migrate_from-interval-0s" interval="0s" name="migrate_from" timeout="20s"/>
+          <op id="rsc1-migrate_to-interval-0s" interval="0s" name="migrate_to" timeout="20s"/>
+          <op id="rsc1-monitor-interval-10s" interval="10s" name="monitor" timeout="20s"/>
+          <op id="rsc1-reload-interval-0s" interval="0s" name="reload" timeout="20s"/>
+          <op id="rsc1-start-interval-0s" interval="0s" name="start" timeout="20s"/>
+          <op id="rsc1-stop-interval-0s" interval="0s" name="stop" timeout="20s"/>
+        </operations>
+      </primitive>
+    </resources>
+    <constraints>
+      <rsc_location id="location-rsc1-node1-INFINITY" node="node1" rsc="rsc1" score="INFINITY"/>
+    </constraints>
+    <fencing-topology/>
+    <op_defaults/>
+    <alerts/>
+    <rsc_defaults/>
+  </configuration>
+  <status>
+    <node_state id="4" uname="node4" in_ccm="true" crmd="online" crm-debug-origin="do_state_transition" join="member" expected="member">
+      <transient_attributes id="4">
+        <instance_attributes id="status-4"/>
+      </transient_attributes>
+      <lrm id="4">
+        <lrm_resources>
+          <lrm_resource id="rsc1" type="Dummy" class="ocf" provider="pacemaker">
+            <lrm_rsc_op id="rsc1_last_0" operation_key="rsc1_monitor_0" operation="monitor" crm-debug-origin="build_active_RAs" crm_feature_set="3.6.3" transition-key="5:51:7:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:7;5:51:7:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node4" call-id="136" rc-code="7" op-status="0" interval="0" last-rc-change="1606076573" last-run="1606076573" exec-time="28" queue-time="0" op-digest="2b1b5ccbabbdb96f3f7edb41b0775563" op-force-restart="  envfile op_sleep passwd state  " op-restart-digest="f2317cad3d54cec5d7d7aa7d0bf35cf8" op-secure-params="  passwd  " op-secure-digest="2b1b5ccbabbdb96f3f7edb41b0775563"/>
+          </lrm_resource>
+          <lrm_resource id="Fencing" type="fence_xvm" class="stonith">
+            <lrm_rsc_op id="Fencing_last_0" operation_key="Fencing_monitor_0" operation="monitor" crm-debug-origin="build_active_RAs" crm_feature_set="3.6.3" transition-key="43:0:7:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:7;43:0:7:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node4" call-id="5" rc-code="7" op-status="0" interval="0" last-rc-change="1606076227" last-run="1606076227" exec-time="2" queue-time="0" op-digest="52e34745a77d95a636428d3b550eb867"/>
+          </lrm_resource>
+        </lrm_resources>
+      </lrm>
+    </node_state>
+    <node_state id="2" uname="node2" in_ccm="true" crmd="online" crm-debug-origin="do_state_transition" join="member" expected="member">
+      <transient_attributes id="2">
+        <instance_attributes id="status-2"/>
+      </transient_attributes>
+      <lrm id="2">
+        <lrm_resources>
+          <lrm_resource id="rsc1" type="Dummy" class="ocf" provider="pacemaker">
+            <lrm_rsc_op id="rsc1_last_0" operation_key="rsc1_monitor_0" operation="monitor" crm-debug-origin="build_active_RAs" crm_feature_set="3.6.3" transition-key="3:51:7:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:7;3:51:7:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node2" call-id="101" rc-code="7" op-status="0" interval="0" last-rc-change="1606076573" last-run="1606076573" exec-time="45" queue-time="0" op-digest="2b1b5ccbabbdb96f3f7edb41b0775563" op-force-restart="  envfile op_sleep passwd state  " op-restart-digest="f2317cad3d54cec5d7d7aa7d0bf35cf8" op-secure-params="  passwd  " op-secure-digest="2b1b5ccbabbdb96f3f7edb41b0775563"/>
+          </lrm_resource>
+          <lrm_resource id="Fencing" type="fence_xvm" class="stonith">
+            <lrm_rsc_op id="Fencing_last_0" operation_key="Fencing_monitor_0" operation="monitor" crm-debug-origin="build_active_RAs" crm_feature_set="3.6.3" transition-key="15:0:7:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:7;15:0:7:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node2" call-id="5" rc-code="7" op-status="0" interval="0" last-rc-change="1606076227" last-run="1606076227" exec-time="4" queue-time="0" op-digest="52e34745a77d95a636428d3b550eb867"/>
+          </lrm_resource>
+        </lrm_resources>
+      </lrm>
+    </node_state>
+    <node_state id="3" uname="node3" in_ccm="true" crmd="online" crm-debug-origin="do_update_resource" join="member" expected="member">
+      <transient_attributes id="3">
+        <instance_attributes id="status-3"/>
+      </transient_attributes>
+      <lrm id="3">
+        <lrm_resources>
+          <lrm_resource id="rsc1" type="Dummy" class="ocf" provider="pacemaker">
+            <lrm_rsc_op id="rsc1_last_0" operation_key="rsc1_stop_0" operation="stop" crm-debug-origin="do_update_resource" crm_feature_set="3.6.3" transition-key="7:55:0:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:0;7:55:0:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node3" call-id="121" rc-code="0" op-status="0" interval="0" last-rc-change="1606077916" last-run="1606077916" exec-time="26" queue-time="0" op-digest="c18bfacc816dc3a5a53f23c000e6e57e" op-force-restart="  envfile op_sleep passwd state  " op-restart-digest="5de1fd72a2e7762ed41543231034f6d7" op-secure-params="  passwd  " op-secure-digest="2b1b5ccbabbdb96f3f7edb41b0775563"/>
+            <lrm_rsc_op id="rsc1_monitor_10000" operation_key="rsc1_monitor_10000" operation="monitor" crm-debug-origin="build_active_RAs" crm_feature_set="3.6.3" transition-key="1:52:0:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:0;1:52:0:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node3" call-id="118" rc-code="0" op-status="0" interval="10000" last-rc-change="1606076598" exec-time="20" queue-time="0" op-digest="0b73673404cb867681a3c190ccebcc51" op-secure-params="  passwd  " op-secure-digest="2b1b5ccbabbdb96f3f7edb41b0775563"/>
+          </lrm_resource>
+          <lrm_resource id="Fencing" type="fence_xvm" class="stonith">
+            <lrm_rsc_op id="Fencing_last_0" operation_key="Fencing_monitor_0" operation="monitor" crm-debug-origin="build_active_RAs" crm_feature_set="3.6.3" transition-key="29:0:7:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:7;29:0:7:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node3" call-id="5" rc-code="7" op-status="0" interval="0" last-rc-change="1606076227" last-run="1606076227" exec-time="24" queue-time="0" op-digest="52e34745a77d95a636428d3b550eb867"/>
+          </lrm_resource>
+        </lrm_resources>
+      </lrm>
+    </node_state>
+    <node_state id="5" uname="node5" in_ccm="true" crmd="online" crm-debug-origin="do_state_transition" join="member" expected="member">
+      <transient_attributes id="5">
+        <instance_attributes id="status-5"/>
+      </transient_attributes>
+      <lrm id="5">
+        <lrm_resources>
+          <lrm_resource id="rsc1" type="Dummy" class="ocf" provider="pacemaker">
+            <lrm_rsc_op id="rsc1_last_0" operation_key="rsc1_monitor_0" operation="monitor" crm-debug-origin="build_active_RAs" crm_feature_set="3.6.3" transition-key="6:51:7:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:7;6:51:7:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node5" call-id="99" rc-code="7" op-status="0" interval="0" last-rc-change="1606076573" last-run="1606076573" exec-time="27" queue-time="0" op-digest="2b1b5ccbabbdb96f3f7edb41b0775563" op-force-restart="  envfile op_sleep passwd state  " op-restart-digest="f2317cad3d54cec5d7d7aa7d0bf35cf8" op-secure-params="  passwd  " op-secure-digest="2b1b5ccbabbdb96f3f7edb41b0775563"/>
+          </lrm_resource>
+          <lrm_resource id="Fencing" type="fence_xvm" class="stonith">
+            <lrm_rsc_op id="Fencing_last_0" operation_key="Fencing_monitor_0" operation="monitor" crm-debug-origin="build_active_RAs" crm_feature_set="3.6.3" transition-key="57:0:7:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:7;57:0:7:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node5" call-id="5" rc-code="7" op-status="0" interval="0" last-rc-change="1606076227" last-run="1606076227" exec-time="14" queue-time="0" op-digest="52e34745a77d95a636428d3b550eb867"/>
+          </lrm_resource>
+        </lrm_resources>
+      </lrm>
+    </node_state>
+    <node_state id="1" uname="node1" in_ccm="true" crmd="online" crm-debug-origin="do_update_resource" join="member" expected="member">
+      <transient_attributes id="1">
+        <instance_attributes id="status-1"/>
+      </transient_attributes>
+      <lrm id="1">
+        <lrm_resources>
+          <lrm_resource id="rsc1" type="Dummy" class="ocf" provider="pacemaker">
+            <lrm_rsc_op id="rsc1_last_0" operation_key="rsc1_start_0" operation="start" crm-debug-origin="do_update_resource" crm_feature_set="3.6.3" transition-key="8:55:0:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:0;8:55:0:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node1" call-id="104" rc-code="0" op-status="0" interval="0" last-rc-change="1606077916" last-run="1606077916" exec-time="22" queue-time="0" op-digest="3acdbe4c12734ebeb1251a59545af936" op-force-restart="  envfile op_sleep passwd state  " op-restart-digest="5de1fd72a2e7762ed41543231034f6d7" op-secure-params="  passwd  " op-secure-digest="279c477dbc38c621904a00ab9e599b2f"/>
+            <lrm_rsc_op id="rsc1_monitor_10000" operation_key="rsc1_monitor_10000" operation="monitor" crm-debug-origin="do_update_resource" crm_feature_set="3.6.3" transition-key="9:55:0:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:0;9:55:0:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node1" call-id="106" rc-code="0" op-status="0" interval="10000" last-rc-change="1606077916" exec-time="20" queue-time="0" op-digest="720718e8d715d5d3be1403cbbcb953bc" op-secure-params="  passwd  " op-secure-digest="279c477dbc38c621904a00ab9e599b2f"/>
+          </lrm_resource>
+          <lrm_resource id="Fencing" type="fence_xvm" class="stonith">
+            <lrm_rsc_op id="Fencing_last_0" operation_key="Fencing_start_0" operation="start" crm-debug-origin="build_active_RAs" crm_feature_set="3.6.3" transition-key="71:0:0:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:0;71:0:0:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node1" call-id="10" rc-code="0" op-status="0" interval="0" last-rc-change="1606076227" last-run="1606076227" exec-time="59" queue-time="0" op-digest="52e34745a77d95a636428d3b550eb867"/>
+            <lrm_rsc_op id="Fencing_monitor_120000" operation_key="Fencing_monitor_120000" operation="monitor" crm-debug-origin="build_active_RAs" crm_feature_set="3.6.3" transition-key="72:0:0:727e4004-8c04-423b-8d63-65ae1fabd119" transition-magic="0:0;72:0:0:727e4004-8c04-423b-8d63-65ae1fabd119" exit-reason="" on_node="node1" call-id="12" rc-code="0" op-status="0" interval="120000" last-rc-change="1606076227" exec-time="70" queue-time="0" op-digest="acc6dd2c58c637db4d12a6fe35626617"/>
+          </lrm_resource>
+        </lrm_resources>
+      </lrm>
+    </node_state>
+  </status>
+</cib>
diff --git a/cts/cli/regression.tools.exp b/cts/cli/regression.tools.exp
index a85b7d6..510cc0a 100644
--- a/cts/cli/regression.tools.exp
+++ b/cts/cli/regression.tools.exp
@@ -4019,3 +4019,37 @@ Resources colocated with clone:
 </pacemaker-result>
 =#=#=#= End test: Recursively check locations and constraints for clone in XML - OK (0) =#=#=#=
 * Passed: crm_resource   - Recursively check locations and constraints for clone in XML
+=#=#=#= Begin test: Show resource digests =#=#=#=
+<pacemaker-result api-version="X" request="crm_resource --digests -r rsc1 -N node1 --output-as=xml">
+  <digests resource="rsc1" node="node1" task="start" interval="0ms">
+    <digest type="all" hash="3acdbe4c12734ebeb1251a59545af936">
+      <parameters passwd="secret" fake="0"/>
+    </digest>
+    <digest type="nonprivate" hash="279c477dbc38c621904a00ab9e599b2f">
+      <parameters fake="0"/>
+    </digest>
+    <digest type="nonreloadable" hash="5de1fd72a2e7762ed41543231034f6d7">
+      <parameters passwd="secret"/>
+    </digest>
+  </digests>
+  <status code="0" message="OK"/>
+</pacemaker-result>
+=#=#=#= End test: Show resource digests - OK (0) =#=#=#=
+* Passed: crm_resource   - Show resource digests
+=#=#=#= Begin test: Show resource digests with overrides =#=#=#=
+<pacemaker-result api-version="X" request="crm_resource --digests -r rsc1 -N node1 --output-as=xml CRM_meta_interval=10000 CRM_meta_timeout=20000">
+  <digests resource="rsc1" node="node1" task="start" interval="10000ms">
+    <digest type="all" hash="720718e8d715d5d3be1403cbbcb953bc">
+      <parameters passwd="secret" fake="0" CRM_meta_timeout="20000"/>
+    </digest>
+    <digest type="nonprivate" hash="279c477dbc38c621904a00ab9e599b2f">
+      <parameters fake="0"/>
+    </digest>
+    <digest type="nonreloadable" hash="5de1fd72a2e7762ed41543231034f6d7">
+      <parameters passwd="secret"/>
+    </digest>
+  </digests>
+  <status code="0" message="OK"/>
+</pacemaker-result>
+=#=#=#= End test: Show resource digests with overrides - OK (0) =#=#=#=
+* Passed: crm_resource   - Show resource digests with overrides
diff --git a/cts/cts-cli.in b/cts/cts-cli.in
index dfdd3de..96f5386 100755
--- a/cts/cts-cli.in
+++ b/cts/cts-cli.in
@@ -1,6 +1,6 @@
 #!@BASH_PATH@
 #
-# Copyright 2008-2020 the Pacemaker project contributors
+# Copyright 2008-2021 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
@@ -791,6 +791,18 @@ function test_tools() {
     done
 
     unset CIB_file
+
+    export CIB_file="$test_home/cli/crm_resource_digests.xml"
+
+    desc="Show resource digests"
+    cmd="crm_resource --digests -r rsc1 -N node1 --output-as=xml"
+    test_assert $CRM_EX_OK 0
+
+    desc="Show resource digests with overrides"
+    cmd="$cmd CRM_meta_interval=10000 CRM_meta_timeout=20000"
+    test_assert $CRM_EX_OK 0
+
+    unset CIB_file
 }
 
 INVALID_PERIODS=(
-- 
1.8.3.1