summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 06:53:20 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 06:53:20 +0000
commite5a812082ae033afb1eed82c0f2df3d0f6bdc93f (patch)
treea6716c9275b4b413f6c9194798b34b91affb3cc7 /tools
parentInitial commit. (diff)
downloadpacemaker-e5a812082ae033afb1eed82c0f2df3d0f6bdc93f.tar.xz
pacemaker-e5a812082ae033afb1eed82c0f2df3d0f6bdc93f.zip
Adding upstream version 2.1.6.upstream/2.1.6
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tools')
-rw-r--r--tools/Makefile.am165
-rw-r--r--tools/attrd_updater.8.inc5
-rw-r--r--tools/attrd_updater.c520
-rw-r--r--tools/cibadmin.8.inc5
-rw-r--r--tools/cibadmin.c954
-rw-r--r--tools/cibsecret.in440
-rwxr-xr-xtools/cluster-clean.in99
-rwxr-xr-xtools/cluster-helper.in201
-rwxr-xr-xtools/cluster-init.in537
-rw-r--r--tools/crm_attribute.8.inc5
-rw-r--r--tools/crm_attribute.c883
-rw-r--r--tools/crm_diff.8.inc5
-rw-r--r--tools/crm_diff.c399
-rw-r--r--tools/crm_error.8.inc5
-rw-r--r--tools/crm_error.c175
-rwxr-xr-xtools/crm_failcount.in294
-rwxr-xr-xtools/crm_master.in92
-rw-r--r--tools/crm_mon.8.inc17
-rw-r--r--tools/crm_mon.c2287
-rw-r--r--tools/crm_mon.h78
-rw-r--r--tools/crm_mon.service.in17
-rw-r--r--tools/crm_mon.upstart.in35
-rw-r--r--tools/crm_mon_curses.c490
-rw-r--r--tools/crm_node.8.inc5
-rw-r--r--tools/crm_node.c601
-rw-r--r--tools/crm_report.in481
-rw-r--r--tools/crm_resource.8.inc5
-rw-r--r--tools/crm_resource.c2182
-rw-r--r--tools/crm_resource.h121
-rw-r--r--tools/crm_resource_ban.c505
-rw-r--r--tools/crm_resource_print.c818
-rw-r--r--tools/crm_resource_runtime.c2178
-rw-r--r--tools/crm_rule.8.inc8
-rw-r--r--tools/crm_rule.c227
-rw-r--r--tools/crm_shadow.8.inc5
-rw-r--r--tools/crm_shadow.c1322
-rw-r--r--tools/crm_simulate.8.inc8
-rw-r--r--tools/crm_simulate.c587
-rwxr-xr-xtools/crm_standby.in158
-rw-r--r--tools/crm_ticket.8.inc5
-rw-r--r--tools/crm_ticket.c1007
-rw-r--r--tools/crm_verify.8.inc5
-rw-r--r--tools/crm_verify.c285
-rw-r--r--tools/crmadmin.8.inc5
-rw-r--r--tools/crmadmin.c275
-rw-r--r--tools/fix-manpages33
-rw-r--r--tools/iso8601.8.inc5
-rw-r--r--tools/iso8601.c282
-rw-r--r--tools/pcmk_simtimes.in159
-rw-r--r--tools/report.collector.in885
-rw-r--r--tools/report.common.in890
-rw-r--r--tools/stonith_admin.8.inc5
-rw-r--r--tools/stonith_admin.c674
53 files changed, 21434 insertions, 0 deletions
diff --git a/tools/Makefile.am b/tools/Makefile.am
new file mode 100644
index 0000000..36bd3ae
--- /dev/null
+++ b/tools/Makefile.am
@@ -0,0 +1,165 @@
+#
+# Copyright 2004-2023 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 $(top_srcdir)/mk/common.mk
+include $(top_srcdir)/mk/man.mk
+
+if BUILD_SYSTEMD
+systemdsystemunit_DATA = crm_mon.service
+endif
+
+noinst_HEADERS = crm_mon.h crm_resource.h
+
+pcmkdir = $(datadir)/$(PACKAGE)
+pcmk_DATA = report.common report.collector
+
+sbin_SCRIPTS = crm_report crm_standby crm_master crm_failcount
+if BUILD_CIBSECRETS
+sbin_SCRIPTS += cibsecret
+endif
+noinst_SCRIPTS = cluster-clean \
+ cluster-init \
+ cluster-helper \
+ pcmk_simtimes
+
+EXTRA_DIST = attrd_updater.8.inc \
+ cibadmin.8.inc \
+ crm_attribute.8.inc \
+ crm_diff.8.inc \
+ crm_error.8.inc \
+ crm_mon.8.inc \
+ crm_node.8.inc \
+ crm_resource.8.inc \
+ crm_rule.8.inc \
+ crm_shadow.8.inc \
+ crm_simulate.8.inc \
+ crm_ticket.8.inc \
+ crm_verify.8.inc \
+ crmadmin.8.inc \
+ fix-manpages \
+ iso8601.8.inc \
+ stonith_admin.8.inc
+
+sbin_PROGRAMS = attrd_updater \
+ cibadmin \
+ crmadmin \
+ crm_simulate \
+ crm_attribute \
+ crm_diff \
+ crm_error \
+ crm_mon \
+ crm_node \
+ crm_resource \
+ crm_rule \
+ crm_shadow \
+ crm_verify \
+ crm_ticket \
+ iso8601 \
+ stonith_admin
+
+## SOURCES
+
+# A few tools are just thin wrappers around crm_attribute.
+# This makes their help get updated when crm_attribute changes
+# (see mk/common.mk).
+MAN8DEPS = crm_attribute
+
+crmadmin_SOURCES = crmadmin.c
+crmadmin_LDADD = $(top_builddir)/lib/pengine/libpe_status.la \
+ $(top_builddir)/lib/cib/libcib.la \
+ $(top_builddir)/lib/common/libcrmcommon.la \
+ $(top_builddir)/lib/pacemaker/libpacemaker.la
+
+crm_error_SOURCES = crm_error.c
+crm_error_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la \
+ $(top_builddir)/lib/common/libcrmcommon.la
+
+cibadmin_SOURCES = cibadmin.c
+cibadmin_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la \
+ $(top_builddir)/lib/cib/libcib.la \
+ $(top_builddir)/lib/common/libcrmcommon.la
+
+crm_shadow_SOURCES = crm_shadow.c
+crm_shadow_LDADD = $(top_builddir)/lib/cib/libcib.la \
+ $(top_builddir)/lib/common/libcrmcommon.la
+
+crm_node_SOURCES = crm_node.c
+crm_node_LDADD = $(top_builddir)/lib/cib/libcib.la \
+ $(top_builddir)/lib/common/libcrmcommon.la
+
+crm_simulate_SOURCES = crm_simulate.c
+
+crm_simulate_LDADD = $(top_builddir)/lib/pengine/libpe_status.la \
+ $(top_builddir)/lib/pacemaker/libpacemaker.la \
+ $(top_builddir)/lib/cib/libcib.la \
+ $(top_builddir)/lib/common/libcrmcommon.la
+
+crm_diff_SOURCES = crm_diff.c
+crm_diff_LDADD = $(top_builddir)/lib/common/libcrmcommon.la
+
+crm_mon_SOURCES = crm_mon.c crm_mon_curses.c
+crm_mon_LDADD = $(top_builddir)/lib/pengine/libpe_status.la \
+ $(top_builddir)/lib/fencing/libstonithd.la \
+ $(top_builddir)/lib/pacemaker/libpacemaker.la \
+ $(top_builddir)/lib/cib/libcib.la \
+ $(top_builddir)/lib/common/libcrmcommon.la \
+ $(CURSESLIBS)
+
+crm_verify_SOURCES = crm_verify.c
+crm_verify_LDADD = $(top_builddir)/lib/pengine/libpe_status.la \
+ $(top_builddir)/lib/pacemaker/libpacemaker.la \
+ $(top_builddir)/lib/cib/libcib.la \
+ $(top_builddir)/lib/common/libcrmcommon.la
+
+crm_attribute_SOURCES = crm_attribute.c
+crm_attribute_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la \
+ $(top_builddir)/lib/cib/libcib.la \
+ $(top_builddir)/lib/common/libcrmcommon.la
+
+crm_resource_SOURCES = crm_resource.c \
+ crm_resource_ban.c \
+ crm_resource_print.c \
+ crm_resource_runtime.c
+crm_resource_LDADD = $(top_builddir)/lib/pengine/libpe_rules.la \
+ $(top_builddir)/lib/fencing/libstonithd.la \
+ $(top_builddir)/lib/lrmd/liblrmd.la \
+ $(top_builddir)/lib/services/libcrmservice.la \
+ $(top_builddir)/lib/pengine/libpe_status.la \
+ $(top_builddir)/lib/pacemaker/libpacemaker.la \
+ $(top_builddir)/lib/cib/libcib.la \
+ $(top_builddir)/lib/common/libcrmcommon.la
+
+crm_rule_SOURCES = crm_rule.c
+crm_rule_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la \
+ $(top_builddir)/lib/cib/libcib.la \
+ $(top_builddir)/lib/pengine/libpe_rules.la \
+ $(top_builddir)/lib/pengine/libpe_status.la \
+ $(top_builddir)/lib/common/libcrmcommon.la
+
+iso8601_SOURCES = iso8601.c
+iso8601_LDADD = $(top_builddir)/lib/common/libcrmcommon.la
+
+attrd_updater_SOURCES = attrd_updater.c
+attrd_updater_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la \
+ $(top_builddir)/lib/common/libcrmcommon.la
+
+crm_ticket_SOURCES = crm_ticket.c
+crm_ticket_LDADD = $(top_builddir)/lib/pengine/libpe_rules.la \
+ $(top_builddir)/lib/pengine/libpe_status.la \
+ $(top_builddir)/lib/pacemaker/libpacemaker.la \
+ $(top_builddir)/lib/cib/libcib.la \
+ $(top_builddir)/lib/common/libcrmcommon.la
+
+stonith_admin_SOURCES = stonith_admin.c
+stonith_admin_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la \
+ $(top_builddir)/lib/cib/libcib.la \
+ $(top_builddir)/lib/pengine/libpe_status.la \
+ $(top_builddir)/lib/fencing/libstonithd.la \
+ $(top_builddir)/lib/common/libcrmcommon.la
+
+CLEANFILES = $(man8_MANS)
diff --git a/tools/attrd_updater.8.inc b/tools/attrd_updater.8.inc
new file mode 100644
index 0000000..780e6a4
--- /dev/null
+++ b/tools/attrd_updater.8.inc
@@ -0,0 +1,5 @@
+[synopsis]
+attrd_updater {-n <attribute> | -P <pattern>} <command> [options]
+
+/node attributes/
+.SH OPTIONS
diff --git a/tools/attrd_updater.c b/tools/attrd_updater.c
new file mode 100644
index 0000000..60e4cc7
--- /dev/null
+++ b/tools/attrd_updater.c
@@ -0,0 +1,520 @@
+/*
+ * Copyright 2004-2023 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 <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <libgen.h>
+
+#include <sys/param.h>
+#include <sys/types.h>
+
+#include <crm/crm.h>
+#include <crm/msg_xml.h>
+#include <crm/common/ipc_attrd_internal.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
+#include <crm/common/xml_internal.h>
+
+#include <crm/common/attrd_internal.h>
+
+#include <pcmki/pcmki_output.h>
+
+#define SUMMARY "query and update Pacemaker node attributes"
+
+static pcmk__supported_format_t formats[] = {
+ PCMK__SUPPORTED_FORMAT_NONE,
+ PCMK__SUPPORTED_FORMAT_TEXT,
+ PCMK__SUPPORTED_FORMAT_XML,
+ { NULL, NULL, NULL }
+};
+
+GError *error = NULL;
+bool printed_values = false;
+
+struct {
+ char command;
+ gchar *attr_dampen;
+ gchar *attr_name;
+ gchar *attr_pattern;
+ gchar *attr_node;
+ gchar *attr_set;
+ char *attr_value;
+ uint32_t attr_options;
+ gboolean query_all;
+ gboolean quiet;
+} options = {
+ .attr_options = pcmk__node_attr_none,
+ .command = 'Q',
+};
+
+static gboolean
+command_cb (const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ pcmk__str_update(&options.attr_value, optarg);
+
+ if (pcmk__str_any_of(option_name, "--update-both", "-B", NULL)) {
+ options.command = 'B';
+ } else if (pcmk__str_any_of(option_name, "--delete", "-D", NULL)) {
+ options.command = 'D';
+ } else if (pcmk__str_any_of(option_name, "--query", "-Q", NULL)) {
+ options.command = 'Q';
+ } else if (pcmk__str_any_of(option_name, "--refresh", "-R", NULL)) {
+ options.command = 'R';
+ } else if (pcmk__str_any_of(option_name, "--update", "-U", "-v", NULL)) {
+ options.command = 'U';
+ } else if (pcmk__str_any_of(option_name, "--update-delay", "-Y", NULL)) {
+ options.command = 'Y';
+ }
+
+ return TRUE;
+}
+
+static gboolean
+private_cb (const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ pcmk__set_node_attr_flags(options.attr_options, pcmk__node_attr_private);
+ return TRUE;
+}
+
+static gboolean
+section_cb (const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ if (pcmk__str_any_of(optarg, "nodes", "forever", NULL)) {
+ pcmk__set_node_attr_flags(options.attr_options, pcmk__node_attr_perm);
+ } else if (pcmk__str_any_of(optarg, "status", "reboot", NULL)) {
+ pcmk__clear_node_attr_flags(options.attr_options, pcmk__node_attr_perm);
+ } else {
+ g_set_error(err, PCMK__EXITC_ERROR, CRM_EX_USAGE, "Unknown value for --lifetime: %s",
+ optarg);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+attr_set_type_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (pcmk__str_any_of(option_name, "-z", "--utilization", NULL)) {
+ pcmk__set_node_attr_flags(options.attr_options, pcmk__node_attr_utilization);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+wait_cb (const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ if (pcmk__str_eq(optarg, "no", pcmk__str_none)) {
+ pcmk__clear_node_attr_flags(options.attr_options, pcmk__node_attr_sync_local | pcmk__node_attr_sync_cluster);
+ return TRUE;
+ } else if (pcmk__str_eq(optarg, PCMK__VALUE_LOCAL, pcmk__str_none)) {
+ pcmk__clear_node_attr_flags(options.attr_options, pcmk__node_attr_sync_local | pcmk__node_attr_sync_cluster);
+ pcmk__set_node_attr_flags(options.attr_options, pcmk__node_attr_sync_local);
+ return TRUE;
+ } else if (pcmk__str_eq(optarg, PCMK__VALUE_CLUSTER, pcmk__str_none)) {
+ pcmk__clear_node_attr_flags(options.attr_options, pcmk__node_attr_sync_local | pcmk__node_attr_sync_cluster);
+ pcmk__set_node_attr_flags(options.attr_options, pcmk__node_attr_sync_cluster);
+ return TRUE;
+ } else {
+ g_set_error(err, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ "--wait= must be one of 'no', 'local', 'cluster'");
+ return FALSE;
+ }
+}
+
+#define INDENT " "
+
+static GOptionEntry required_entries[] = {
+ { "name", 'n', 0, G_OPTION_ARG_STRING, &options.attr_name,
+ "The attribute's name",
+ "NAME" },
+
+ { "pattern", 'P', 0, G_OPTION_ARG_STRING, &options.attr_pattern,
+ "Operate on all attributes matching this pattern\n"
+ INDENT "(with -B, -D, -U, or -Y)",
+ "PATTERN"
+ },
+
+ { NULL }
+};
+
+static GOptionEntry command_entries[] = {
+ { "update", 'U', 0, G_OPTION_ARG_CALLBACK, command_cb,
+ "Update attribute's value in pacemaker-attrd. If this causes the value\n"
+ INDENT "to change, it will also be updated in the cluster configuration.",
+ "VALUE" },
+
+ { "update-both", 'B', 0, G_OPTION_ARG_CALLBACK, command_cb,
+ "Update attribute's value and time to wait (dampening) in\n"
+ INDENT "pacemaker-attrd. If this causes the value or dampening to change,\n"
+ INDENT "the attribute will also be written to the cluster configuration,\n"
+ INDENT "so be aware that repeatedly changing the dampening reduces its\n"
+ INDENT "effectiveness.\n"
+ INDENT "Requires -d/--delay",
+ "VALUE" },
+
+ { "update-delay", 'Y', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Update attribute's dampening in pacemaker-attrd. If this causes\n"
+ INDENT "the dampening to change, the attribute will also be written\n"
+ INDENT "to the cluster configuration, so be aware that repeatedly\n"
+ INDENT "changing the dampening reduces its effectiveness.\n"
+ INDENT "Requires -d/--delay",
+ NULL },
+
+ { "query", 'Q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Query the attribute's value from pacemaker-attrd",
+ NULL },
+
+ { "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Unset attribute from pacemaker-attrd. At the moment, there is no way\n"
+ INDENT "to remove an attribute. This option will instead set its value\n"
+ INDENT "to the empty string.",
+ NULL },
+
+ { "refresh", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "(Advanced) Force the pacemaker-attrd daemon to resend all current\n"
+ INDENT "values to the CIB",
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry addl_entries[] = {
+ { "delay", 'd', 0, G_OPTION_ARG_STRING, &options.attr_dampen,
+ "The time to wait (dampening) in seconds for further changes\n"
+ INDENT "before sending to the CIB",
+ "SECONDS" },
+
+ { "set", 's', 0, G_OPTION_ARG_STRING, &options.attr_set,
+ "(Advanced) The attribute set in which to place the value",
+ "SET" },
+
+ { "node", 'N', 0, G_OPTION_ARG_STRING, &options.attr_node,
+ "Set the attribute for the named node (instead of the local one)",
+ "NODE" },
+
+ { "all", 'A', 0, G_OPTION_ARG_NONE, &options.query_all,
+ "Show values of the attribute for all nodes (query only)",
+ NULL },
+
+ { "lifetime", 'l', 0, G_OPTION_ARG_CALLBACK, section_cb,
+ "(Not yet implemented) Lifetime of the node attribute (silently\n"
+ INDENT "ignored by cluster)",
+ "SECTION" },
+
+ { "private", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, private_cb,
+ "If this creates a new attribute, never write the attribute to CIB",
+ NULL },
+
+ { "wait", 'W', 0, G_OPTION_ARG_CALLBACK, wait_cb,
+ "Wait for some event to occur before returning. Values are 'no' (wait\n"
+ INDENT "only for the attribute daemon to acknowledge the request),\n"
+ INDENT "'local' (wait until the change has propagated to where a local\n"
+ INDENT "query will return the request value, or the value set by a\n"
+ INDENT "later request), or 'cluster' (wait until the change has propagated\n"
+ INDENT "to where a query anywhere on the cluster will return the requested\n"
+ INDENT "value, or the value set by a later request). Default is 'no'.",
+ "UNTIL" },
+
+ { "utilization", 'z', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attr_set_type_cb,
+ "When creating a new attribute, create it as a node utilization attribute\n"
+ INDENT "instead of an instance attribute. If the attribute already exists,\n"
+ INDENT "its existing type (utilization vs. instance) will be used regardless.\n"
+ INDENT "(with -B, -U, -Y)",
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry deprecated_entries[] = {
+ { "quiet", 'q', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.quiet,
+ NULL,
+ NULL },
+
+ { "update", 'v', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, command_cb,
+ NULL,
+ NULL },
+
+ { "section", 'S', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, section_cb,
+ NULL,
+ NULL },
+
+ { NULL }
+};
+
+static int send_attrd_query(pcmk__output_t *out, const char *attr_name, const char *attr_node,
+ gboolean query_all);
+static int send_attrd_update(char command, const char *attr_node, const char *attr_name,
+ const char *attr_value, const char *attr_set,
+ const char *attr_dampen, uint32_t attr_options);
+
+static bool
+pattern_used_correctly(void)
+{
+ /* --pattern can only be used with:
+ * -B (update-both), -D (delete), -U (update), or -Y (update-delay)
+ */
+ return options.command == 'B' || options.command == 'D' || options.command == 'U' || options.command == 'Y';
+}
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
+ GOptionContext *context = NULL;
+
+ context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
+
+ pcmk__add_arg_group(context, "required", "Required Arguments:",
+ "Show required arguments", required_entries);
+ pcmk__add_arg_group(context, "command", "Command:",
+ "Show command options (mutually exclusive)", command_entries);
+ pcmk__add_arg_group(context, "additional", "Additional Options:",
+ "Show additional options", addl_entries);
+ pcmk__add_arg_group(context, "deprecated", "Deprecated Options:",
+ "Show deprecated options", deprecated_entries);
+
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ int rc = pcmk_rc_ok;
+ crm_exit_t exit_code = CRM_EX_OK;
+
+ pcmk__output_t *out = NULL;
+
+ GOptionGroup *output_group = NULL;
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ GOptionContext *context = build_arg_context(args, &output_group);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, "dlnsvBNUS");
+
+ pcmk__register_formats(output_group, formats);
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("attrd_updater", args->verbosity);
+
+ rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
+ if (rc != pcmk_rc_ok) {
+ exit_code = CRM_EX_ERROR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s",
+ args->output_ty, pcmk_rc_str(rc));
+ goto done;
+ }
+
+ if (args->version) {
+ out->version(out, false);
+ goto done;
+ }
+
+ if (options.attr_pattern) {
+ if (options.attr_name) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Error: --name and --pattern cannot be used at the same time");
+ goto done;
+ }
+
+ if (!pattern_used_correctly()) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Error: pattern can only be used with delete or update");
+ goto done;
+ }
+
+ g_free(options.attr_name);
+ options.attr_name = options.attr_pattern;
+ options.attr_options |= pcmk__node_attr_pattern;
+ }
+
+ if (options.command != 'R' && options.attr_name == NULL) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Command requires --name or --pattern argument");
+ goto done;
+ } else if ((options.command == 'B'|| options.command == 'Y') && options.attr_dampen == NULL) {
+ out->info(out, "Warning: '%c' command given without required --delay", options.command);
+ }
+
+ pcmk__register_lib_messages(out);
+
+ if (options.command == 'Q') {
+ int rc = send_attrd_query(out, options.attr_name, options.attr_node, options.query_all);
+ exit_code = pcmk_rc2exitc(rc);
+ } else {
+ /* @TODO We don't know whether the specified node is a Pacemaker Remote
+ * node or not, so we can't set pcmk__node_attr_remote when appropriate.
+ * However, it's not a big problem, because pacemaker-attrd will learn
+ * and remember a node's "remoteness".
+ */
+ int rc = send_attrd_update(options.command, options.attr_node,
+ options.attr_name, options.attr_value,
+ options.attr_set, options.attr_dampen,
+ options.attr_options);
+ exit_code = pcmk_rc2exitc(rc);
+ }
+
+done:
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+ g_free(options.attr_dampen);
+ g_free(options.attr_name);
+ g_free(options.attr_node);
+ g_free(options.attr_set);
+ free(options.attr_value);
+
+ pcmk__output_and_clear_error(&error, out);
+
+ if (out != NULL) {
+ out->finish(out, exit_code, true, NULL);
+ pcmk__output_free(out);
+ }
+
+ pcmk__unregister_formats();
+ crm_exit(exit_code);
+}
+
+/*!
+ * \brief Print the attribute values in a pacemaker-attrd XML query reply
+ *
+ * \param[in,out] out Output object
+ * \param[in] reply List of attribute name/value pairs
+ *
+ * \return true if any values were printed
+ */
+static void
+print_attrd_values(pcmk__output_t *out, const GList *reply)
+{
+ for (const GList *iter = reply; iter != NULL; iter = iter->next) {
+ const pcmk__attrd_query_pair_t *pair = iter->data;
+
+ out->message(out, "attribute", NULL, NULL, pair->name, pair->value,
+ pair->node);
+ printed_values = true;
+ }
+}
+
+static void
+attrd_event_cb(pcmk_ipc_api_t *attrd_api, enum pcmk_ipc_event event_type,
+ crm_exit_t status, void *event_data, void *user_data)
+{
+ pcmk__output_t *out = (pcmk__output_t *) user_data;
+ pcmk__attrd_api_reply_t *reply = event_data;
+
+ if (event_type != pcmk_ipc_event_reply || status != CRM_EX_OK) {
+ return;
+ }
+
+ /* Print the values from the reply. */
+ if (reply->reply_type == pcmk__attrd_reply_query) {
+ print_attrd_values(out, reply->data.pairs);
+ }
+}
+
+/*!
+ * \brief Submit a query to pacemaker-attrd and print reply
+ *
+ * \param[in,out] out Output object
+ * \param[in] attr_name Name of attribute to be affected by request
+ * \param[in] attr_node Name of host to query for (or NULL for localhost)
+ * \param[in] query_all If TRUE, ignore attr_node and query all nodes
+ *
+ * \return Standard Pacemaker return code
+ */
+static int
+send_attrd_query(pcmk__output_t *out, const char *attr_name,
+ const char *attr_node, gboolean query_all)
+{
+ uint32_t options = pcmk__node_attr_none;
+ pcmk_ipc_api_t *attrd_api = NULL;
+ int rc = pcmk_rc_ok;
+
+ // Create attrd IPC object
+ rc = pcmk_new_ipc_api(&attrd_api, pcmk_ipc_attrd);
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ "Could not connect to attrd: %s", pcmk_rc_str(rc));
+ return ENOTCONN;
+ }
+
+ pcmk_register_ipc_callback(attrd_api, attrd_event_cb, out);
+
+ // Connect to attrd (without main loop)
+ rc = pcmk_connect_ipc(attrd_api, pcmk_ipc_dispatch_sync);
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ "Could not connect to attrd: %s", pcmk_rc_str(rc));
+ pcmk_free_ipc_api(attrd_api);
+ return rc;
+ }
+
+ /* Decide which node(s) to query */
+ if (query_all == TRUE) {
+ options |= pcmk__node_attr_query_all;
+ }
+
+ rc = pcmk__attrd_api_query(attrd_api, attr_node, attr_name, options);
+
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__RC_ERROR, rc, "Could not query value of %s: %s (%d)",
+ attr_name, pcmk_strerror(rc), rc);
+ } else if (!printed_values) {
+ rc = pcmk_rc_schema_validation;
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ "Could not query value of %s: attribute does not exist", attr_name);
+ }
+
+ pcmk_disconnect_ipc(attrd_api);
+ pcmk_free_ipc_api(attrd_api);
+
+ return rc;
+}
+
+static int
+send_attrd_update(char command, const char *attr_node, const char *attr_name,
+ const char *attr_value, const char *attr_set,
+ const char *attr_dampen, uint32_t attr_options)
+{
+ int rc = pcmk_rc_ok;
+
+ switch (command) {
+ case 'B':
+ rc = pcmk__attrd_api_update(NULL, attr_node, attr_name, attr_value,
+ attr_dampen, attr_set, NULL,
+ attr_options | pcmk__node_attr_value | pcmk__node_attr_delay);
+ break;
+
+ case 'D':
+ rc = pcmk__attrd_api_delete(NULL, attr_node, attr_name, attr_options);
+ break;
+
+ case 'R':
+ rc = pcmk__attrd_api_refresh(NULL, attr_node);
+ break;
+
+ case 'U':
+ rc = pcmk__attrd_api_update(NULL, attr_node, attr_name, attr_value,
+ NULL, attr_set, NULL,
+ attr_options | pcmk__node_attr_value);
+ break;
+
+ case 'Y':
+ rc = pcmk__attrd_api_update(NULL, attr_node, attr_name, NULL,
+ attr_dampen, attr_set, NULL,
+ attr_options | pcmk__node_attr_delay);
+ break;
+ }
+
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__RC_ERROR, rc, "Could not update %s=%s: %s (%d)",
+ attr_name, attr_value, pcmk_rc_str(rc), rc);
+ }
+
+ return rc;
+}
diff --git a/tools/cibadmin.8.inc b/tools/cibadmin.8.inc
new file mode 100644
index 0000000..bfd969b
--- /dev/null
+++ b/tools/cibadmin.8.inc
@@ -0,0 +1,5 @@
+[=synopsis]
+cibadmin <command> [options]
+
+/query and edit the Pacemaker configuration/
+.SH OPTIONS
diff --git a/tools/cibadmin.c b/tools/cibadmin.c
new file mode 100644
index 0000000..f80afae
--- /dev/null
+++ b/tools/cibadmin.c
@@ -0,0 +1,954 @@
+/*
+ * Copyright 2004-2023 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 <stdio.h>
+#include <crm/crm.h>
+#include <crm/msg_xml.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/ipc.h>
+#include <crm/common/xml.h>
+#include <crm/cib/internal.h>
+
+#include <pacemaker-internal.h>
+
+#define SUMMARY "query and edit the Pacemaker configuration"
+
+#define INDENT " "
+
+enum cibadmin_section_type {
+ cibadmin_section_all = 0,
+ cibadmin_section_scope,
+ cibadmin_section_xpath,
+};
+
+static int request_id = 0;
+
+static cib_t *the_cib = NULL;
+static GMainLoop *mainloop = NULL;
+static crm_exit_t exit_code = CRM_EX_OK;
+
+static struct {
+ const char *cib_action;
+ int cmd_options;
+ enum cibadmin_section_type section_type;
+ char *cib_section;
+ char *validate_with;
+ gint message_timeout_sec;
+ enum pcmk__acl_render_how acl_render_mode;
+ gchar *cib_user;
+ gchar *dest_node;
+ gchar *input_file;
+ gchar *input_xml;
+ gboolean input_stdin;
+ bool delete_all;
+ gboolean allow_create;
+ gboolean force;
+ gboolean get_node_path;
+ gboolean local;
+ gboolean no_children;
+ gboolean sync_call;
+
+ /* @COMPAT: For "-!" version option. Not advertised nor marked as
+ * deprecated, but accepted.
+ */
+ gboolean extended_version;
+
+ //! \deprecated
+ gboolean no_bcast;
+} options;
+
+int do_init(void);
+static int do_work(xmlNode *input, xmlNode **output);
+void cibadmin_op_callback(xmlNode *msg, int call_id, int rc, xmlNode *output,
+ void *user_data);
+
+static void
+print_xml_output(xmlNode * xml)
+{
+ char *buffer;
+
+ if (!xml) {
+ return;
+ } else if (xml->type != XML_ELEMENT_NODE) {
+ return;
+ }
+
+ if (pcmk_is_set(options.cmd_options, cib_xpath_address)) {
+ const char *id = crm_element_value(xml, XML_ATTR_ID);
+
+ if (pcmk__str_eq((const char *)xml->name, "xpath-query", pcmk__str_casei)) {
+ xmlNode *child = NULL;
+
+ for (child = xml->children; child; child = child->next) {
+ print_xml_output(child);
+ }
+
+ } else if (id) {
+ printf("%s\n", id);
+ }
+
+ } else {
+ buffer = dump_xml_formatted(xml);
+ fprintf(stdout, "%s", pcmk__s(buffer, "<null>\n"));
+ free(buffer);
+ }
+}
+
+// Upgrade requested but already at latest schema
+static void
+report_schema_unchanged(void)
+{
+ const char *err = pcmk_rc_str(pcmk_rc_schema_unchanged);
+
+ crm_info("Upgrade unnecessary: %s\n", err);
+ printf("Upgrade unnecessary: %s\n", err);
+ exit_code = CRM_EX_OK;
+}
+
+/*!
+ * \internal
+ * \brief Check whether the current CIB action is dangerous
+ * \return true if \p options.cib_action is dangerous, or false otherwise
+ */
+static inline bool
+cib_action_is_dangerous(void)
+{
+ return options.no_bcast || options.delete_all
+ || pcmk__str_any_of(options.cib_action,
+ PCMK__CIB_REQUEST_UPGRADE,
+ PCMK__CIB_REQUEST_ERASE,
+ NULL);
+}
+
+/*!
+ * \internal
+ * \brief Determine whether the given CIB scope is valid for \p cibadmin
+ *
+ * \param[in] scope Scope to validate
+ *
+ * \return true if \p scope is valid, or false otherwise
+ * \note An invalid scope applies the operation to the entire CIB.
+ */
+static inline bool
+scope_is_valid(const char *scope)
+{
+ return pcmk__str_any_of(scope,
+ XML_CIB_TAG_CONFIGURATION,
+ XML_CIB_TAG_NODES,
+ XML_CIB_TAG_RESOURCES,
+ XML_CIB_TAG_CONSTRAINTS,
+ XML_CIB_TAG_CRMCONFIG,
+ XML_CIB_TAG_RSCCONFIG,
+ XML_CIB_TAG_OPCONFIG,
+ XML_CIB_TAG_ACLS,
+ XML_TAG_FENCING_TOPOLOGY,
+ XML_CIB_TAG_TAGS,
+ XML_CIB_TAG_ALERTS,
+ XML_CIB_TAG_STATUS,
+ NULL);
+}
+
+static gboolean
+command_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **error)
+{
+ options.delete_all = false;
+
+ if (pcmk__str_any_of(option_name, "-u", "--upgrade", NULL)) {
+ options.cib_action = PCMK__CIB_REQUEST_UPGRADE;
+
+ } else if (pcmk__str_any_of(option_name, "-Q", "--query", NULL)) {
+ options.cib_action = PCMK__CIB_REQUEST_QUERY;
+
+ } else if (pcmk__str_any_of(option_name, "-E", "--erase", NULL)) {
+ options.cib_action = PCMK__CIB_REQUEST_ERASE;
+
+ } else if (pcmk__str_any_of(option_name, "-B", "--bump", NULL)) {
+ options.cib_action = PCMK__CIB_REQUEST_BUMP;
+
+ } else if (pcmk__str_any_of(option_name, "-C", "--create", NULL)) {
+ options.cib_action = PCMK__CIB_REQUEST_CREATE;
+
+ } else if (pcmk__str_any_of(option_name, "-M", "--modify", NULL)) {
+ options.cib_action = PCMK__CIB_REQUEST_MODIFY;
+
+ } else if (pcmk__str_any_of(option_name, "-P", "--patch", NULL)) {
+ options.cib_action = PCMK__CIB_REQUEST_APPLY_PATCH;
+
+ } else if (pcmk__str_any_of(option_name, "-R", "--replace", NULL)) {
+ options.cib_action = PCMK__CIB_REQUEST_REPLACE;
+
+ } else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) {
+ options.cib_action = PCMK__CIB_REQUEST_DELETE;
+
+ } else if (pcmk__str_any_of(option_name, "-d", "--delete-all", NULL)) {
+ options.cib_action = PCMK__CIB_REQUEST_DELETE;
+ options.delete_all = true;
+
+ } else if (pcmk__str_any_of(option_name, "-a", "--empty", NULL)) {
+ options.cib_action = "empty";
+ pcmk__str_update(&options.validate_with, optarg);
+
+ } else if (pcmk__str_any_of(option_name, "-5", "--md5-sum", NULL)) {
+ options.cib_action = "md5-sum";
+
+ } else if (pcmk__str_any_of(option_name, "-6", "--md5-sum-versioned",
+ NULL)) {
+ options.cib_action = "md5-sum-versioned";
+
+ } else {
+ // Should be impossible
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+show_access_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **error)
+{
+ if (pcmk__str_eq(optarg, "auto", pcmk__str_null_matches)) {
+ options.acl_render_mode = pcmk__acl_render_default;
+
+ } else if (g_strcmp0(optarg, "namespace") == 0) {
+ options.acl_render_mode = pcmk__acl_render_namespace;
+
+ } else if (g_strcmp0(optarg, "text") == 0) {
+ options.acl_render_mode = pcmk__acl_render_text;
+
+ } else if (g_strcmp0(optarg, "color") == 0) {
+ options.acl_render_mode = pcmk__acl_render_color;
+
+ } else {
+ g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ "Invalid value '%s' for option '%s'",
+ optarg, option_name);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+section_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **error)
+{
+ if (pcmk__str_any_of(option_name, "-o", "--scope", NULL)) {
+ options.section_type = cibadmin_section_scope;
+
+ } else if (pcmk__str_any_of(option_name, "-A", "--xpath", NULL)) {
+ options.section_type = cibadmin_section_xpath;
+
+ } else {
+ // Should be impossible
+ return FALSE;
+ }
+
+ pcmk__str_update(&options.cib_section, optarg);
+ return TRUE;
+}
+
+static GOptionEntry command_entries[] = {
+ { "upgrade", 'u', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Upgrade the configuration to the latest syntax", NULL },
+
+ { "query", 'Q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Query the contents of the CIB", NULL },
+
+ { "erase", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Erase the contents of the whole CIB", NULL },
+
+ { "bump", 'B', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Increase the CIB's epoch value by 1", NULL },
+
+ { "create", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Create an object in the CIB (will fail if object already exists)",
+ NULL },
+
+ { "modify", 'M', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Find object somewhere in CIB's XML tree and update it (fails if object "
+ "does not exist unless -c is also specified)",
+ NULL },
+
+ { "patch", 'P', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Supply an update in the form of an XML diff (see crm_diff(8))", NULL },
+
+ { "replace", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Recursively replace an object in the CIB", NULL },
+
+ { "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Delete first object matching supplied criteria (for example, "
+ "<" XML_ATTR_OP " " XML_ATTR_ID "=\"rsc1_op1\" "
+ XML_ATTR_NAME "=\"monitor\"/>).\n"
+ INDENT "The XML element name and all attributes must match in order for "
+ "the element to be deleted.",
+ NULL },
+
+ { "delete-all", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
+ command_cb,
+ "When used with --xpath, remove all matching objects in the "
+ "configuration instead of just the first one",
+ NULL },
+
+ { "empty", 'a', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
+ command_cb,
+ "Output an empty CIB. Accepts an optional schema name argument to use as "
+ "the " XML_ATTR_VALIDATION " value.\n"
+ INDENT "If no schema is given, the latest will be used.",
+ "[schema]" },
+
+ { "md5-sum", '5', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Calculate the on-disk CIB digest", NULL },
+
+ { "md5-sum-versioned", '6', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
+ command_cb, "Calculate an on-the-wire versioned CIB digest", NULL },
+
+ { NULL }
+};
+
+static GOptionEntry data_entries[] = {
+ /* @COMPAT: These arguments should be last-wins. We can have an enum option
+ * that stores the input type, along with a single string option that stores
+ * the XML string for --xml-text, filename for --xml-file, or NULL for
+ * --xml-pipe.
+ */
+ { "xml-text", 'X', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
+ &options.input_xml, "Retrieve XML from the supplied string", "value" },
+
+ { "xml-file", 'x', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME,
+ &options.input_file, "Retrieve XML from the named file", "value" },
+
+ { "xml-pipe", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
+ &options.input_stdin, "Retrieve XML from stdin", NULL },
+
+ { NULL }
+};
+
+static GOptionEntry addl_entries[] = {
+ { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force,
+ "Force the action to be performed", NULL },
+
+ { "timeout", 't', G_OPTION_FLAG_NONE, G_OPTION_ARG_INT,
+ &options.message_timeout_sec,
+ "Time (in seconds) to wait before declaring the operation failed",
+ "value" },
+
+ { "user", 'U', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.cib_user,
+ "Run the command with permissions of the named user (valid only for the "
+ "root and " CRM_DAEMON_USER " accounts)", "value" },
+
+ { "sync-call", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
+ &options.sync_call, "Wait for call to complete before returning", NULL },
+
+ { "local", 'l', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.local,
+ "Command takes effect locally (should be used only for queries)", NULL },
+
+ { "scope", 'o', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb,
+ "Limit scope of operation to specific section of CIB\n"
+ INDENT "Valid values: " XML_CIB_TAG_CONFIGURATION ", " XML_CIB_TAG_NODES
+ ", " XML_CIB_TAG_RESOURCES ", " XML_CIB_TAG_CONSTRAINTS
+ ", " XML_CIB_TAG_CRMCONFIG ", " XML_CIB_TAG_RSCCONFIG ",\n"
+ INDENT " " XML_CIB_TAG_OPCONFIG ", " XML_CIB_TAG_ACLS
+ ", " XML_TAG_FENCING_TOPOLOGY ", " XML_CIB_TAG_TAGS
+ ", " XML_CIB_TAG_ALERTS ", " XML_CIB_TAG_STATUS "\n"
+ INDENT "If both --scope/-o and --xpath/-a are specified, the last one to "
+ "appear takes effect",
+ "value" },
+
+ { "xpath", 'A', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb,
+ "A valid XPath to use instead of --scope/-o\n"
+ INDENT "If both --scope/-o and --xpath/-a are specified, the last one to "
+ "appear takes effect",
+ "value" },
+
+ { "node-path", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
+ &options.get_node_path,
+ "When performing XPath queries, return paths of any matches found\n"
+ INDENT "(for example, "
+ "\"/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION
+ "/" XML_CIB_TAG_RESOURCES "/" XML_CIB_TAG_INCARNATION
+ "[@" XML_ATTR_ID "='dummy-clone']"
+ "/" XML_CIB_TAG_RESOURCE "[@" XML_ATTR_ID "='dummy']\")",
+ NULL },
+
+ { "show-access", 'S', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
+ show_access_cb,
+ "Whether to use syntax highlighting for ACLs (with -Q/--query and "
+ "-U/--user)\n"
+ INDENT "Allowed values: 'color' (default for terminal), 'text' (plain text, "
+ "default for non-terminal),\n"
+ INDENT " 'namespace', or 'auto' (use default value)\n"
+ INDENT "Default value: 'auto'",
+ "[value]" },
+
+ { "allow-create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
+ &options.allow_create,
+ "(Advanced) Allow target of --modify/-M to be created if it does not "
+ "exist",
+ NULL },
+
+ { "no-children", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
+ &options.no_children,
+ "(Advanced) When querying an object, do not include its children in the "
+ "result",
+ NULL },
+
+ { "node", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.dest_node,
+ "(Advanced) Send command to the specified host", "value" },
+
+ // @COMPAT: Deprecated
+ { "no-bcast", 'b', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE,
+ &options.no_bcast, "deprecated", NULL },
+
+ // @COMPAT: Deprecated
+ { "host", 'h', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
+ &options.dest_node, "deprecated", NULL },
+
+ { NULL }
+};
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args)
+{
+ const char *desc = NULL;
+ GOptionContext *context = NULL;
+
+ GOptionEntry extra_prog_entries[] = {
+ // @COMPAT: Deprecated
+ { "extended-version", '!', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE,
+ &options.extended_version, "deprecated", NULL },
+
+ { NULL }
+ };
+
+ desc = "Examples:\n\n"
+ "Query the configuration from the local node:\n\n"
+ "\t# cibadmin --query --local\n\n"
+ "Query just the cluster options configuration:\n\n"
+ "\t# cibadmin --query --scope " XML_CIB_TAG_CRMCONFIG "\n\n"
+ "Query all '" XML_RSC_ATTR_TARGET_ROLE "' settings:\n\n"
+ "\t# cibadmin --query --xpath "
+ "\"//" XML_CIB_TAG_NVPAIR
+ "[@" XML_NVPAIR_ATTR_NAME "='" XML_RSC_ATTR_TARGET_ROLE"']\""
+ "\n\n"
+ "Remove all '" XML_RSC_ATTR_MANAGED "' settings:\n\n"
+ "\t# cibadmin --delete-all --xpath "
+ "\"//" XML_CIB_TAG_NVPAIR
+ "[@" XML_NVPAIR_ATTR_NAME "='" XML_RSC_ATTR_MANAGED "']\"\n\n"
+ "Remove the resource named 'old':\n\n"
+ "\t# cibadmin --delete --xml-text "
+ "'<" XML_CIB_TAG_RESOURCE " " XML_ATTR_ID "=\"old\"/>'\n\n"
+ "Remove all resources from the configuration:\n\n"
+ "\t# cibadmin --replace --scope " XML_CIB_TAG_RESOURCES
+ " --xml-text '<" XML_CIB_TAG_RESOURCES "/>'\n\n"
+ "Replace complete configuration with contents of "
+ "$HOME/pacemaker.xml:\n\n"
+ "\t# cibadmin --replace --xml-file $HOME/pacemaker.xml\n\n"
+ "Replace " XML_CIB_TAG_CONSTRAINTS " section of configuration with "
+ "contents of $HOME/constraints.xml:\n\n"
+ "\t# cibadmin --replace --scope " XML_CIB_TAG_CONSTRAINTS
+ " --xml-file $HOME/constraints.xml\n\n"
+ "Increase configuration version to prevent old configurations from "
+ "being loaded accidentally:\n\n"
+ "\t# cibadmin --modify --xml-text "
+ "'<" XML_TAG_CIB " " XML_ATTR_GENERATION_ADMIN
+ "=\"" XML_ATTR_GENERATION_ADMIN "++\"/>'\n\n"
+ "Edit the configuration with your favorite $EDITOR:\n\n"
+ "\t# cibadmin --query > $HOME/local.xml\n\n"
+ "\t# $EDITOR $HOME/local.xml\n\n"
+ "\t# cibadmin --replace --xml-file $HOME/local.xml\n\n"
+ "Assuming terminal, render configuration in color (green for "
+ "writable, blue for readable, red for\n"
+ "denied) to visualize permissions for user tony:\n\n"
+ "\t# cibadmin --show-access=color --query --user tony | less -r\n\n"
+ "SEE ALSO:\n"
+ " crm(8), pcs(8), crm_shadow(8), crm_diff(8)\n";
+
+ context = pcmk__build_arg_context(args, NULL, NULL, "<command>");
+ g_option_context_set_description(context, desc);
+
+ pcmk__add_main_args(context, extra_prog_entries);
+
+ pcmk__add_arg_group(context, "commands", "Commands:", "Show command help",
+ command_entries);
+ pcmk__add_arg_group(context, "data", "Data:", "Show data help",
+ data_entries);
+ pcmk__add_arg_group(context, "additional", "Additional Options:",
+ "Show additional options", addl_entries);
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ int rc = pcmk_rc_ok;
+ const char *source = NULL;
+ xmlNode *output = NULL;
+ xmlNode *input = NULL;
+ gchar *acl_cred = NULL;
+
+ GError *error = NULL;
+
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, "ANSUXhotx");
+ GOptionContext *context = build_arg_context(args);
+
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ if (g_strv_length(processed_args) > 1) {
+ gchar *help = g_option_context_get_help(context, TRUE, NULL);
+ GString *extra = g_string_sized_new(128);
+
+ for (int lpc = 1; processed_args[lpc] != NULL; lpc++) {
+ if (extra->len > 0) {
+ g_string_append_c(extra, ' ');
+ }
+ g_string_append(extra, processed_args[lpc]);
+ }
+
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "non-option ARGV-elements: %s\n\n%s", extra->str, help);
+ g_free(help);
+ g_string_free(extra, TRUE);
+ goto done;
+ }
+
+ if (args->version || options.extended_version) {
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+
+ /* FIXME: When cibadmin is converted to use formatted output, this can
+ * be replaced by out->version with the appropriate boolean flag.
+ *
+ * options.extended_version is deprecated and will be removed in a
+ * future release.
+ */
+ pcmk__cli_help(options.extended_version? '!' : 'v');
+ }
+
+ /* At LOG_ERR, stderr for CIB calls is rather verbose. Several lines like
+ *
+ * (func@file:line) error: CIB <op> failures <XML>
+ *
+ * In cibadmin we explicitly output the XML portion without the prefixes. So
+ * we default to LOG_CRIT.
+ */
+ pcmk__cli_init_logging("cibadmin", 0);
+ set_crm_log_level(LOG_CRIT);
+
+ if (args->verbosity > 0) {
+ cib__set_call_options(options.cmd_options, crm_system_name,
+ cib_verbose);
+
+ for (int i = 0; i < args->verbosity; i++) {
+ crm_bump_log_level(argc, argv);
+ }
+ }
+
+ if (options.cib_action == NULL) {
+ // @COMPAT: Create a default command if other tools have one
+ gchar *help = g_option_context_get_help(context, TRUE, NULL);
+
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Must specify a command option\n\n%s", help);
+ g_free(help);
+ goto done;
+ }
+
+ if (strcmp(options.cib_action, "empty") == 0) {
+ // Output an empty CIB
+ char *buf = NULL;
+
+ output = createEmptyCib(1);
+ crm_xml_add(output, XML_ATTR_VALIDATION, options.validate_with);
+ buf = dump_xml_formatted(output);
+ fprintf(stdout, "%s", pcmk__s(buf, "<null>\n"));
+ free(buf);
+ goto done;
+ }
+
+ if (cib_action_is_dangerous() && !options.force) {
+ exit_code = CRM_EX_UNSAFE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "The supplied command is considered dangerous. To prevent "
+ "accidental destruction of the cluster, the --force flag "
+ "is required in order to proceed.");
+ goto done;
+ }
+
+ if (options.message_timeout_sec < 1) {
+ // Set default timeout
+ options.message_timeout_sec = 30;
+ }
+
+ if (options.section_type == cibadmin_section_xpath) {
+ // Enable getting section by XPath
+ cib__set_call_options(options.cmd_options, crm_system_name,
+ cib_xpath);
+
+ } else if (options.section_type == cibadmin_section_scope) {
+ if (!scope_is_valid(options.cib_section)) {
+ // @COMPAT: Consider requiring --force to proceed
+ fprintf(stderr,
+ "Invalid value '%s' for '--scope'. Operation will apply "
+ "to the entire CIB.\n", options.cib_section);
+ }
+ }
+
+ if (options.allow_create) {
+ // Allow target of --modify/-M to be created if it does not exist
+ cib__set_call_options(options.cmd_options, crm_system_name,
+ cib_can_create);
+ }
+
+ if (options.delete_all) {
+ // With cibadmin_section_xpath, remove all matching objects
+ cib__set_call_options(options.cmd_options, crm_system_name,
+ cib_multiple);
+ }
+
+ if (options.get_node_path) {
+ /* Enable getting node path of XPath query matches.
+ * Meaningful only if options.section_type == cibadmin_section_xpath.
+ */
+ cib__set_call_options(options.cmd_options, crm_system_name,
+ cib_xpath_address);
+ }
+
+ if (options.local) {
+ // Configure command to take effect only locally
+ cib__set_call_options(options.cmd_options, crm_system_name,
+ cib_scope_local);
+ }
+
+ // @COMPAT: Deprecated option
+ if (options.no_bcast) {
+ // Configure command to take effect only locally and not to broadcast
+ cib__set_call_options(options.cmd_options, crm_system_name,
+ cib_inhibit_bcast|cib_scope_local);
+ }
+
+ if (options.no_children) {
+ // When querying an object, don't include its children in the result
+ cib__set_call_options(options.cmd_options, crm_system_name,
+ cib_no_children);
+ }
+
+ if (options.sync_call
+ || (options.acl_render_mode != pcmk__acl_render_none)) {
+ /* Wait for call to complete before returning.
+ *
+ * The ACL render modes work only with sync calls due to differences in
+ * output handling between sync/async. It shouldn't matter to the user
+ * whether the call is synchronous; for a CIB query, we have to wait for
+ * the result in order to display it in any case.
+ */
+ cib__set_call_options(options.cmd_options, crm_system_name,
+ cib_sync_call);
+ }
+
+ if (options.input_file != NULL) {
+ input = filename2xml(options.input_file);
+ source = options.input_file;
+
+ } else if (options.input_xml != NULL) {
+ input = string2xml(options.input_xml);
+ source = "input string";
+
+ } else if (options.input_stdin) {
+ source = "STDIN";
+ input = stdin2xml();
+
+ } else if (options.acl_render_mode != pcmk__acl_render_none) {
+ char *username = pcmk__uid2username(geteuid());
+ bool required = pcmk_acl_required(username);
+
+ free(username);
+
+ if (required) {
+ if (options.force) {
+ fprintf(stderr, "The supplied command can provide skewed"
+ " result since it is run under user that also"
+ " gets guarded per ACLs on their own right."
+ " Continuing since --force flag was"
+ " provided.\n");
+
+ } else {
+ exit_code = CRM_EX_UNSAFE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "The supplied command can provide skewed result "
+ "since it is run under user that also gets guarded "
+ "per ACLs in their own right. To accept the risk "
+ "of such a possible distortion (without even "
+ "knowing it at this time), use the --force flag.");
+ goto done;
+ }
+ }
+
+ if (options.cib_user == NULL) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "The supplied command requires -U user specified.");
+ goto done;
+ }
+
+ /* We already stopped/warned ACL-controlled users about consequences.
+ *
+ * Note: acl_cred takes ownership of options.cib_user here.
+ * options.cib_user is set to NULL so that the CIB is obtained as the
+ * user running the cibadmin command. The CIB must be obtained as a user
+ * with full permissions in order to show the CIB correctly annotated
+ * for the options.cib_user's permissions.
+ */
+ acl_cred = options.cib_user;
+ options.cib_user = NULL;
+ }
+
+ if (input != NULL) {
+ crm_log_xml_debug(input, "[admin input]");
+
+ } else if (source != NULL) {
+ exit_code = CRM_EX_CONFIG;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Couldn't parse input from %s.", source);
+ goto done;
+ }
+
+ if (strcmp(options.cib_action, "md5-sum") == 0) {
+ char *digest = NULL;
+
+ if (input == NULL) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Please supply XML to process with -X, -x, or -p");
+ goto done;
+ }
+
+ digest = calculate_on_disk_digest(input);
+ fprintf(stderr, "Digest: ");
+ fprintf(stdout, "%s\n", pcmk__s(digest, "<null>"));
+ free(digest);
+ goto done;
+
+ } else if (strcmp(options.cib_action, "md5-sum-versioned") == 0) {
+ char *digest = NULL;
+ const char *version = NULL;
+
+ if (input == NULL) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Please supply XML to process with -X, -x, or -p");
+ goto done;
+ }
+
+ version = crm_element_value(input, XML_ATTR_CRM_VERSION);
+ digest = calculate_xml_versioned_digest(input, FALSE, TRUE, version);
+ fprintf(stderr, "Versioned (%s) digest: ", version);
+ fprintf(stdout, "%s\n", pcmk__s(digest, "<null>"));
+ free(digest);
+ goto done;
+ }
+
+ rc = do_init();
+ if (rc != pcmk_ok) {
+ rc = pcmk_legacy2rc(rc);
+ exit_code = pcmk_rc2exitc(rc);
+
+ crm_err("Init failed, could not perform requested operations: %s",
+ pcmk_rc_str(rc));
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Init failed, could not perform requested operations: %s",
+ pcmk_rc_str(rc));
+ goto done;
+ }
+
+ rc = do_work(input, &output);
+ if (rc > 0) {
+ /* wait for the reply by creating a mainloop and running it until
+ * the callbacks are invoked...
+ */
+ request_id = rc;
+
+ the_cib->cmds->register_callback(the_cib, request_id,
+ options.message_timeout_sec, FALSE,
+ NULL, "cibadmin_op_callback",
+ cibadmin_op_callback);
+
+ mainloop = g_main_loop_new(NULL, FALSE);
+
+ crm_trace("%s waiting for reply from the local CIB", crm_system_name);
+
+ crm_info("Starting mainloop");
+ g_main_loop_run(mainloop);
+
+ } else if ((rc == -pcmk_err_schema_unchanged)
+ && (strcmp(options.cib_action,
+ PCMK__CIB_REQUEST_UPGRADE) == 0)) {
+ report_schema_unchanged();
+
+ } else if (rc < 0) {
+ rc = pcmk_legacy2rc(rc);
+ crm_err("Call failed: %s", pcmk_rc_str(rc));
+ fprintf(stderr, "Call failed: %s\n", pcmk_rc_str(rc));
+
+ if (rc == pcmk_rc_schema_validation) {
+ if (strcmp(options.cib_action, PCMK__CIB_REQUEST_UPGRADE) == 0) {
+ xmlNode *obj = NULL;
+ int version = 0;
+
+ if (the_cib->cmds->query(the_cib, NULL, &obj,
+ options.cmd_options) == pcmk_ok) {
+ update_validation(&obj, &version, 0, TRUE, FALSE);
+ }
+ free_xml(obj);
+
+ } else if (output) {
+ validate_xml_verbose(output);
+ }
+ }
+ exit_code = pcmk_rc2exitc(rc);
+ }
+
+ if ((output != NULL)
+ && (options.acl_render_mode != pcmk__acl_render_none)) {
+
+ xmlDoc *acl_evaled_doc;
+ rc = pcmk__acl_annotate_permissions(acl_cred, output->doc, &acl_evaled_doc);
+ if (rc == pcmk_rc_ok) {
+ xmlChar *rendered = NULL;
+
+ rc = pcmk__acl_evaled_render(acl_evaled_doc,
+ options.acl_render_mode, &rendered);
+ if (rc != pcmk_rc_ok) {
+ exit_code = CRM_EX_CONFIG;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Could not render evaluated access: %s",
+ pcmk_rc_str(rc));
+ goto done;
+ }
+ printf("%s\n", (char *) rendered);
+ free(rendered);
+
+ } else {
+ exit_code = CRM_EX_CONFIG;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Could not evaluate access per request (%s, error: %s)",
+ acl_cred, pcmk_rc_str(rc));
+ goto done;
+ }
+
+ } else if (output != NULL) {
+ print_xml_output(output);
+ }
+
+ crm_trace("%s exiting normally", crm_system_name);
+
+done:
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+
+ g_free(options.cib_user);
+ g_free(options.dest_node);
+ g_free(options.input_file);
+ g_free(options.input_xml);
+ free(options.cib_section);
+ free(options.validate_with);
+
+ g_free(acl_cred);
+ free_xml(input);
+ free_xml(output);
+
+ rc = cib__clean_up_connection(&the_cib);
+ if (exit_code == CRM_EX_OK) {
+ exit_code = pcmk_rc2exitc(rc);
+ }
+
+ pcmk__output_and_clear_error(&error, NULL);
+ crm_exit(exit_code);
+}
+
+static int
+do_work(xmlNode *input, xmlNode **output)
+{
+ /* construct the request */
+ the_cib->call_timeout = options.message_timeout_sec;
+ if ((strcmp(options.cib_action, PCMK__CIB_REQUEST_REPLACE) == 0)
+ && pcmk__str_eq(crm_element_name(input), XML_TAG_CIB, pcmk__str_casei)) {
+ xmlNode *status = pcmk_find_cib_element(input, XML_CIB_TAG_STATUS);
+
+ if (status == NULL) {
+ create_xml_node(input, XML_CIB_TAG_STATUS);
+ }
+ }
+
+ crm_trace("Passing \"%s\" to variant_op...", options.cib_action);
+ return cib_internal_op(the_cib, options.cib_action, options.dest_node,
+ options.cib_section, input, output,
+ options.cmd_options, options.cib_user);
+}
+
+int
+do_init(void)
+{
+ int rc = pcmk_ok;
+
+ the_cib = cib_new();
+ rc = the_cib->cmds->signon(the_cib, crm_system_name, cib_command);
+ if (rc != pcmk_ok) {
+ crm_err("Could not connect to the CIB: %s", pcmk_strerror(rc));
+ fprintf(stderr, "Could not connect to the CIB: %s\n",
+ pcmk_strerror(rc));
+ }
+
+ return rc;
+}
+
+void
+cibadmin_op_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data)
+{
+ rc = pcmk_legacy2rc(rc);
+ exit_code = pcmk_rc2exitc(rc);
+
+ if (rc == pcmk_rc_schema_unchanged) {
+ report_schema_unchanged();
+
+ } else if (rc != pcmk_rc_ok) {
+ crm_warn("Call %s failed: %s " CRM_XS " rc=%d",
+ options.cib_action, pcmk_rc_str(rc), rc);
+ fprintf(stderr, "Call %s failed: %s\n",
+ options.cib_action, pcmk_rc_str(rc));
+ print_xml_output(output);
+
+ } else if ((strcmp(options.cib_action, PCMK__CIB_REQUEST_QUERY) == 0)
+ && (output == NULL)) {
+ crm_err("Query returned no output");
+ crm_log_xml_err(msg, "no output");
+
+ } else if (output == NULL) {
+ crm_info("Call passed");
+
+ } else {
+ crm_info("Call passed");
+ print_xml_output(output);
+ }
+
+ if (call_id == request_id) {
+ g_main_loop_quit(mainloop);
+
+ } else {
+ crm_info("Message was not the response we were looking for (%d vs. %d)",
+ call_id, request_id);
+ }
+}
diff --git a/tools/cibsecret.in b/tools/cibsecret.in
new file mode 100644
index 0000000..4569863
--- /dev/null
+++ b/tools/cibsecret.in
@@ -0,0 +1,440 @@
+#!@BASH_PATH@
+
+# Copyright 2011-2020 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.
+#
+
+# cibsecret
+#
+# Manage the secrets directory (by default, /var/lib/pacemaker/lrm/secrets).
+# Secrets are ASCII files, holding one value per file:
+# <secrets-directory>/<rsc>/<param>
+
+# These constants must track crm_exit_t values
+CRM_EX_OK=0
+CRM_EX_ERROR=1
+CRM_EX_NOT_INSTALLED=5
+CRM_EX_USAGE=64
+CRM_EX_UNAVAILABLE=69
+CRM_EX_OSFILE=72
+CRM_EX_CONFIG=78
+CRM_EX_DIGEST=104
+CRM_EX_NOSUCH=105
+CRM_EX_EXISTS=108
+
+LRM_CIBSECRETS="@LRM_CIBSECRETS_DIR@"
+
+PROG="$(basename "$0")"
+SSH_OPTS="-o StrictHostKeyChecking=no"
+MAGIC="lrm://"
+
+usage() {
+ cat <<EOF
+cibsecret - manage sensitive information in Pacemaker CIB
+
+Usage:
+ $PROG [<options>] <command> [<parameters>]
+
+Options:
+ --help Show this message, then exit
+ --version Display version information, then exit
+ -C Don't read or write the CIB
+
+Commands and their parameters:
+ set <resource-id> <resource-parameter> <value>
+ Set the value of a sensitive resource parameter.
+
+ get <resource-id> <resource-parameter>
+ Display the locally stored value of a sensitive resource parameter.
+
+ check <resource-id> <resource-parameter>
+ Verify that the locally stored value of a sensitive resource parameter
+ matches its locally stored MD5 hash.
+
+ stash <resource-id> <resource-parameter>
+ Make a non-sensitive resource parameter that is already in the CIB
+ sensitive (move its value to a locally stored and protected file).
+ This may not be used with -C.
+
+ unstash <resource-id> <resource-parameter>
+ Make a sensitive resource parameter that is already in the CIB
+ non-sensitive (move its value from the locally stored file to the CIB).
+ This may not be used with -C.
+
+ delete <resource-id> <resource-parameter>
+ Remove a sensitive resource parameter value.
+
+ sync
+ Copy all locally stored secrets to all other nodes.
+
+This command manages sensitive resource parameter values that should not be
+stored directly in Pacemaker's Cluster Information Base (CIB). Such values
+are handled by storing a special string directly in the CIB that tells
+Pacemaker to look in a separate, protected file for the actual value.
+
+The secret files are not encrypted, but protected by file system permissions
+such that only root can read or modify them.
+
+Since the secret files are stored locally, they must be synchronized across all
+cluster nodes. This command handles the synchronization using (in order of
+preference) pssh, pdsh, or ssh, so one of those must be installed. Before
+synchronizing, this command will ping the cluster nodes to determine which are
+alive, using fping if it is installed, otherwise the ping command. Installing
+fping is strongly recommended for better performance.
+
+Known limitations:
+
+ This command can only be run from full cluster nodes (not Pacemaker Remote
+ nodes).
+
+ Changes are not atomic, so the cluster may use different values while a
+ change is in progress. To avoid problems, it is recommended to put the
+ cluster in maintenance mode when making changes with this command.
+
+ Changes in secret values do not trigger an agent reload or restart of the
+ affected resource, since they do not change the CIB. If a response is
+ desired before the next cluster recheck interval, any CIB change (such as
+ setting a node attribute) will trigger it.
+
+ If any node is down when changes to secrets are made, or a new node is
+ later added to the cluster, it may have different values when it joins the
+ cluster, before "$PROG sync" is run. To avoid this, it is recommended to
+ run the sync command (from another node) before starting Pacemaker on the
+ node.
+
+Examples:
+
+ $PROG set ipmi_node1 passwd SecreT_PASS
+
+ $PROG get ipmi_node1 passwd
+
+ $PROG check ipmi_node1 passwd
+
+ $PROG stash ipmi_node2 passwd
+
+ $PROG sync
+EOF
+ exit "$1"
+}
+
+check_usage() {
+ case "$1" in
+ set) [ "$2" -ne 4 ] && [ "$2" -ne 3 ] && usage 1 ;;
+ get) [ "$2" -ne 3 ] && usage 1 ;;
+ check) [ "$2" -ne 3 ] && usage 1 ;;
+ stash) [ "$2" -ne 3 ] && usage 1 ;;
+ unstash) [ "$2" -ne 3 ] && usage 1 ;;
+ delete) [ "$2" -ne 3 ] && usage 1 ;;
+ sync) [ "$2" -ne 1 ] && usage 1 ;;
+ --help) usage $CRM_EX_OK ;;
+ --version) crm_attribute --version; exit $? ;;
+ *) usage $CRM_EX_USAGE ;;
+ esac
+}
+
+fatal() {
+ rc=$1
+ shift
+ echo "ERROR: $*"
+ exit $rc
+}
+
+warn() {
+ echo "WARNING: $*"
+}
+
+info() {
+ echo "INFO: $*"
+}
+
+check_env() {
+ which md5sum >/dev/null 2>&1 ||
+ fatal $CRM_EX_NOT_INSTALLED "please install md5sum to run $PROG"
+ if which pssh >/dev/null 2>&1; then
+ rsh=pssh_fun
+ rcp_to_from=pscp_fun
+
+ # -q is a SUSE patch not present in upstream pssh
+ PSSH_QUIET_OPTION=""
+ pssh -q 2>&1|grep "no such option: -q" > /dev/null ||
+ PSSH_QUIET_OPTION="-q"
+ elif which pdsh >/dev/null 2>&1; then
+ rsh=pdsh_fun
+ rcp_to_from=pdcp_fun
+ elif which ssh >/dev/null 2>&1; then
+ rsh=ssh_fun
+ rcp_to_from=scp_fun
+ else
+ fatal $CRM_EX_NOT_INSTALLED "please install pssh, pdsh, or ssh to run $PROG"
+ fi
+ ps -ef | grep '[p]acemaker-controld' >/dev/null ||
+ fatal $CRM_EX_UNAVAILABLE "pacemaker not running? $PROG needs pacemaker"
+}
+
+# This must be called (and return success) before calling $rsh or $rcp_to_from
+get_live_peers() {
+ # Get local node name
+ GLP_LOCAL_NODE="$(crm_node -n)"
+ [ $? -eq 0 ] || fatal $CRM_EX_UNAVAILABLE "couldn't get local node name"
+
+ # Get a list of all other cluster nodes
+ GLP_ALL_PEERS="$(crmadmin -N -q)"
+ [ $? -eq 0 ] || fatal $CRM_EX_UNAVAILABLE "couldn't determine cluster nodes"
+ GLP_ALL_PEERS="$(echo "$GLP_ALL_PEERS" | grep -v "^${GLP_LOCAL_NODE}$")"
+
+ # Make a list of those that respond to pings
+ if [ "$(id -u)" = "0" ] && which fping >/dev/null 2>&1; then
+ LIVE_NODES=$(fping -a $GLP_ALL_PEERS 2>/dev/null)
+ else
+ LIVE_NODES=""
+ for GLP_NODE in $GLP_ALL_PEERS; do \
+ ping -c 2 -q "$GLP_NODE" >/dev/null 2>&1 &&
+ LIVE_NODES="$LIVE_NODES $GLP_NODE"
+ done
+ fi
+
+ # Warn the user about any that didn't respond to pings
+ GLP_DOWN="$( (for GLP_NODE in $LIVE_NODES $GLP_ALL_PEERS; do echo "$GLP_NODE"; done) | sort | uniq -u)"
+ if [ "$(echo "$GLP_DOWN" | wc -w)" = "1" ]; then
+ warn "node $GLP_DOWN is down"
+ warn "you'll need to update it using \"$PROG sync\" later"
+ elif [ -n "$GLP_DOWN" ]; then
+ warn "nodes $(echo "$GLP_DOWN" | tr '\n' ' ')are down"
+ warn "you'll need to update them using \"$PROG sync\" later"
+ fi
+
+ if [ "$LIVE_NODES" = "" ]; then
+ info "no other nodes live"
+ return 1
+ fi
+ return 0
+}
+
+pssh_fun() {
+ pssh $PSSH_QUIET_OPTION -i -H "$LIVE_NODES" -x "$SSH_OPTS" -- "$@"
+}
+
+pscp_fun() {
+ PSCP_DEST="$1"
+ shift
+ pscp $PSSH_QUIET_OPTION -H "$LIVE_NODES" -x "-pr" -x "$SSH_OPTS" -- "$@" "$PSCP_DEST"
+}
+
+pdsh_fun() {
+ PDSH_NODES=$(echo "$LIVE_NODES" | tr '[:space:]' ',')
+ export PDSH_SSH_ARGS_APPEND="$SSH_OPTS"
+ pdsh -w "$PDSH_NODES" -- "$@"
+}
+
+pdcp_fun() {
+ PDCP_DEST="$1"
+ shift
+ PDCP_NODES=$(echo "$LIVE_NODES" | tr '[:space:]' ',')
+ export PDSH_SSH_ARGS_APPEND="$SSH_OPTS"
+ pdcp -pr -w "$PDCP_NODES" -- "$@" "$PDCP_DEST"
+}
+
+ssh_fun() {
+ for SSH_NODE in $LIVE_NODES; do
+ ssh $SSH_OPTS "$SSH_NODE" -- "$@" || return
+ done
+}
+
+scp_fun() {
+ SCP_DEST="$1"
+ shift
+ for SCP_NODE in $LIVE_NODES; do
+ scp -pqr $SSH_OPTS "$@" "$SCP_NODE:$SCP_DEST" || return
+ done
+}
+
+# TODO: this procedure should be replaced with csync2
+# provided that csync2 has already been configured
+sync_files() {
+ get_live_peers || return
+ if [ "$cmd" != "delete" ]; then
+ info "syncing $LRM_CIBSECRETS to $(echo "$LIVE_NODES" | tr '\n' ' ') ..."
+ else
+ info "deleting $LRM_CIBSECRETS from $(echo "$LIVE_NODES" | tr '\n' ' ') ..."
+ fi
+ $rsh rm -rf "$LRM_CIBSECRETS" &&
+ $rsh mkdir -p "$(dirname "$LRM_CIBSECRETS")" &&
+ $rcp_to_from "$(dirname "$LRM_CIBSECRETS")" "$LRM_CIBSECRETS"
+}
+
+sync_one() {
+ SO_FILE="$1"
+ get_live_peers || return
+ if [ "$cmd" != "delete" ]; then
+ info "syncing $SO_FILE to $(echo "$LIVE_NODES" | tr '\n' ' ') ..."
+ else
+ info "deleting $SO_FILE from $(echo "$LIVE_NODES" | tr '\n' ' ') ..."
+ fi
+ $rsh mkdir -p "$(dirname "$SO_FILE")" &&
+ if [ -f "$SO_FILE" ]; then
+ $rcp_to_from "$(dirname "$SO_FILE")" "$SO_FILE" "${SO_FILE}.sign"
+ else
+ $rsh rm -f "$SO_FILE" "${SO_FILE}.sign"
+ fi
+}
+
+is_secret() {
+ # assume that the secret is in the CIB if we cannot talk to cib
+ [ "$NO_CRM" ] || test "$1" = "$MAGIC"
+}
+
+check_cib_rsc() {
+ CCR_OUT="$($NO_CRM crm_resource -r "$1" -W 2>&1)" || fatal $CRM_EX_NOSUCH "$CCR_OUT"
+}
+
+get_cib_param() {
+ GCP_RSC="$1"
+ GCP_PARAM="$2"
+ $NO_CRM crm_resource -r "$GCP_RSC" -g "$GCP_PARAM" 2>/dev/null
+}
+
+set_cib_param() {
+ SET_RSC="$1"
+ SET_PARAM="$2"
+ SET_VAL="$3"
+ $NO_CRM crm_resource -r "$SET_RSC" -p "$SET_PARAM" -v "$SET_VAL" 2>/dev/null
+}
+
+remove_cib_param() {
+ RM_RSC="$1"
+ RM_PARAM="$2"
+ $NO_CRM crm_resource -r "$RM_RSC" -d "$RM_PARAM" 2>/dev/null
+}
+
+localfiles() {
+ LF_CMD="$1"
+ LF_RSC="$2"
+ LF_PARAM="$3"
+ LF_VALUE="$4"
+ LF_FILE="$LRM_CIBSECRETS/$LF_RSC/$LF_PARAM"
+ case "$LF_CMD" in
+ get)
+ cat "$LF_FILE" 2>/dev/null
+ true
+ ;;
+
+ getsum)
+ cat "${LF_FILE}.sign" 2>/dev/null
+ true
+ ;;
+
+ set)
+ LF_SUM="$(printf %s "$LF_VALUE" | md5sum)" ||
+ fatal $CRM_EX_ERROR "md5sum failed to produce hash for resource $LF_RSC parameter $LF_PARAM"
+ LF_SUM="$(echo "$LF_SUM" | awk '{print $1}')"
+ mkdir -p "$(dirname "$LF_FILE")" &&
+ echo "$LF_VALUE" > "$LF_FILE" &&
+ echo "$LF_SUM" > "${LF_FILE}.sign" &&
+ sync_one "$LF_FILE"
+ ;;
+
+ remove)
+ rm -f "$LF_FILE" "${LF_FILE}.sign"
+ sync_one "$LF_FILE"
+ ;;
+ esac
+}
+
+cibsecret_set() {
+ CS_VALUE="$1"
+
+ if [ "$2" -ne 4 ]; then
+ read -p "Enter value: " CS_VALUE
+ fi
+
+ check_cib_rsc "$rsc"
+ CIBSET_CURRENT="$(get_cib_param "$rsc" "$param")"
+ [ -z "$NO_CRM" ] &&
+ [ ! -z "$CIBSET_CURRENT" ] &&
+ [ "$CIBSET_CURRENT" != "$MAGIC" ] &&
+ [ "$CIBSET_CURRENT" != "$CS_VALUE" ] &&
+ fatal $CRM_EX_CONFIG "CIB value <$CIBSET_CURRENT> different for $rsc parameter $param; please delete it first"
+ localfiles set "$rsc" "$param" "$CS_VALUE" &&
+ set_cib_param "$rsc" "$param" "$MAGIC"
+}
+
+cibsecret_check() {
+ check_cib_rsc "$rsc"
+ is_secret "$(get_cib_param "$rsc" "$param")" ||
+ fatal $CRM_EX_CONFIG "resource $rsc parameter $param not set as secret, nothing to check"
+ CSC_LOCAL_SUM="$(localfiles getsum "$rsc" "$param")"
+ [ "$CSC_LOCAL_SUM" ] ||
+ fatal $CRM_EX_OSFILE "no MD5 hash for resource $rsc parameter $param"
+ CSC_LOCAL_VALUE="$(localfiles get "$rsc" "$param")"
+ CSC_CALC_SUM="$(printf "%s" "$CSC_LOCAL_VALUE" | md5sum | awk '{print $1}')"
+ [ "$CSC_CALC_SUM" = "$CSC_LOCAL_SUM" ] ||
+ fatal $CRM_EX_DIGEST "MD5 hash mismatch for resource $rsc parameter $param"
+}
+
+cibsecret_get() {
+ cibsecret_check
+ localfiles get "$rsc" "$param"
+}
+
+cibsecret_delete() {
+ check_cib_rsc "$rsc"
+ localfiles remove "$rsc" "$param" && remove_cib_param "$rsc" "$param"
+}
+
+cibsecret_stash() {
+ [ "$NO_CRM" ] && fatal $CRM_EX_USAGE "no access to Pacemaker, stash not supported"
+ check_cib_rsc "$rsc"
+ CIBSTASH_CURRENT="$(get_cib_param "$rsc" "$param")"
+ [ "$CIBSTASH_CURRENT" = "" ] &&
+ fatal $CRM_EX_NOSUCH "nothing to stash for resource $rsc parameter $param"
+ is_secret "$CIBSTASH_CURRENT" &&
+ fatal $CRM_EX_EXISTS "resource $rsc parameter $param already set as secret, nothing to stash"
+ cibsecret_set "$CIBSTASH_CURRENT" 4
+}
+
+cibsecret_unstash() {
+ [ "$NO_CRM" ] && fatal $CRM_EX_USAGE "no access to Pacemaker, unstash not supported"
+ UNSTASH_LOCAL_VALUE="$(localfiles get "$rsc" "$param")"
+ [ "$UNSTASH_LOCAL_VALUE" = "" ] &&
+ fatal $CRM_EX_NOSUCH "nothing to unstash for resource $rsc parameter $param"
+ check_cib_rsc "$rsc"
+ is_secret "$(get_cib_param "$rsc" "$param")" ||
+ warn "resource $rsc parameter $param not set as secret, but we have local value so proceeding anyway"
+ localfiles remove "$rsc" "$param" &&
+ set_cib_param "$rsc" "$param" "$UNSTASH_LOCAL_VALUE"
+}
+
+cibsecret_sync() {
+ sync_files
+}
+
+# Grab arguments
+if [ "$1" = "-C" ]; then
+ NO_CRM=':'
+ shift
+fi
+cmd="$1"
+rsc="$2"
+param="$3"
+value="$4"
+
+# Ensure we have everything we need
+check_usage "$cmd" $#
+check_env
+umask 0077
+
+# for dirname() function (@TODO why are we replacing dirname?)
+. "@OCF_ROOT_DIR@/lib/heartbeat/ocf-shellfuncs"
+
+"cibsecret_$cmd" "$value" $#
+rc=$?
+
+if [ $rc -ne 0 ]; then
+ fatal $CRM_EX_ERROR "$cmd(): failed with rc: $rc"
+fi
+
+# vim: set expandtab tabstop=8 softtabstop=4 shiftwidth=4 textwidth=80:
diff --git a/tools/cluster-clean.in b/tools/cluster-clean.in
new file mode 100755
index 0000000..4b75edf
--- /dev/null
+++ b/tools/cluster-clean.in
@@ -0,0 +1,99 @@
+#!@BASH_PATH@
+#
+# Copyright 2011-2023 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.
+#
+
+hosts=
+group=
+kill=0
+while true; do
+ case "$1" in
+ -x) set -x; shift;;
+ -w) for h in $2; do
+ hosts="$hosts -w $h";
+ done
+ shift; shift;;
+ -g) group=$2; shift; shift;;
+ --kill) kill=1; shift;;
+ --kill-only) kill=2; shift;;
+ "") break;;
+ *) echo "unknown option: $1"; exit 1;;
+ esac
+done
+
+if [ x"$group" = x -a x"$hosts" = x ]; then
+ group=$CTS_GROUP
+fi
+
+if [ x"$hosts" != x ]; then
+ echo `date` ": Cleaning up hosts:"
+ target=$hosts
+elif [ x"$group" != x ]; then
+ echo `date` ": Cleaning up group: $group"
+ target="-g $group"
+else
+ echo "You didn't specify any nodes to clean up"
+ exit 1
+fi
+cluster-helper --list bullet $target
+
+if [ $kill != 0 ]; then
+ echo "Cleaning processes"
+
+ # Bah. Force systemd to actually look at the process and realize it's dead
+ cluster-helper $target -- "service corosync stop" &> /dev/null &
+ cluster-helper $target -- "service pacemaker stop" &> /dev/null &
+
+ cluster-helper $target -- "killall -q -9 corosync pacemakerd pacemaker-attrd pacemaker-based pacemaker-controld pacemaker-execd pacemaker-fenced pacemaker-remoted pacemaker-schedulerd dlm_controld gfs_controld" &> /dev/null
+ cluster-helper $target -- 'kill -9 `pidof valgrind`' &> /dev/null
+
+ if [ $kill == 2 ]; then
+ exit 0
+ fi
+fi
+
+#logrotate -f $cluster_rotate
+echo "Cleaning files"
+
+log_files=""
+log_files="$log_files 'messages*'"
+log_files="$log_files 'localmessages*'"
+log_files="$log_files 'cluster*.log'"
+log_files="$log_files 'corosync.log*'"
+log_files="$log_files 'pacemaker.log*'"
+log_files="$log_files '*.journal'"
+log_files="$log_files '*.journal~'"
+log_files="$log_files 'secure-*'"
+
+state_files=""
+state_files="$state_files 'cib.xml*'"
+state_files="$state_files 'valgrind-*'"
+state_files="$state_files 'cib-*'"
+state_files="$state_files 'core.*'"
+state_files="$state_files 'cts.*'"
+state_files="$state_files 'pe*.bz2'"
+state_files="$state_files 'fdata-*'"
+
+for f in $log_files; do
+ cluster-helper $target -- "find /var/log -name '$f' -exec rm -f \{\} \;"
+done
+
+for f in $state_files; do
+ cluster-helper $target -- "find /var/lib -name '$f' -exec rm -f \{\} \;"
+done
+
+cluster-helper $target -- "find /dev/shm -name 'qb-*' -exec rm -f \{\} \;"
+cluster-helper $target -- "find @CRM_BLACKBOX_DIR@ -name '*-*' -exec rm -f \{\} \;"
+cluster-helper $target -- "find /tmp -name '*.valgrind' -exec rm -f \{\} \;"
+
+cluster-helper $target -- 'service rsyslog restart' > /dev/null 2>&1
+cluster-helper $target -- 'systemctl restart systemd-journald.socket' > /dev/null 2>&1
+cluster-helper $target -- logger -i -p daemon.info __clean_logs__
+
+#touch $cluster_log
+echo `date` ": Clean complete"
diff --git a/tools/cluster-helper.in b/tools/cluster-helper.in
new file mode 100755
index 0000000..d8dac6e
--- /dev/null
+++ b/tools/cluster-helper.in
@@ -0,0 +1,201 @@
+#!@BASH_PATH@
+#
+# Copyright 2011-2023 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.
+#
+
+hosts=
+group=$cluster_name
+user=root
+pdsh=`which pdsh 2>/dev/null`
+ssh=`which qarsh 2>/dev/null`
+scp=`which qacp 2>/dev/null`
+command=list
+format=oneline
+replace="{}"
+
+if [ x$ssh = "x" ]; then
+ ssh=ssh
+ scp=scp
+fi
+
+function helptext() {
+ echo "cluster-helper - A tool for running commands on multiple hosts"
+ echo ""
+ echo "Attempt to use pdsh, qarsh, or ssh (in that order) to execute commands"
+ echo "on multiple hosts"
+ echo ""
+ echo "DSH groups can be configured and specified with -g instead of listing"
+ echo "the individual hosts every time"
+ echo ""
+ echo "Usage: cluster-helper [options] [command]"
+ echo ""
+ echo "Options:"
+ echo "--ssh Force the use of ssh instead of qarsh even if it available"
+ echo "-g, --group Specify the group to operate on/with"
+ echo "-w, --host Specify a host to operate on/with. May be specified multiple times"
+ echo "-f, --format Specifiy the output format When listing hosts or group contents"
+ echo " Allowed values: [oneline], long, short, pdsh, bullet"
+ echo ""
+ echo ""
+ echo "Commands:"
+ echo "--list format List the contents of a group in the specified format"
+ echo "--add name Add supplied (-w) hosts to the named group"
+ echo "--create name Create the named group with the supplied (-w) hosts"
+ echo "--run, -- Treat all subsequent arguments as a command to perform on"
+ echo " the specified command on the hosts or group"
+ echo "--xargs Run the supplied command having replaced any occurrences"
+ echo " of {} with the node name"
+ echo ""
+ echo "--copy file(s) host:file Pass subsequent arguments to scp or qacp"
+ echo " Any occurrences of {} are replaced with the node name"
+ echo "--key Install an ssh key"
+ echo ""
+ exit $1
+}
+
+while true ; do
+ case "$1" in
+ --help|-h|-\?) helptext 0;;
+ -x) set -x; shift;;
+ --ssh) ssh="ssh"; scp="scp"; pdsh=""; shift;;
+ -g|--group) group="$2"; shift; shift;;
+ -w|--host) for h in $2; do
+ hosts="$hosts $h";
+ done
+ shift; shift;;
+ -f|--format) format=$2; shift; shift;;
+ -I) replace=$2; shift; shift;;
+ --list|list) format=$2; command=list; shift; shift;;
+ --add|add) command=group-add; shift;;
+ --create|create) group="$2", command=group-create; shift; shift;;
+ --run|run) command=run; shift;;
+ --copy|copy) command=copy; shift; break ;;
+ --key|key) command=key; shift; break ;;
+ --xargs) command=xargs; shift; break ;;
+ --) command=run; shift; break ;;
+ "") break;;
+ *) helptext 1;;
+ esac
+done
+
+if [ x"$group" = x -a x"$hosts" = x ]; then
+ group=$CTS_GROUP
+fi
+
+function expand() {
+ fmt=$1
+ if [ x$group != x -a -f ~/.dsh/group/$group ]; then
+ hosts=`cat ~/.dsh/group/$group`
+ elif [ x$group != x ]; then
+ echo "Unknown group: $group" >&2
+ exit 1
+ fi
+
+ if [ "x$hosts" != x -a $fmt = oneline ]; then
+ echo $hosts
+
+ elif [ "x$hosts" != x -a $fmt = short ]; then
+ ( for h in $hosts; do
+ echo $h | sed 's:\..*::'
+ done ) | tr '\n' ' '
+ echo ""
+
+ elif [ "x$hosts" != x -a $fmt = pdsh ]; then
+ ( for h in $hosts; do
+ echo "-w $h"
+ done ) | tr '\n' ' '
+ echo ""
+
+ elif [ "x$hosts" != x -a $fmt = long ]; then
+ for h in $hosts; do
+ echo $h
+ done
+
+ elif [ "x$hosts" != x -a $fmt = bullet ]; then
+ for h in $hosts; do
+ echo " * $h"
+ done
+
+ elif [ "x$hosts" != x ]; then
+ echo "Unknown format: $fmt" >&2
+ fi
+}
+
+if [ $command = list ]; then
+ expand $format
+
+elif [ $command = key ]; then
+ hosts=`expand oneline`
+ for h in $hosts; do
+ ssh-copy-id root@$h
+ done
+
+elif [ $command = group-create ]; then
+
+ f=`mktemp`
+ mkdir -p ~/.dsh/group
+
+ if [ -f ~/.dsh/group/$group ]; then
+ echo "Overwriting existing group $group"
+ fi
+
+ for h in $hosts; do
+ echo $h >> $f
+ done
+
+ echo "Creating group $group in ~/.dsh/group"
+ sort -u $f > ~/.dsh/group/$group
+ rm -f $f
+
+elif [ $command = group-add ]; then
+ if [ x$group = x ]; then
+ echo "Please specify a group to append to"
+ exit 1
+ fi
+
+ f=`mktemp`
+ mkdir -p ~/.dsh/group
+
+ if [ -f ~/.dsh/group/$group ]; then
+ cat ~/.dsh/group/$group > $f
+ fi
+
+ for h in $hosts; do
+ echo $h >> $f
+ done
+
+ echo "Appending hosts to group $group in ~/.dsh/group"
+ sort -u $f > ~/.dsh/group/$group
+ rm -f $f
+
+elif [ $command = run ]; then
+ if [ x$pdsh != x ]; then
+ hosts=`expand pdsh`
+ $pdsh -l $user $hosts -- $*
+
+ else
+ hosts=`expand oneline`
+ for n in $hosts; do
+ $ssh -l $user $n -- $* < /dev/null
+ done
+ if [ x"$hosts" = x ]; then
+ echo "No hosts specified"
+ fi
+ fi
+elif [ $command = copy ]; then
+ hosts=`expand oneline`
+ for n in $hosts; do
+ $scp `echo $* | sed 's@'$replace'@'$n'@'`
+ done
+
+elif [ $command = xargs ]; then
+ hosts=`expand oneline`
+ for n in $hosts; do
+ eval `echo $* | sed 's@'$replace'@'$n'@'`
+ done
+fi
diff --git a/tools/cluster-init.in b/tools/cluster-init.in
new file mode 100755
index 0000000..1485c81
--- /dev/null
+++ b/tools/cluster-init.in
@@ -0,0 +1,537 @@
+#!@BASH_PATH@
+#
+# Copyright 2011-2023 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.
+#
+
+accept_defaults=0
+do_raw=0
+ETCHOSTS=0
+nodelist=0
+limit=0
+
+pkgs="corosync xinetd nmap abrt-cli fence-agents perl-TimeDate gdb"
+
+transport="multicast"
+inaddr_any="no"
+
+INSTALL=
+cs_conf=
+fence_conf=
+
+dsh_group=0
+if [ ! -z $cluster_name ]; then
+ cluster=$cluster_name
+else
+ cluster=dummy0
+fi
+
+# Corosync Settings
+cs_port=666
+
+# Settings that work great on nXX
+join=60
+#token=3000
+consensus=1500
+
+# Official settings
+join=2000
+token=5000
+consensus=2500
+
+# Testing
+join=1000
+consensus=7500
+do_debug=off
+
+function ip_for_node() {
+ ping -c 1 $1 | grep "bytes from" | head -n 1 | sed -e 's/.*bytes from//' -e 's/: icmp.*//' | awk '{print $NF}' | sed 's:(::' | sed 's:)::'
+# if [ $do_raw = 1 ]; then
+# echo $1
+# else
+# #host $1 | grep "has address" | head -n 1 | awk '{print $NF}' | sed 's:(::' | sed 's:)::'
+# fi
+}
+function id_for_node() {
+ ip_for_node $* | tr '.' ' ' | awk '{print $4}'
+}
+function name_for_node() {
+ echo $1 | awk -F. '{print $1}'
+}
+
+function helptext() {
+ echo "cluster-init - Configure cluster communication for the infrastructures supported by Pacemaker"
+ echo ""
+ echo "-g, --group Specify the group to operate on/with"
+ echo "-w, --host Specify a host to operate on/with. May be specified multiple times"
+ echo "-r, --raw-ip Supplied nodes were listed as their IP addresses"
+ echo ""
+ echo "-c, --corosync configure for corosync"
+ echo "-C, --nodelist configure for corosync with a node list"
+ echo "-u, --unicast configure point-to-point communication instead of multicast"
+ echo ""
+ echo "-I, --install Install packages"
+ echo ""
+ echo "-d, --debug Enable debug logging for the cluster"
+ echo "--hosts Copy the local /etc/hosts file to all nodes"
+ echo "-e, --extra list Whitespace separated list of extra packages to install"
+ echo "-l, --limit N Use the first N hosts from the named group"
+ echo " Extra packages to install"
+ exit $1
+}
+
+host_input=""
+while true; do
+ case "$1" in
+ -g) cluster=$2;
+ shift; shift;;
+ -w|--host)
+ for h in $2; do
+ host_input="$host_input -w $h";
+ done
+ shift; shift;;
+ -w) host_input="$host_input -w $2"
+ shift; shift;;
+ -r|--raw-ip) do_raw=1; shift;;
+
+ -d|--debug) do_debug=on; shift;;
+
+ -I|--install) INSTALL=Yes; shift;;
+ --hosts) ETCHOSTS=1; shift;;
+
+ -c|--corosync) CTYPE=corosync; shift;;
+ -C|--nodelist) CTYPE=corosync; nodelist=1; shift;;
+ -u|--unicast) nodelist=1; transport=udpu; inaddr_any="yes"; shift;;
+ -e|--extra) pkgs="$pkgs $2"; shift; shift;;
+ -t|--test) pkgs="$pkgs valgrind"; shift;;
+ -l|--limit) limit=$2; shift; shift;;
+
+ r*[0-9])
+ rhel=`echo $1 | sed -e s/rhel// -e s/-// -e s/r//`
+ pkgs="$pkgs qarsh-server";
+ case $rhel in
+ 7) CTYPE=corosync;;
+ esac
+ shift
+ ;;
+
+ f*[0-9][0-9])
+ CTYPE=corosync;
+ shift
+ ;;
+
+ -y|--yes|--defaults) accept_defaults=1; shift;;
+ -x) set -x; shift;;
+ -\?|--help) helptext 0; shift;;
+ "") break;;
+ *) echo "unknown option: $1"; exit 1;;
+ esac
+done
+
+if [ ! -z $cluster ]; then
+ host_input="-g $cluster"
+ # use the last digit present in the variable (if any)
+ dsh_group=`echo $cluster | sed 's/[^0-9][^0-9]*//g;s/.*\([0-9]\)$/\1/'`
+fi
+
+if [ -z $dsh_group ]; then
+ dsh_group=1
+fi
+
+if [ x = "x$host_input" -a x = "x$cluster" ]; then
+ if [ -d $HOME/.dsh/group ]; then
+ read -p "Please specify a dsh group you'd like to configure as a cluster? [] " -t 60 cluster
+ else
+ read -p "Please specify a whitespace delimetered list of nodes you'd like to configure as a cluster? [] " -t 60 host_list
+
+ for h in $2; do
+ host_input="$host_input -w $h";
+ done
+ fi
+fi
+
+if [ -z "$host_input" ]; then
+ echo "You didn't specify any nodes or groups to configure"
+ exit 1
+fi
+
+if [ $limit -gt 0 ]; then
+ echo "Using only the first $limit hosts in $cluster group"
+ host_list=`cluster-helper --list bullet $host_input | head -n $limit | tr '\n*' ' '`
+else
+ host_list=`cluster-helper --list short $host_input`
+fi
+num_hosts=`echo $host_list | wc -w`
+
+if [ $num_hosts -gt 9 ]; then
+ cs_port=66
+fi
+
+for h in $host_list; do
+ ping -c 1 -q $h
+ if [ $? != 0 ]; then
+ echo "Using long names..."
+ host_list=`cluster-helper --list long $host_input`
+ break
+ fi
+done
+
+if [ -z $CTYPE ]; then
+ echo ""
+ read -p "Where should Pacemaker obtain membership and quorum from? [corosync] (corosync) " -t 60 CTYPE
+fi
+
+case $CTYPE in
+ corosync) cs_conf="@PCMK__COROSYNC_CONF@" ;;
+esac
+
+function get_defaults()
+{
+ if [ -z $SSH ]; then
+ SSH="No"
+ fi
+
+ if [ -z $SELINUX ]; then
+ SELINUX="No"
+ fi
+
+ if [ -z $IPTABLES ]; then
+ IPTABLES="Yes"
+ fi
+
+ if [ -z $DOMAIN ]; then
+ DOMAIN="No"
+ fi
+ if [ -z $INSTALL ]; then
+ INSTALL="Yes"
+ fi
+ if [ -z $DATE ]; then
+ DATE="No"
+ fi
+}
+
+get_defaults
+if [ $accept_defaults = 0 ]; then
+ echo ""
+ read -p "Shall I install an ssh key to cluster nodes? [$SSH] " -t 60 SSH
+ echo ""
+ echo "SELinux prevent many things, including password-less ssh logins"
+ read -p "Shall I disable selinux? [$SELINUX] " -t 60 SELINUX
+ echo ""
+ echo "Incorrectly configured firewalls will prevent corosync from starting up"
+ read -p "Shall I disable iptables? [$IPTABLES] " -t 60 IPTABLES
+
+ echo ""
+ read -p "Shall I install/update the relevant packages? [$INSTALL] " -t 60 INSTALL
+
+ echo ""
+ read -p "Shall I sync the date/time? [$DATE] " -t 60 DATE
+fi
+get_defaults
+
+echo ""
+echo "Detecting possible fencing options"
+if [ -e /etc/cluster/fence_xvm.key ]; then
+ echo "* Found fence_xvm"
+ fence_conf=/etc/cluster/fence_xvm.key
+ pkgs="$pkgs fence-virt"
+fi
+
+if [ ! -z ${OS_AUTH_URL} ]; then
+ echo "* Found openstack credentials"
+ fence_conf=/sbin/fence_openstack
+ pkgs="$pkgs python-novaclient"
+fi
+echo ""
+echo "Beginning cluster configuration"
+echo ""
+
+case $SSH in
+ [Yy][Ee][Ss]|[Yy])
+ for host in $host_list; do
+ echo "Installing our ssh key on ${host}"
+ ssh-copy-id root@${host} >/dev/null 2>&1
+ # Fix selinux labeling
+ ssh -l root ${host} -- restorecon -R -v .
+ done
+ ;;
+esac
+
+case $DATE in
+ [Yy][Ee][Ss]|[Yy])
+ for host in $host_list; do
+ echo "Setting time on ${host}"
+ scp /etc/localtime root@${host}:/etc
+ now=`date +%s`
+ ssh -l root ${host} -- date -s @$now
+ echo ""
+ done
+ ;;
+esac
+
+init=`mktemp`
+cat<<-END>$init
+verbose=0
+pkgs="$pkgs"
+
+lhost=\`uname -n\`
+lshort=\`echo \$lhost | awk -F. '{print \$1}'\`
+
+log() {
+ printf "%-10s \$*\n" "\$lshort:" 1>&2
+}
+
+debug() {
+ if [ \$verbose -gt 0 ]; then
+ log "Debug: \$*"
+ fi
+}
+
+info() {
+ log "\$*"
+}
+
+warning() {
+ log "WARN: \$*"
+}
+
+fatal() {
+ log "ERROR: \$*"
+ exit 1
+}
+
+case $SELINUX in
+ [Yy][Ee][Ss]|[Yy])
+ sed -i.sed "s/enforcing/disabled/g" /etc/selinux/config
+ ;;
+esac
+
+case $IPTABLES in
+ [Yy][Ee][Ss]|[Yy]|"")
+ service iptables stop
+ chkconfig iptables off
+ service firewalld stop
+ chkconfig firewalld off
+ ;;
+esac
+
+case $DOMAIN in
+ [Nn][Oo]|"")
+ ;;
+ *.*)
+ if
+ ! grep domain /etc/resolv.conf
+ then
+ sed -i.sed "s/nameserver/domain\ $DOMAIN\\\nnameserver/g" /etc/resolv.conf
+ fi
+ ;;
+ *) echo "Unknown domain: $DOMAIN";;
+esac
+
+case $INSTALL in
+ [Yy][Ee][Ss]|[Yy]|"")
+ info Installing cluster software
+ yum install -y $pkgs pacemaker
+ ;;
+esac
+
+info "Configuring services"
+chkconfig xinetd on
+service xinetd start &>/dev/null
+
+chkconfig corosync off &> /dev/null
+mkdir -p /etc/cluster
+
+info "Turning on core files"
+grep -q "unlimited" /etc/bashrc
+if [ $? = 1 ]; then
+ sed -i.sed "s/bashrc/bashrc\\\nulimit\ -c\ unlimited/g" /etc/bashrc
+fi
+
+function patch_cs_config() {
+ test $num_hosts != 2
+ two_node=$?
+
+ priority="info"
+ if [ $do_debug = 1 ]; then
+ priority="debug"
+ fi
+
+ ssh -l root ${host} -- sed -i.sed "s/.*mcastaddr:.*/mcastaddr:\ 226.94.1.1/g" $cs_conf
+ ssh -l root ${host} -- sed -i.sed "s/.*mcastport:.*/mcastport:\ $cs_port$dsh_group/g" $cs_conf
+ ssh -l root ${host} -- sed -i.sed "s/.*bindnetaddr:.*/bindnetaddr:\ $ip/g" $cs_conf
+ ssh -l root ${host} -- sed -i.sed "s/.*syslog_facility:.*/syslog_facility:\ daemon/g" $cs_conf
+ ssh -l root ${host} -- sed -i.sed "s/.*logfile_priority:.*/logfile_priority:\ $priority/g" $cs_conf
+
+ if [ ! -z $token ]; then
+ ssh -l root ${host} -- sed -i.sed "s/.*token:.*/token:\ $token/g" $cs_conf
+ fi
+ if [ ! -z $consensus ]; then
+ ssh -l root ${host} -- sed -i.sed "s/.*consensus:.*/consensus:\ $consensus/g" $cs_conf
+ fi
+ if [ ! -z $join ]; then
+ ssh -l root ${host} -- sed -i.sed "s/^join:.*/join:\ $join/g" $cs_conf
+ ssh -l root ${host} -- sed -i.sed "s/\\\Wjoin:.*/join:\ $join/g" $cs_conf
+ fi
+
+ ssh -l root ${host} -- grep -q "corosync_votequorum" $cs_conf 2>&1 > /dev/null
+ if [ $? -eq 0 ]; then
+ ssh -l root ${host} -- sed -i.sed "s/\\\Wexpected_votes:.*/expected_votes:\ $num_hosts/g" $cs_conf
+ ssh -l root ${host} -- sed -i.sed "s/\\\Wtwo_node:.*/two_node:\ $two_node/g" $cs_conf
+ else
+ printf "%-10s Wrong quorum provider: installing $cs_conf for corosync instead\n" ${host}
+ create_cs_config
+ fi
+}
+
+function create_cs_config() {
+ cs_tmp=/tmp/cs_conf.$$
+ test $num_hosts != 2
+ two_node=$?
+
+ # Base config
+ priority="info"
+ if [ $do_debug = 1 ]; then
+ priority="debug"
+ fi
+
+ cat <<-END >$cs_tmp
+# Please read the corosync.conf.5 manual page
+totem {
+ version: 2
+
+ # cypto_cipher and crypto_hash: Used for mutual node authentication.
+ # If you choose to enable this, then do remember to create a shared
+ # secret with "corosync-keygen".
+ crypto_cipher: none
+ crypto_hash: none
+
+ # Assign a fixed node id
+ nodeid: $id
+
+ # Disable encryption
+ secauth: off
+
+ transport: $transport
+ inaddr_any: $inaddr_any
+
+ # interface: define at least one interface to communicate
+ # over. If you define more than one interface stanza, you must
+ # also set rrp_mode.
+ interface {
+ # Rings must be consecutively numbered, starting at 0.
+ ringnumber: 0
+
+ # This is normally the *network* address of the
+ # interface to bind to. This ensures that you can use
+ # identical instances of this configuration file
+ # across all your cluster nodes, without having to
+ # modify this option.
+ bindnetaddr: $ip
+
+ # However, if you have multiple physical network
+ # interfaces configured for the same subnet, then the
+ # network address alone is not sufficient to identify
+ # the interface Corosync should bind to. In that case,
+ # configure the *host* address of the interface
+ # instead:
+ # bindnetaddr: 192.168.1.1
+ # When selecting a multicast address, consider RFC
+ # 2365 (which, among other things, specifies that
+ # 239.255.x.x addresses are left to the discretion of
+ # the network administrator). Do not reuse multicast
+ # addresses across multiple Corosync clusters sharing
+ # the same network.
+
+ # Corosync uses the port you specify here for UDP
+ # messaging, and also the immediately preceding
+ # port. Thus if you set this to 5405, Corosync sends
+ # messages over UDP ports 5405 and 5404.
+ mcastport: $cs_port$dsh_group
+
+ # Time-to-live for cluster communication packets. The
+ # number of hops (routers) that this ring will allow
+ # itself to pass. Note that multicast routing must be
+ # specifically enabled on most network routers.
+ ttl: 1
+ mcastaddr: 226.94.1.1
+ }
+}
+
+logging {
+ debug: off
+ fileline: off
+ to_syslog: yes
+ to_stderr: no
+ syslog_facility: daemon
+ timestamp: on
+ to_logfile: yes
+ logfile: /var/log/corosync.log
+ logfile_priority: $priority
+}
+
+amf {
+ mode: disabled
+}
+
+quorum {
+ provider: corosync_votequorum
+ expected_votes: $num_hosts
+ votes: 1
+ two_node: $two_node
+ wait_for_all: 0
+ last_man_standing: 0
+ auto_tie_breaker: 0
+}
+END
+ scp -q $cs_tmp root@${host}:$cs_conf
+ rm -f $cs_tmp
+}
+
+for host in $host_list; do
+ echo ""
+ echo ""
+ echo "* Configuring $host"
+
+ cs_short_host=`name_for_node $host`
+ ip=`ip_for_node $host`
+ id=`id_for_node $host`
+
+ echo $ip | grep -qis NXDOMAIN
+ if [ $? = 0 ]; then
+ echo "Couldn't find resolve $host to an IP address"
+ exit 1
+ fi
+
+ if [ `uname -n` = $host ]; then
+ bash $init
+ else
+ cat $init | ssh -l root -T $host -- "cat > $init; bash $init"
+ fi
+
+ if [ "x$fence_conf" != x ]; then
+ if [ -e $fence_conf ]; then
+ scp $fence_conf root@${host}:$fence_conf
+ fi
+ fi
+
+ if [ $ETCHOSTS = 1 ]; then
+ scp /etc/hosts root@${host}:/etc/hosts
+ fi
+
+ ssh -l root ${host} -- grep -q "token:" $cs_conf 2>&1 > /dev/null
+ new_config=$?
+ new_config=1
+
+ if [ $new_config = 0 ]; then
+ printf "%-10s Updating $cs_conf\n" ${host}:
+ patch_cs_config
+ else
+ printf "%-10s Installing $cs_conf\n" ${host}:
+ create_cs_config
+ fi
+done
diff --git a/tools/crm_attribute.8.inc b/tools/crm_attribute.8.inc
new file mode 100644
index 0000000..2be8253
--- /dev/null
+++ b/tools/crm_attribute.8.inc
@@ -0,0 +1,5 @@
+[synopsis]
+crm_attribute -n <attribute> <command> [options]
+
+/and node attributes/
+.SH OPTIONS
diff --git a/tools/crm_attribute.c b/tools/crm_attribute.c
new file mode 100644
index 0000000..358b150
--- /dev/null
+++ b/tools/crm_attribute.c
@@ -0,0 +1,883 @@
+/*
+ * Copyright 2004-2023 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 <stdint.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <time.h>
+
+#include <sys/param.h>
+#include <sys/types.h>
+
+#include <crm/crm.h>
+#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
+#include <crm/common/ipc.h>
+#include <crm/common/util.h>
+#include <crm/cluster.h>
+
+#include <crm/cib.h>
+#include <crm/cib/internal.h>
+#include <crm/common/attrd_internal.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/ipc_attrd_internal.h>
+#include <crm/common/ipc_controld.h>
+#include <crm/common/output_internal.h>
+#include <sys/utsname.h>
+
+#include <pacemaker-internal.h>
+
+#define SUMMARY "crm_attribute - query and update Pacemaker cluster options and node attributes"
+
+GError *error = NULL;
+crm_exit_t exit_code = CRM_EX_OK;
+uint64_t cib_opts = cib_sync_call;
+
+PCMK__OUTPUT_ARGS("attribute", "const char *", "const char *", "const char *",
+ "const char *", "const char *")
+static int
+attribute_text(pcmk__output_t *out, va_list args)
+{
+ const char *scope = va_arg(args, const char *);
+ const char *instance = va_arg(args, const char *);
+ const char *name = va_arg(args, const char *);
+ const char *value = va_arg(args, const char *);
+ const char *host G_GNUC_UNUSED = va_arg(args, const char *);
+
+ if (out->quiet) {
+ if (value != NULL) {
+ pcmk__formatted_printf(out, "%s\n", value);
+ }
+ } else {
+ out->info(out, "%s%s %s%s %s%s value=%s",
+ scope ? "scope=" : "", scope ? scope : "",
+ instance ? "id=" : "", instance ? instance : "",
+ name ? "name=" : "", name ? name : "",
+ value ? value : "(null)");
+ }
+
+ return pcmk_rc_ok;
+}
+
+static pcmk__supported_format_t formats[] = {
+ PCMK__SUPPORTED_FORMAT_NONE,
+ PCMK__SUPPORTED_FORMAT_TEXT,
+ PCMK__SUPPORTED_FORMAT_XML,
+ { NULL, NULL, NULL }
+};
+
+static pcmk__message_entry_t fmt_functions[] = {
+ { "attribute", "text", attribute_text },
+
+ { NULL, NULL, NULL }
+};
+
+struct {
+ char command;
+ gchar *attr_default;
+ gchar *attr_id;
+ gchar *attr_name;
+ uint32_t attr_options;
+ gchar *attr_pattern;
+ char *attr_value;
+ char *dest_node;
+ gchar *dest_uname;
+ gboolean inhibit;
+ gchar *set_name;
+ char *set_type;
+ gchar *type;
+ gboolean promotion_score;
+} options = {
+ .command = 'G',
+ .promotion_score = FALSE
+};
+
+#define INDENT " "
+
+static gboolean
+delete_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.command = 'D';
+ pcmk__str_update(&options.attr_value, NULL);
+ return TRUE;
+}
+
+static gboolean
+promotion_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ char *score_name = NULL;
+
+ options.promotion_score = TRUE;
+
+ if (options.attr_name) {
+ g_free(options.attr_name);
+ }
+
+ score_name = pcmk_promotion_score_name(optarg);
+ if (score_name != NULL) {
+ options.attr_name = g_strdup(score_name);
+ free(score_name);
+ } else {
+ options.attr_name = NULL;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+update_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.command = 'u';
+ pcmk__str_update(&options.attr_value, optarg);
+ return TRUE;
+}
+
+static gboolean
+utilization_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (options.type) {
+ g_free(options.type);
+ }
+
+ options.type = g_strdup(XML_CIB_TAG_NODES);
+ pcmk__str_update(&options.set_type, XML_TAG_UTILIZATION);
+ return TRUE;
+}
+
+static gboolean
+value_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.command = 'G';
+ pcmk__str_update(&options.attr_value, NULL);
+ return TRUE;
+}
+
+static gboolean
+wait_cb (const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ if (pcmk__str_eq(optarg, "no", pcmk__str_none)) {
+ pcmk__clear_node_attr_flags(options.attr_options, pcmk__node_attr_sync_local | pcmk__node_attr_sync_cluster);
+ return TRUE;
+ } else if (pcmk__str_eq(optarg, PCMK__VALUE_LOCAL, pcmk__str_none)) {
+ pcmk__clear_node_attr_flags(options.attr_options, pcmk__node_attr_sync_local | pcmk__node_attr_sync_cluster);
+ pcmk__set_node_attr_flags(options.attr_options, pcmk__node_attr_sync_local);
+ return TRUE;
+ } else if (pcmk__str_eq(optarg, PCMK__VALUE_CLUSTER, pcmk__str_none)) {
+ pcmk__clear_node_attr_flags(options.attr_options, pcmk__node_attr_sync_local | pcmk__node_attr_sync_cluster);
+ pcmk__set_node_attr_flags(options.attr_options, pcmk__node_attr_sync_cluster);
+ return TRUE;
+ } else {
+ g_set_error(err, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ "--wait= must be one of 'no', 'local', 'cluster'");
+ return FALSE;
+ }
+}
+
+static GOptionEntry selecting_entries[] = {
+ { "id", 'i', 0, G_OPTION_ARG_STRING, &options.attr_id,
+ "(Advanced) Operate on instance of specified attribute with this\n"
+ INDENT "XML ID",
+ "XML_ID"
+ },
+
+ { "name", 'n', 0, G_OPTION_ARG_STRING, &options.attr_name,
+ "Operate on attribute or option with this name. For queries, this\n"
+ INDENT "is optional, in which case all matching attributes will be\n"
+ INDENT "returned.",
+ "NAME"
+ },
+
+ { "pattern", 'P', 0, G_OPTION_ARG_STRING, &options.attr_pattern,
+ "Operate on all attributes matching this pattern\n"
+ INDENT "(with -v, -D, or -G)",
+ "PATTERN"
+ },
+
+ { "promotion", 'p', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, promotion_cb,
+ "Operate on node attribute used as promotion score for specified\n"
+ INDENT "resource, or resource given in OCF_RESOURCE_INSTANCE environment\n"
+ INDENT "variable if none is specified; this also defaults -l/--lifetime\n"
+ INDENT "to reboot (normally invoked from an OCF resource agent)",
+ "RESOURCE"
+ },
+
+ { "set-name", 's', 0, G_OPTION_ARG_STRING, &options.set_name,
+ "(Advanced) Operate on instance of specified attribute that is\n"
+ INDENT "within set with this XML ID",
+ "NAME"
+ },
+
+ { NULL }
+};
+
+static GOptionEntry command_entries[] = {
+ { "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, delete_cb,
+ "Delete the attribute/option",
+ NULL
+ },
+
+ { "query", 'G', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, value_cb,
+ "Query the current value of the attribute/option.\n"
+ INDENT "See also: -n, -P",
+ NULL
+ },
+
+ { "update", 'v', 0, G_OPTION_ARG_CALLBACK, update_cb,
+ "Update the value of the attribute/option",
+ "VALUE"
+ },
+
+ { NULL }
+};
+
+static GOptionEntry addl_entries[] = {
+ { "default", 'd', 0, G_OPTION_ARG_STRING, &options.attr_default,
+ "(Advanced) Default value to display if none is found in configuration",
+ "VALUE"
+ },
+
+ { "lifetime", 'l', 0, G_OPTION_ARG_STRING, &options.type,
+ "Lifetime of the node attribute.\n"
+ INDENT "Valid values: reboot, forever",
+ "LIFETIME"
+ },
+
+ { "node", 'N', 0, G_OPTION_ARG_STRING, &options.dest_uname,
+ "Set a node attribute for named node (instead of a cluster option).\n"
+ INDENT "See also: -l",
+ "NODE"
+ },
+
+ { "type", 't', 0, G_OPTION_ARG_STRING, &options.type,
+ "Which part of the configuration to update/delete/query the option in.\n"
+ INDENT "Valid values: crm_config, rsc_defaults, op_defaults, tickets",
+ "SECTION"
+ },
+
+ { "wait", 'W', 0, G_OPTION_ARG_CALLBACK, wait_cb,
+ "Wait for some event to occur before returning. Values are 'no' (wait\n"
+ INDENT "only for the attribute daemon to acknowledge the request),\n"
+ INDENT "'local' (wait until the change has propagated to where a local\n"
+ INDENT "query will return the request value, or the value set by a\n"
+ INDENT "later request), or 'cluster' (wait until the change has propagated\n"
+ INDENT "to where a query anywhere on the cluster will return the requested\n"
+ INDENT "value, or the value set by a later request). Default is 'no'.\n"
+ INDENT "(with -N, and one of -D or -u)",
+ "UNTIL" },
+
+ { "utilization", 'z', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, utilization_cb,
+ "Set an utilization attribute for the node.",
+ NULL
+ },
+
+ { "inhibit-policy-engine", '!', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.inhibit,
+ NULL, NULL
+ },
+
+ { NULL }
+};
+
+static GOptionEntry deprecated_entries[] = {
+ { "attr-id", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.attr_id,
+ NULL, NULL
+ },
+
+ { "attr-name", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.attr_name,
+ NULL, NULL
+ },
+
+ { "attr-value", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, update_cb,
+ NULL, NULL
+ },
+
+ { "delete-attr", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, delete_cb,
+ NULL, NULL
+ },
+
+ { "get-value", 0, G_OPTION_FLAG_HIDDEN|G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, value_cb,
+ NULL, NULL
+ },
+
+ { "node-uname", 'U', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.dest_uname,
+ NULL, NULL
+ },
+
+ { NULL }
+};
+
+static void
+get_node_name_from_local(void)
+{
+ char *hostname = pcmk_hostname();
+
+ g_free(options.dest_uname);
+
+ /* This silliness is so that dest_uname is always a glib-managed
+ * string so we know how to free it later. pcmk_hostname returns
+ * a newly allocated string via strdup.
+ */
+ options.dest_uname = g_strdup(hostname);
+ free(hostname);
+}
+
+static int
+send_attrd_update(char command, const char *attr_node, const char *attr_name,
+ const char *attr_value, const char *attr_set,
+ const char *attr_dampen, uint32_t attr_options)
+{
+ int rc = pcmk_rc_ok;
+ uint32_t opts = attr_options;
+
+ switch (command) {
+ case 'D':
+ rc = pcmk__attrd_api_delete(NULL, attr_node, attr_name, opts);
+ break;
+
+ case 'u':
+ rc = pcmk__attrd_api_update(NULL, attr_node, attr_name,
+ attr_value, NULL, attr_set, NULL,
+ opts | pcmk__node_attr_value);
+ break;
+ }
+
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__RC_ERROR, rc, "Could not update %s=%s: %s (%d)",
+ attr_name, attr_value, pcmk_rc_str(rc), rc);
+ }
+
+ return rc;
+}
+
+struct delete_data_s {
+ pcmk__output_t *out;
+ cib_t *cib;
+};
+
+static int
+delete_attr_on_node(xmlNode *child, void *userdata)
+{
+ struct delete_data_s *dd = (struct delete_data_s *) userdata;
+
+ const char *attr_name = crm_element_value(child, XML_NVPAIR_ATTR_NAME);
+ int rc = pcmk_rc_ok;
+
+ if (!pcmk__str_eq(attr_name, options.attr_pattern, pcmk__str_regex)) {
+ return pcmk_rc_ok;
+ }
+
+ rc = cib__delete_node_attr(dd->out, dd->cib, cib_opts, options.type,
+ options.dest_node, options.set_type,
+ options.set_name, options.attr_id,
+ attr_name, options.attr_value, NULL);
+
+ if (rc == ENXIO) {
+ rc = pcmk_rc_ok;
+ }
+
+ return rc;
+}
+
+static int
+command_delete(pcmk__output_t *out, cib_t *cib)
+{
+ int rc = pcmk_rc_ok;
+
+ xmlNode *result = NULL;
+ bool use_pattern = options.attr_pattern != NULL;
+
+ /* See the comment in command_query regarding xpath and regular expressions. */
+ if (use_pattern) {
+ struct delete_data_s dd = { out, cib };
+
+ rc = cib__get_node_attrs(out, cib, options.type, options.dest_node,
+ options.set_type, options.set_name, NULL, NULL,
+ NULL, &result);
+
+ if (rc != pcmk_rc_ok) {
+ goto done_deleting;
+ }
+
+ rc = pcmk__xe_foreach_child(result, NULL, delete_attr_on_node, &dd);
+
+ if (rc != pcmk_rc_ok) {
+ goto done_deleting;
+ }
+
+ } else {
+ rc = cib__delete_node_attr(out, cib, cib_opts, options.type, options.dest_node,
+ options.set_type, options.set_name, options.attr_id,
+ options.attr_name, options.attr_value, NULL);
+ }
+
+done_deleting:
+ free_xml(result);
+
+ if (rc == ENXIO) {
+ /* Nothing to delete...
+ * which means it's not there...
+ * which is what the admin wanted
+ */
+ rc = pcmk_rc_ok;
+ }
+
+ return rc;
+}
+
+struct update_data_s {
+ pcmk__output_t *out;
+ cib_t *cib;
+ int is_remote_node;
+};
+
+static int
+update_attr_on_node(xmlNode *child, void *userdata)
+{
+ struct update_data_s *ud = (struct update_data_s *) userdata;
+
+ const char *attr_name = crm_element_value(child, XML_NVPAIR_ATTR_NAME);
+
+ if (!pcmk__str_eq(attr_name, options.attr_pattern, pcmk__str_regex)) {
+ return pcmk_rc_ok;
+ }
+
+ return cib__update_node_attr(ud->out, ud->cib, cib_opts, options.type,
+ options.dest_node, options.set_type,
+ options.set_name, options.attr_id,
+ attr_name, options.attr_value, NULL,
+ ud->is_remote_node ? "remote" : NULL);
+}
+
+static int
+command_update(pcmk__output_t *out, cib_t *cib, int is_remote_node)
+{
+ int rc = pcmk_rc_ok;
+
+ xmlNode *result = NULL;
+ bool use_pattern = options.attr_pattern != NULL;
+
+ CRM_LOG_ASSERT(options.type != NULL);
+ CRM_LOG_ASSERT(options.attr_name != NULL);
+ CRM_LOG_ASSERT(options.attr_value != NULL);
+
+ /* See the comment in command_query regarding xpath and regular expressions. */
+ if (use_pattern) {
+ struct update_data_s ud = { out, cib, is_remote_node };
+
+ rc = cib__get_node_attrs(out, cib, options.type, options.dest_node,
+ options.set_type, options.set_name, NULL, NULL,
+ NULL, &result);
+
+ if (rc != pcmk_rc_ok) {
+ goto done_updating;
+ }
+
+ rc = pcmk__xe_foreach_child(result, NULL, update_attr_on_node, &ud);
+
+ if (rc != pcmk_rc_ok) {
+ goto done_updating;
+ }
+
+ } else {
+ rc = cib__update_node_attr(out, cib, cib_opts, options.type,
+ options.dest_node, options.set_type,
+ options.set_name, options.attr_id,
+ options.attr_name, options.attr_value,
+ NULL, is_remote_node ? "remote" : NULL);
+ }
+
+done_updating:
+ free_xml(result);
+ return rc;
+}
+
+struct output_data_s {
+ pcmk__output_t *out;
+ bool use_pattern;
+ bool did_output;
+};
+
+static int
+output_one_attribute(xmlNode *node, void *userdata)
+{
+ struct output_data_s *od = (struct output_data_s *) userdata;
+
+ const char *name = crm_element_value(node, XML_NVPAIR_ATTR_NAME);
+ const char *value = crm_element_value(node, XML_NVPAIR_ATTR_VALUE);
+ const char *host = crm_element_value(node, PCMK__XA_ATTR_NODE_NAME);
+
+ const char *type = options.type;
+ const char *attr_id = options.attr_id;
+
+ if (od->use_pattern && !pcmk__str_eq(name, options.attr_pattern, pcmk__str_regex)) {
+ return pcmk_rc_ok;
+ }
+
+ od->out->message(od->out, "attribute", type, attr_id, name, value, host);
+ od->did_output = true;
+ crm_info("Read %s='%s' %s%s",
+ pcmk__s(name, "<null>"), pcmk__s(value, ""),
+ options.set_name ? "in " : "", options.set_name ? options.set_name : "");
+
+ return pcmk_rc_ok;
+}
+
+static int
+command_query(pcmk__output_t *out, cib_t *cib)
+{
+ int rc = pcmk_rc_ok;
+
+ xmlNode *result = NULL;
+ bool use_pattern = options.attr_pattern != NULL;
+
+ /* libxml2 doesn't support regular expressions in xpath queries (which is how
+ * cib__get_node_attrs -> find_attr finds attributes). So instead, we'll just
+ * find all the attributes for a given node here by passing NULL for attr_id
+ * and attr_name, and then later see if they match the given pattern.
+ */
+ if (use_pattern) {
+ rc = cib__get_node_attrs(out, cib, options.type, options.dest_node,
+ options.set_type, options.set_name, NULL,
+ NULL, NULL, &result);
+ } else {
+ rc = cib__get_node_attrs(out, cib, options.type, options.dest_node,
+ options.set_type, options.set_name, options.attr_id,
+ options.attr_name, NULL, &result);
+ }
+
+ if (rc == ENXIO && options.attr_default) {
+ /* Make static analysis happy */
+ const char *type = options.type;
+ const char *attr_id = options.attr_id;
+ const char *attr_name = options.attr_name;
+ const char *attr_default = options.attr_default;
+ const char *dest_uname = options.dest_uname;
+
+ out->message(out, "attribute", type, attr_id, attr_name, attr_default,
+ dest_uname);
+ rc = pcmk_rc_ok;
+
+ } else if (rc != pcmk_rc_ok) {
+ // Don't do anything.
+
+ } else if (xml_has_children(result)) {
+ struct output_data_s od = { out, use_pattern, false };
+
+ pcmk__xe_foreach_child(result, NULL, output_one_attribute, &od);
+
+ if (!od.did_output) {
+ rc = ENXIO;
+ }
+
+ } else {
+ struct output_data_s od = { out, use_pattern, false };
+ output_one_attribute(result, &od);
+ }
+
+ free_xml(result);
+ return rc;
+}
+
+static void
+set_type(void)
+{
+ if (options.type == NULL) {
+ if (options.promotion_score) {
+ // Updating a promotion score node attribute
+ options.type = g_strdup(XML_CIB_TAG_STATUS);
+
+ } else if (options.dest_uname != NULL) {
+ // Updating some other node attribute
+ options.type = g_strdup(XML_CIB_TAG_NODES);
+
+ } else {
+ // Updating cluster options
+ options.type = g_strdup(XML_CIB_TAG_CRMCONFIG);
+ }
+
+ } else if (pcmk__str_eq(options.type, "reboot", pcmk__str_casei)) {
+ options.type = g_strdup(XML_CIB_TAG_STATUS);
+
+ } else if (pcmk__str_eq(options.type, "forever", pcmk__str_casei)) {
+ options.type = g_strdup(XML_CIB_TAG_NODES);
+ }
+}
+
+static bool
+use_attrd(void)
+{
+ /* Only go through the attribute manager for transient attributes, and
+ * then only if we're not using a file as the CIB.
+ */
+ return pcmk__str_eq(options.type, XML_CIB_TAG_STATUS, pcmk__str_casei) &&
+ getenv("CIB_file") == NULL && getenv("CIB_shadow") == NULL;
+}
+
+static bool
+try_ipc_update(void)
+{
+ return use_attrd() && (options.command == 'D' || options.command == 'u');
+}
+
+static bool
+pattern_used_correctly(void)
+{
+ /* --pattern can only be used with:
+ * -G (query), -v (update), or -D (delete)
+ */
+ return options.command == 'G' || options.command == 'u' || options.command == 'D';
+}
+
+static bool
+delete_used_correctly(void)
+{
+ return options.command != 'D' || options.attr_name != NULL || options.attr_pattern != NULL;
+}
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
+ GOptionContext *context = NULL;
+
+ GOptionEntry extra_prog_entries[] = {
+ { "quiet", 'q', 0, G_OPTION_ARG_NONE, &(args->quiet),
+ "Print only the value on stdout",
+ NULL },
+
+ { "quiet", 'Q', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &(args->quiet),
+ NULL, NULL
+ },
+
+ { NULL }
+ };
+
+ const char *description = "Examples:\n\n"
+ "Add new node attribute called 'location' with the value of 'office' for host 'myhost':\n\n"
+ "\tcrm_attribute --node myhost --name location --update office\n\n"
+ "Query the value of the 'location' node attribute for host 'myhost':\n\n"
+ "\tcrm_attribute --node myhost --name location --query\n\n"
+ "Change the value of the 'location' node attribute for host 'myhost':\n\n"
+ "\tcrm_attribute --node myhost --name location --update backoffice\n\n"
+ "Delete the 'location' node attribute for host 'myhost':\n\n"
+ "\tcrm_attribute --node myhost --name location --delete\n\n"
+ "Query the value of the 'cluster-delay' cluster option:\n\n"
+ "\tcrm_attribute --type crm_config --name cluster-delay --query\n\n"
+ "Query value of the 'cluster-delay' cluster option and print only the value:\n\n"
+ "\tcrm_attribute --type crm_config --name cluster-delay --query --quiet\n\n";
+
+ context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
+ pcmk__add_main_args(context, extra_prog_entries);
+ g_option_context_set_description(context, description);
+
+ pcmk__add_arg_group(context, "selections", "Selecting attributes:",
+ "Show selecting options", selecting_entries);
+ pcmk__add_arg_group(context, "command", "Commands:",
+ "Show command options", command_entries);
+ pcmk__add_arg_group(context, "additional", "Additional options:",
+ "Show additional options", addl_entries);
+ pcmk__add_arg_group(context, "deprecated", "Deprecated Options:",
+ "Show deprecated options", deprecated_entries);
+
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ cib_t *the_cib = NULL;
+ int is_remote_node = 0;
+
+ int rc = pcmk_rc_ok;
+
+ pcmk__output_t *out = NULL;
+
+ GOptionGroup *output_group = NULL;
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, "NPUdilnpstv");
+ GOptionContext *context = build_arg_context(args, &output_group);
+
+ pcmk__register_formats(output_group, formats);
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("crm_attribute", args->verbosity);
+
+ rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
+ if (rc != pcmk_rc_ok) {
+ exit_code = CRM_EX_ERROR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s",
+ args->output_ty, pcmk_rc_str(rc));
+ goto done;
+ }
+
+ pcmk__register_lib_messages(out);
+ pcmk__register_messages(out, fmt_functions);
+
+ if (args->version) {
+ out->version(out, false);
+ goto done;
+ }
+
+ out->quiet = args->quiet;
+
+ if (options.promotion_score && options.attr_name == NULL) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "-p/--promotion must be called from an OCF resource agent "
+ "or with a resource ID specified");
+ goto done;
+ }
+
+ if (options.inhibit) {
+ crm_warn("Inhibiting notifications for this update");
+ cib__set_call_options(cib_opts, crm_system_name, cib_inhibit_notify);
+ }
+
+ the_cib = cib_new();
+ rc = the_cib->cmds->signon(the_cib, crm_system_name, cib_command);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Could not connect to the CIB: %s", pcmk_rc_str(rc));
+ goto done;
+ }
+
+ set_type();
+
+ // Use default node if not given (except for cluster options and tickets)
+ if (!pcmk__strcase_any_of(options.type, XML_CIB_TAG_CRMCONFIG, XML_CIB_TAG_TICKETS,
+ NULL)) {
+ /* If we are being called from a resource agent via the cluster,
+ * the correct local node name will be passed as an environment
+ * variable. Otherwise, we have to ask the cluster.
+ */
+ const char *target = pcmk__node_attr_target(options.dest_uname);
+
+ if (target != NULL) {
+ g_free(options.dest_uname);
+ options.dest_uname = g_strdup(target);
+ } else if (getenv("CIB_file") != NULL && options.dest_uname == NULL) {
+ get_node_name_from_local();
+ }
+
+ if (options.dest_uname == NULL) {
+ char *node_name = NULL;
+
+ rc = pcmk__query_node_name(out, 0, &node_name, 0);
+
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ free(node_name);
+ goto done;
+ }
+ options.dest_uname = g_strdup(node_name);
+ free(node_name);
+ }
+
+ rc = query_node_uuid(the_cib, options.dest_uname, &options.dest_node, &is_remote_node);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Could not map name=%s to a UUID", options.dest_uname);
+ goto done;
+ }
+ }
+
+ if (!delete_used_correctly()) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Error: must specify attribute name or pattern to delete");
+ goto done;
+ }
+
+ if (options.attr_pattern) {
+ if (options.attr_name) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Error: --name and --pattern cannot be used at the same time");
+ goto done;
+ }
+
+ if (!pattern_used_correctly()) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Error: pattern can only be used with delete, query, or update");
+ goto done;
+ }
+
+ g_free(options.attr_name);
+ options.attr_name = options.attr_pattern;
+ options.attr_options |= pcmk__node_attr_pattern;
+ }
+
+ if (is_remote_node) {
+ options.attr_options |= pcmk__node_attr_remote;
+ }
+
+ if (pcmk__str_eq(options.set_type, XML_TAG_UTILIZATION, pcmk__str_none)) {
+ options.attr_options |= pcmk__node_attr_utilization;
+ }
+
+ if (try_ipc_update() &&
+ (send_attrd_update(options.command, options.dest_uname, options.attr_name,
+ options.attr_value, options.set_name, NULL, options.attr_options) == pcmk_rc_ok)) {
+ crm_info("Update %s=%s sent via pacemaker-attrd",
+ options.attr_name, ((options.command == 'D')? "<none>" : options.attr_value));
+
+ } else if (options.command == 'D') {
+ rc = command_delete(out, the_cib);
+
+ } else if (options.command == 'u') {
+ rc = command_update(out, the_cib, is_remote_node);
+
+ } else {
+ rc = command_query(out, the_cib);
+ }
+
+ if (rc == ENOTUNIQ) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Please choose from one of the matches below and supply the 'id' with --attr-id");
+
+ } else if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Error performing operation: %s", pcmk_strerror(rc));
+ }
+
+done:
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+
+ free(options.attr_default);
+ g_free(options.attr_id);
+ g_free(options.attr_name);
+ free(options.attr_value);
+ free(options.dest_node);
+ g_free(options.dest_uname);
+ g_free(options.set_name);
+ free(options.set_type);
+ g_free(options.type);
+
+ cib__clean_up_connection(&the_cib);
+
+ pcmk__output_and_clear_error(&error, out);
+
+ if (out != NULL) {
+ out->finish(out, exit_code, true, NULL);
+ pcmk__output_free(out);
+ }
+
+ pcmk__unregister_formats();
+ return crm_exit(exit_code);
+}
diff --git a/tools/crm_diff.8.inc b/tools/crm_diff.8.inc
new file mode 100644
index 0000000..4229cbf
--- /dev/null
+++ b/tools/crm_diff.8.inc
@@ -0,0 +1,5 @@
+[synopsis]
+crm_diff original_xml operation [options]
+
+/as a patch/
+.SH OPTIONS
diff --git a/tools/crm_diff.c b/tools/crm_diff.c
new file mode 100644
index 0000000..efe2fcf
--- /dev/null
+++ b/tools/crm_diff.c
@@ -0,0 +1,399 @@
+/*
+ * Copyright 2005-2023 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 <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/param.h>
+#include <sys/types.h>
+
+#include <crm/crm.h>
+#include <crm/msg_xml.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
+#include <crm/common/xml.h>
+#include <crm/common/ipc.h>
+#include <crm/cib.h>
+
+#define SUMMARY "Compare two Pacemaker configurations (in XML format) to produce a custom diff-like output, " \
+ "or apply such an output as a patch"
+
+struct {
+ gboolean apply;
+ gboolean as_cib;
+ gboolean no_version;
+ gboolean raw_1;
+ gboolean raw_2;
+ gboolean use_stdin;
+ char *xml_file_1;
+ char *xml_file_2;
+} options;
+
+gboolean new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+
+static GOptionEntry original_xml_entries[] = {
+ { "original", 'o', 0, G_OPTION_ARG_STRING, &options.xml_file_1,
+ "XML is contained in the named file",
+ "FILE" },
+ { "original-string", 'O', 0, G_OPTION_ARG_CALLBACK, original_string_cb,
+ "XML is contained in the supplied string",
+ "STRING" },
+
+ { NULL }
+};
+
+static GOptionEntry operation_entries[] = {
+ { "new", 'n', 0, G_OPTION_ARG_STRING, &options.xml_file_2,
+ "Compare the original XML to the contents of the named file",
+ "FILE" },
+ { "new-string", 'N', 0, G_OPTION_ARG_CALLBACK, new_string_cb,
+ "Compare the original XML with the contents of the supplied string",
+ "STRING" },
+ { "patch", 'p', 0, G_OPTION_ARG_CALLBACK, patch_cb,
+ "Patch the original XML with the contents of the named file",
+ "FILE" },
+
+ { NULL }
+};
+
+static GOptionEntry addl_entries[] = {
+ { "cib", 'c', 0, G_OPTION_ARG_NONE, &options.as_cib,
+ "Compare/patch the inputs as a CIB (includes versions details)",
+ NULL },
+ { "stdin", 's', 0, G_OPTION_ARG_NONE, &options.use_stdin,
+ "",
+ NULL },
+ { "no-version", 'u', 0, G_OPTION_ARG_NONE, &options.no_version,
+ "Generate the difference without versions details",
+ NULL },
+
+ { NULL }
+};
+
+gboolean
+new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.raw_2 = TRUE;
+ pcmk__str_update(&options.xml_file_2, optarg);
+ return TRUE;
+}
+
+gboolean
+original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.raw_1 = TRUE;
+ pcmk__str_update(&options.xml_file_1, optarg);
+ return TRUE;
+}
+
+gboolean
+patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.apply = TRUE;
+ pcmk__str_update(&options.xml_file_2, optarg);
+ return TRUE;
+}
+
+static void
+print_patch(xmlNode *patch)
+{
+ char *buffer = dump_xml_formatted(patch);
+
+ printf("%s", pcmk__s(buffer, "<null>\n"));
+ free(buffer);
+ fflush(stdout);
+}
+
+// \return Standard Pacemaker return code
+static int
+apply_patch(xmlNode *input, xmlNode *patch, gboolean as_cib)
+{
+ xmlNode *output = copy_xml(input);
+ int rc = xml_apply_patchset(output, patch, as_cib);
+
+ rc = pcmk_legacy2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ fprintf(stderr, "Could not apply patch: %s\n", pcmk_rc_str(rc));
+ free_xml(output);
+ return rc;
+ }
+
+ if (output != NULL) {
+ const char *version;
+ char *buffer;
+
+ print_patch(output);
+
+ version = crm_element_value(output, XML_ATTR_CRM_VERSION);
+ buffer = calculate_xml_versioned_digest(output, FALSE, TRUE, version);
+ crm_trace("Digest: %s", pcmk__s(buffer, "<null>\n"));
+ free(buffer);
+ free_xml(output);
+ }
+ return pcmk_rc_ok;
+}
+
+static void
+log_patch_cib_versions(xmlNode *patch)
+{
+ int add[] = { 0, 0, 0 };
+ int del[] = { 0, 0, 0 };
+
+ const char *fmt = NULL;
+ const char *digest = NULL;
+
+ xml_patch_versions(patch, add, del);
+ fmt = crm_element_value(patch, "format");
+ digest = crm_element_value(patch, XML_ATTR_DIGEST);
+
+ if (add[2] != del[2] || add[1] != del[1] || add[0] != del[0]) {
+ crm_info("Patch: --- %d.%d.%d %s", del[0], del[1], del[2], fmt);
+ crm_info("Patch: +++ %d.%d.%d %s", add[0], add[1], add[2], digest);
+ }
+}
+
+static void
+strip_patch_cib_version(xmlNode *patch, const char **vfields, size_t nvfields)
+{
+ int format = 1;
+
+ crm_element_value_int(patch, "format", &format);
+ if (format == 2) {
+ xmlNode *version_xml = find_xml_node(patch, "version", FALSE);
+
+ if (version_xml) {
+ free_xml(version_xml);
+ }
+
+ } else {
+ int i = 0;
+
+ const char *tags[] = {
+ XML_TAG_DIFF_REMOVED,
+ XML_TAG_DIFF_ADDED,
+ };
+
+ for (i = 0; i < PCMK__NELEM(tags); i++) {
+ xmlNode *tmp = NULL;
+ int lpc;
+
+ tmp = find_xml_node(patch, tags[i], FALSE);
+ if (tmp) {
+ for (lpc = 0; lpc < nvfields; lpc++) {
+ xml_remove_prop(tmp, vfields[lpc]);
+ }
+
+ tmp = find_xml_node(tmp, XML_TAG_CIB, FALSE);
+ if (tmp) {
+ for (lpc = 0; lpc < nvfields; lpc++) {
+ xml_remove_prop(tmp, vfields[lpc]);
+ }
+ }
+ }
+ }
+ }
+}
+
+// \return Standard Pacemaker return code
+static int
+generate_patch(xmlNode *object_1, xmlNode *object_2, const char *xml_file_2,
+ gboolean as_cib, gboolean no_version)
+{
+ xmlNode *output = NULL;
+ int rc = pcmk_rc_ok;
+
+ pcmk__output_t *logger_out = NULL;
+ int out_rc = pcmk_rc_no_output;
+ int temp_rc = pcmk_rc_no_output;
+
+ const char *vfields[] = {
+ XML_ATTR_GENERATION_ADMIN,
+ XML_ATTR_GENERATION,
+ XML_ATTR_NUMUPDATES,
+ };
+
+ rc = pcmk__log_output_new(&logger_out);
+ CRM_CHECK(rc == pcmk_rc_ok, return rc);
+
+ /* If we're ignoring the version, make the version information
+ * identical, so it isn't detected as a change. */
+ if (no_version) {
+ int lpc;
+
+ for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
+ crm_copy_xml_element(object_1, object_2, vfields[lpc]);
+ }
+ }
+
+ xml_track_changes(object_2, NULL, object_2, FALSE);
+ if(as_cib) {
+ xml_calculate_significant_changes(object_1, object_2);
+ } else {
+ xml_calculate_changes(object_1, object_2);
+ }
+ crm_log_xml_debug(object_2, (xml_file_2? xml_file_2: "target"));
+
+ output = xml_create_patchset(0, object_1, object_2, NULL, FALSE);
+
+ pcmk__output_set_log_level(logger_out, LOG_INFO);
+ out_rc = pcmk__xml_show_changes(logger_out, object_2);
+
+ xml_accept_changes(object_2);
+
+ if (output == NULL) {
+ goto done; // rc == pcmk_rc_ok
+ }
+
+ /* pcmk_rc_error means there's non-empty diff.
+ * @COMPAT: Choose a more descriptive return code, like one that maps to
+ * CRM_EX_DIGEST?
+ */
+ rc = pcmk_rc_error;
+
+ patchset_process_digest(output, object_1, object_2, as_cib);
+
+ if (as_cib) {
+ log_patch_cib_versions(output);
+
+ } else if (no_version) {
+ strip_patch_cib_version(output, vfields, PCMK__NELEM(vfields));
+ }
+
+ pcmk__output_set_log_level(logger_out, LOG_NOTICE);
+ temp_rc = logger_out->message(logger_out, "xml-patchset", output);
+ out_rc = pcmk__output_select_rc(out_rc, temp_rc);
+
+ print_patch(output);
+ free_xml(output);
+
+done:
+ logger_out->finish(logger_out, pcmk_rc2exitc(out_rc), true, NULL);
+ pcmk__output_free(logger_out);
+
+ return rc;
+}
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args) {
+ GOptionContext *context = NULL;
+
+ const char *description = "Examples:\n\n"
+ "Obtain the two different configuration files by running cibadmin on the two cluster setups to compare:\n\n"
+ "\t# cibadmin --query > cib-old.xml\n\n"
+ "\t# cibadmin --query > cib-new.xml\n\n"
+ "Calculate and save the difference between the two files:\n\n"
+ "\t# crm_diff --original cib-old.xml --new cib-new.xml > patch.xml\n\n"
+ "Apply the patch to the original file:\n\n"
+ "\t# crm_diff --original cib-old.xml --patch patch.xml > updated.xml\n\n"
+ "Apply the patch to the running cluster:\n\n"
+ "\t# cibadmin --patch -x patch.xml\n";
+
+ context = pcmk__build_arg_context(args, NULL, NULL, NULL);
+ g_option_context_set_description(context, description);
+
+ pcmk__add_arg_group(context, "xml", "Original XML:",
+ "Show original XML options", original_xml_entries);
+ pcmk__add_arg_group(context, "operation", "Operation:",
+ "Show operation options", operation_entries);
+ pcmk__add_arg_group(context, "additional", "Additional Options:",
+ "Show additional options", addl_entries);
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ xmlNode *object_1 = NULL;
+ xmlNode *object_2 = NULL;
+
+ crm_exit_t exit_code = CRM_EX_OK;
+ GError *error = NULL;
+
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, "nopNO");
+ GOptionContext *context = build_arg_context(args);
+
+ int rc = pcmk_rc_ok;
+
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("crm_diff", args->verbosity);
+
+ if (args->version) {
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+ /* FIXME: When crm_diff is converted to use formatted output, this can go. */
+ pcmk__cli_help('v');
+ }
+
+ if (options.apply && options.no_version) {
+ fprintf(stderr, "warning: -u/--no-version ignored with -p/--patch\n");
+ } else if (options.as_cib && options.no_version) {
+ fprintf(stderr, "error: -u/--no-version incompatible with -c/--cib\n");
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ if (options.raw_1) {
+ object_1 = string2xml(options.xml_file_1);
+
+ } else if (options.use_stdin) {
+ fprintf(stderr, "Input first XML fragment:");
+ object_1 = stdin2xml();
+
+ } else if (options.xml_file_1 != NULL) {
+ object_1 = filename2xml(options.xml_file_1);
+ }
+
+ if (options.raw_2) {
+ object_2 = string2xml(options.xml_file_2);
+
+ } else if (options.use_stdin) {
+ fprintf(stderr, "Input second XML fragment:");
+ object_2 = stdin2xml();
+
+ } else if (options.xml_file_2 != NULL) {
+ object_2 = filename2xml(options.xml_file_2);
+ }
+
+ if (object_1 == NULL) {
+ fprintf(stderr, "Could not parse the first XML fragment\n");
+ exit_code = CRM_EX_DATAERR;
+ goto done;
+ }
+ if (object_2 == NULL) {
+ fprintf(stderr, "Could not parse the second XML fragment\n");
+ exit_code = CRM_EX_DATAERR;
+ goto done;
+ }
+
+ if (options.apply) {
+ rc = apply_patch(object_1, object_2, options.as_cib);
+ } else {
+ rc = generate_patch(object_1, object_2, options.xml_file_2, options.as_cib, options.no_version);
+ }
+ exit_code = pcmk_rc2exitc(rc);
+
+done:
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+ free(options.xml_file_1);
+ free(options.xml_file_2);
+ free_xml(object_1);
+ free_xml(object_2);
+
+ pcmk__output_and_clear_error(&error, NULL);
+ crm_exit(exit_code);
+}
diff --git a/tools/crm_error.8.inc b/tools/crm_error.8.inc
new file mode 100644
index 0000000..7ef0440
--- /dev/null
+++ b/tools/crm_error.8.inc
@@ -0,0 +1,5 @@
+[synopsis]
+crm_error [options] -- <rc> [...]
+
+/Pacemaker error code/
+.SH OPTIONS
diff --git a/tools/crm_error.c b/tools/crm_error.c
new file mode 100644
index 0000000..8911eae
--- /dev/null
+++ b/tools/crm_error.c
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2012-2023 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 <crm/msg_xml.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
+#include <crm/common/strings_internal.h>
+
+#include <crm/crm.h>
+
+#include <pacemaker-internal.h>
+
+#define SUMMARY "crm_error - display name or description of a Pacemaker error code"
+
+struct {
+ gboolean with_name;
+ gboolean do_list;
+ enum pcmk_result_type result_type; // How to interpret result codes
+} options = {
+ .result_type = pcmk_result_legacy,
+};
+
+static gboolean
+result_type_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **error)
+{
+ if (pcmk__str_any_of(option_name, "--exit", "-X", NULL)) {
+ options.result_type = pcmk_result_exitcode;
+ } else if (pcmk__str_any_of(option_name, "--rc", "-r", NULL)) {
+ options.result_type = pcmk_result_rc;
+ }
+
+ return TRUE;
+}
+
+static GOptionEntry entries[] = {
+ { "name", 'n', 0, G_OPTION_ARG_NONE, &options.with_name,
+ "Show error's name with its description (useful for looking for sources "
+ "of the error in source code)",
+ NULL },
+ { "list", 'l', 0, G_OPTION_ARG_NONE, &options.do_list,
+ "Show all known errors (enabled by default if no rc is specified)",
+ NULL },
+ { "exit", 'X', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, result_type_cb,
+ "Interpret as exit code rather than legacy function return value",
+ NULL },
+ { "rc", 'r', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, result_type_cb,
+ "Interpret as return code rather than legacy function return value",
+ NULL },
+
+ { NULL }
+};
+
+static pcmk__supported_format_t formats[] = {
+ PCMK__SUPPORTED_FORMAT_NONE,
+ PCMK__SUPPORTED_FORMAT_TEXT,
+ PCMK__SUPPORTED_FORMAT_XML,
+ { NULL, NULL, NULL }
+};
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
+ GOptionContext *context = NULL;
+
+ context = pcmk__build_arg_context(args, "text (default), xml", group,
+ "[-- <rc> [<rc>...]]");
+ pcmk__add_main_args(context, entries);
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ crm_exit_t exit_code = CRM_EX_OK;
+ int rc = pcmk_rc_ok;
+
+ pcmk__output_t *out = NULL;
+
+ GError *error = NULL;
+
+ GOptionGroup *output_group = NULL;
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, NULL);
+ GOptionContext *context = build_arg_context(args, &output_group);
+
+ pcmk__register_formats(output_group, formats);
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("crm_error", args->verbosity);
+
+ rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
+ if (rc != pcmk_rc_ok) {
+ exit_code = CRM_EX_ERROR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Error creating output format %s: %s", args->output_ty,
+ pcmk_rc_str(rc));
+ goto done;
+ }
+
+ if (g_strv_length(processed_args) < 2) {
+ // If no result codes were specified, list them all
+ options.do_list = TRUE;
+ }
+
+ if (args->version) {
+ out->version(out, false);
+ goto done;
+ }
+
+ pcmk__register_lib_messages(out);
+
+ if (options.do_list) {
+ uint32_t flags = pcmk_rc_disp_code|pcmk_rc_disp_desc;
+
+ if (options.with_name) {
+ flags = pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE,
+ "pcmk_rc_disp_flags",
+ "pcmk__list_result_codes", flags,
+ pcmk_rc_disp_name, "pcmk_rc_disp_name");
+ }
+ pcmk__list_result_codes(out, options.result_type, flags);
+
+ } else {
+ uint32_t flags = pcmk_rc_disp_desc;
+
+ // For text output, print only "[name -] description" by default
+ if (args->verbosity > 0) {
+ flags = pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE,
+ "pcmk_rc_disp_flags",
+ "pcmk__show_result_code", flags,
+ pcmk_rc_disp_code, "pcmk_rc_disp_code");
+ }
+
+ if (options.with_name) {
+ flags = pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE,
+ "pcmk_rc_disp_flags",
+ "pcmk__show_result_code", flags,
+ pcmk_rc_disp_name, "pcmk_rc_disp_name");
+ }
+
+ /* Skip #1 because that's the program name. */
+ for (int lpc = 1; processed_args[lpc] != NULL; lpc++) {
+ int code = 0;
+
+ if (pcmk__str_eq(processed_args[lpc], "--", pcmk__str_none)) {
+ continue;
+ }
+ pcmk__scan_min_int(processed_args[lpc], &code, INT_MIN);
+ pcmk__show_result_code(out, code, options.result_type, flags);
+ }
+ }
+
+ done:
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+
+ pcmk__output_and_clear_error(&error, out);
+
+ if (out != NULL) {
+ out->finish(out, exit_code, true, NULL);
+ pcmk__output_free(out);
+ }
+ pcmk__unregister_formats();
+ crm_exit(exit_code);
+}
diff --git a/tools/crm_failcount.in b/tools/crm_failcount.in
new file mode 100755
index 0000000..f70fe78
--- /dev/null
+++ b/tools/crm_failcount.in
@@ -0,0 +1,294 @@
+#!@BASH_PATH@
+#
+# Copyright 2009-2018 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.
+#
+
+USAGE_TEXT="Usage: crm_failcount <command> [<options>]
+Common options:
+ --help Display this text, then exit
+ --version Display version information, then exit
+ -V, --verbose Specify multiple times to increase debug output
+ -q, --quiet Print only the value (if querying)
+
+Commands:
+ -G, --query Query the current value of the resource's fail count
+ -D, --delete Delete resource's recorded failures
+
+Additional Options:
+ -r, --resource=value Name of the resource to use (required)
+ -n, --operation=value Name of operation to use (instead of all operations)
+ -I, --interval=value If operation is specified, its interval
+ -N, --node=value Use failcount on named node (instead of local node)"
+
+
+HELP_TEXT="crm_failcount - Query or delete resource fail counts
+
+$USAGE_TEXT"
+
+# These constants must track crm_exit_t values
+CRM_EX_OK=0
+CRM_EX_USAGE=64
+CRM_EX_NOSUCH=105
+
+exit_usage() {
+ if [ $# -gt 0 ]; then
+ echo "error:" "$@" >&2
+ fi
+ echo
+ echo "$USAGE_TEXT"
+ exit $CRM_EX_USAGE
+}
+
+warn() {
+ echo "warning:" "$@" >&2
+}
+
+interval_re() {
+ echo "^[[:blank:]]*([0-9]+)[[:blank:]]*(${1})[[:blank:]]*$"
+}
+
+# This function should follow crm_get_interval() as closely as possible
+parse_interval() {
+ INT_S="$1"
+
+ INT_8601RE="^P(([0-9]+)Y)?(([0-9]+)M)?(([0-9]+)D)?T?(([0-9]+)H)?(([0-9]+)M)?(([0-9]+)S)?$"
+
+ if [[ $INT_S =~ $(interval_re "") ]]; then
+ echo $(( ${BASH_REMATCH[1]} * 1000 ))
+
+ elif [[ $INT_S =~ $(interval_re "s|sec") ]]; then
+ echo $(( ${BASH_REMATCH[1]} * 1000 ))
+
+ elif [[ $INT_S =~ $(interval_re "ms|msec") ]]; then
+ echo "${BASH_REMATCH[1]}"
+
+ elif [[ $INT_S =~ $(interval_re "m|min") ]]; then
+ echo $(( ${BASH_REMATCH[1]} * 60000 ))
+
+ elif [[ $INT_S =~ $(interval_re "h|hr") ]]; then
+ echo $(( ${BASH_REMATCH[1]} * 3600000 ))
+
+ elif [[ $INT_S =~ $(interval_re "us|usec") ]]; then
+ echo $(( ${BASH_REMATCH[1]} / 1000 ))
+
+ elif [[ $INT_S =~ ^P([0-9]+)W$ ]]; then
+ echo $(( ${BASH_REMATCH[1]} * 604800000 ))
+
+ elif [[ $INT_S =~ $INT_8601RE ]]; then
+ echo $(( ( ${BASH_REMATCH[2]:-0} * 31536000000 ) \
+ + ( ${BASH_REMATCH[4]:-0} * 2592000000 ) \
+ + ( ${BASH_REMATCH[6]:-0} * 86400000 ) \
+ + ( ${BASH_REMATCH[8]:-0} * 3600000 ) \
+ + ( ${BASH_REMATCH[10]:-0} * 60000 ) \
+ + ( ${BASH_REMATCH[12]:-0} * 1000 ) ))
+
+ else
+ warn "Unrecognized interval, using 0"
+ echo "0"
+ fi
+}
+
+query_single_attr() {
+ QSR_TARGET="$1"
+ QSR_ATTR="$2"
+
+ crm_attribute $VERBOSE --quiet --query -t status -d 0 \
+ -N "$QSR_TARGET" -n "$QSR_ATTR"
+}
+
+query_attr_sum() {
+ QAS_TARGET="$1"
+ QAS_PREFIX="$2"
+
+ # Build xpath to match all transient node attributes with prefix
+ QAS_XPATH="/cib/status/node_state[@uname='${QAS_TARGET}']"
+ QAS_XPATH="${QAS_XPATH}/transient_attributes/instance_attributes"
+ QAS_XPATH="${QAS_XPATH}/nvpair[starts-with(@name,'$QAS_PREFIX')]"
+
+ # Query attributes that match xpath
+ # @TODO We ignore stderr because we don't want "no results" to look
+ # like an error, but that also makes $VERBOSE pointless.
+ QAS_ALL=$(cibadmin --query --sync-call --local \
+ --xpath="$QAS_XPATH" 2>/dev/null)
+ QAS_EX=$?
+
+ # "No results" is not an error
+ if [ $QAS_EX -ne $CRM_EX_OK ] && [ $QAS_EX -ne $CRM_EX_NOSUCH ]; then
+ echo "error: could not query CIB for fail counts" >&2
+ exit $QAS_EX
+ fi
+
+ # Extract the attribute values (one per line) from the output
+ QAS_VALUE=$(echo "$QAS_ALL" | sed -n -e \
+ 's/.*<nvpair.*value="\([0-9][0-9]*\|INFINITY\)".*>.*/\1/p')
+
+ # Sum the values
+ QAS_SUM=0
+ for i in 0 $QAS_VALUE; do
+ if [ "$i" = "INFINITY" ]; then
+ QAS_SUM="INFINITY"
+ break
+ else
+ QAS_SUM=$(($QAS_SUM + $i))
+ fi
+ done
+ if [ "$QAS_SUM" = "INFINITY" ]; then
+ echo $QAS_SUM
+ elif [ "$QAS_SUM" -ge 1000000 ]; then
+ echo "INFINITY"
+ else
+ echo $QAS_SUM
+ fi
+}
+
+query_failcount() {
+ QF_TARGET="$1"
+ QF_RESOURCE="$2"
+ QF_OPERATION="$3"
+ QF_INTERVAL="$4"
+
+ QF_ATTR_RSC="fail-count-${QF_RESOURCE}"
+
+ if [ -n "$QF_OPERATION" ]; then
+ QF_ATTR_DISPLAY="${QF_ATTR_RSC}#${QF_OPERATION}_${QF_INTERVAL}"
+ QF_COUNT=$(query_single_attr "$QF_TARGET" "$QF_ATTR_DISPLAY")
+ else
+ QF_ATTR_DISPLAY="$QF_ATTR_RSC"
+ QF_COUNT=$(query_attr_sum "$QF_TARGET" "${QF_ATTR_RSC}#")
+ fi
+
+ # @COMPAT attributes set < 1.1.17:
+ # If we didn't find any per-operation failcount,
+ # check whether there is a legacy per-resource failcount.
+ if [ "$QF_COUNT" = "0" ]; then
+ QF_COUNT=$(query_single_attr "$QF_TARGET" "$QF_ATTR_RSC")
+ if [ "$QF_COUNT" != "0" ]; then
+ QF_ATTR_DISPLAY="$QF_ATTR_RSC"
+ fi
+ fi
+
+ # Echo result (comparable to crm_attribute, for backward compatibility)
+ if [ -n "$QUIET" ]; then
+ echo $QF_COUNT
+ else
+ echo "scope=status name=$QF_ATTR_DISPLAY value=$QF_COUNT"
+ fi
+}
+
+clear_failcount() {
+ CF_TARGET="$1"
+ CF_RESOURCE="$2"
+ CF_OPERATION="$3"
+ CF_INTERVAL="$4"
+
+ if [ -n "$CF_OPERATION" ]; then
+ CF_OPERATION="-n $CF_OPERATION -I ${CF_INTERVAL}ms"
+ fi
+ crm_resource $QUIET $VERBOSE --cleanup \
+ -N "$CF_TARGET" -r "$CF_RESOURCE" $CF_OPERATION
+}
+
+QUIET=""
+VERBOSE=""
+
+command=""
+resource=""
+operation=""
+interval="0"
+target=$(crm_node -n 2>/dev/null)
+
+SHORTOPTS="qDGQVN:U:v:i:l:r:n:I:"
+
+LONGOPTS_COMMON="help,version,verbose,quiet"
+LONGOPTS_COMMANDS="query,delete"
+LONGOPTS_OTHER="resource:,node:,operation:,interval:"
+LONGOPTS_COMPAT="delete-attr,get-value,resource-id:,uname:,lifetime:,attr-value:,attr-id:"
+
+LONGOPTS="$LONGOPTS_COMMON,$LONGOPTS_COMMANDS,$LONGOPTS_OTHER,$LONGOPTS_COMPAT"
+
+TEMP=$(@GETOPT_PATH@ -o $SHORTOPTS --long $LONGOPTS -n crm_failcount -- "$@")
+if [ $? -ne 0 ]; then
+ exit_usage
+fi
+eval set -- "$TEMP" # Quotes around $TEMP are essential
+
+while true ; do
+ case "$1" in
+ --help)
+ echo "$HELP_TEXT"
+ exit $CRM_EX_OK
+ ;;
+ --version)
+ crm_attribute --version
+ exit $?
+ ;;
+ -q|-Q|--quiet)
+ QUIET="--quiet"
+ shift
+ ;;
+ -V|--verbose)
+ VERBOSE="$VERBOSE $1"
+ shift
+ ;;
+ -G|--query|--get-value)
+ command="--query"
+ shift
+ ;;
+ -D|--delete|--delete-attr)
+ command="--delete"
+ shift
+ ;;
+ -r|--resource|--resource-id)
+ resource="$2"
+ shift 2
+ ;;
+ -n|--operation)
+ operation="$2"
+ shift 2
+ ;;
+ -I|--interval)
+ interval="$2"
+ shift 2
+ ;;
+ -N|--node|-U|--uname)
+ target="$2"
+ shift 2
+ ;;
+ -v|--attr-value)
+ if [ "$2" = "0" ]; then
+ command="--delete"
+ else
+ warn "ignoring deprecated option '$1' with nonzero value"
+ fi
+ shift 2
+ ;;
+ -i|--attr-id|-l|--lifetime)
+ warn "ignoring deprecated option '$1'"
+ shift 2
+ ;;
+ --)
+ shift
+ break
+ ;;
+ *)
+ exit_usage "unknown option '$1'"
+ ;;
+ esac
+done
+
+[ -n "$command" ] || exit_usage "must specify a command"
+[ -n "$resource" ] || exit_usage "resource name required"
+[ -n "$target" ] || exit_usage "node name required"
+
+interval=$(parse_interval $interval)
+
+if [ "$command" = "--query" ]; then
+ query_failcount "$target" "$resource" "$operation" "$interval"
+else
+ clear_failcount "$target" "$resource" "$operation" "$interval"
+fi
diff --git a/tools/crm_master.in b/tools/crm_master.in
new file mode 100755
index 0000000..a4769f4
--- /dev/null
+++ b/tools/crm_master.in
@@ -0,0 +1,92 @@
+#!@BASH_PATH@
+#
+# Copyright 2009-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.
+#
+
+USAGE_TEXT="Usage: crm_master <command> [<options>]
+
+This command is deprecated. Use crm_attribute with the --promotion option
+instead."
+
+exit_usage() {
+ if [ $# -gt 0 ]; then
+ echo "error:" "$@" >&2
+ fi
+ echo
+ echo "$USAGE_TEXT"
+ exit 1
+}
+
+SHORTOPTS_DEPRECATED="U:Q"
+LONGOPTS_DEPRECATED="uname:,get-value,delete-attr,attr-value:,attr-id:"
+SHORTOPTS="VqGv:DN:l:i:r:"
+LONGOPTS="help,version,verbose,quiet,query,update:,delete,node:,lifetime:,id:,resource:"
+
+TEMP=$(@GETOPT_PATH@ -o ${SHORTOPTS}${SHORTOPTS_DEPRECATED} \
+ --long ${LONGOPTS},${LONGOPTS_DEPRECATED} \
+ -n crm_master -- "$@")
+if [ $? -ne 0 ]; then
+ exit_usage
+fi
+
+eval set -- "$TEMP" # Quotes around $TEMP are essential
+
+# Explicitly set the (usual default) lifetime, so the attribute gets set as a
+# node attribute and not a cluster property.
+options="--lifetime forever"
+
+while true ; do
+ case "$1" in
+ --help)
+ echo "crm_master - Query, update, or delete a resource's promotion score"
+ echo
+ echo "$USAGE_TEXT"
+ exit 0
+ ;;
+ --version)
+ crm_attribute --version
+ exit 0
+ ;;
+ --verbose|-V|--quiet|-q|--query|-G|--delete|-D)
+ options="$options $1"
+ shift
+ ;;
+ --update|-v|--node|-N|--lifetime|-l|--id|-i)
+ options="$options $1 $2"
+ shift
+ shift
+ ;;
+ -r|--resource)
+ OCF_RESOURCE_INSTANCE=$2;
+ shift
+ shift
+ ;;
+ --get-value|--delete-attr|-Q) # deprecated
+ options="$options $1"
+ shift
+ ;;
+ --uname|-U|--attr-value|--attr-id) # deprecated
+ options="$options $1 $2"
+ shift
+ shift
+ ;;
+ --)
+ shift
+ break
+ ;;
+ *)
+ exit_usage "unknown option '$1'"
+ ;;
+ esac
+done
+
+if [ -z "$OCF_RESOURCE_INSTANCE" ]; then
+ exit_usage "No resource specified"
+fi
+
+crm_attribute -n master-$OCF_RESOURCE_INSTANCE $options
diff --git a/tools/crm_mon.8.inc b/tools/crm_mon.8.inc
new file mode 100644
index 0000000..4bc2d82
--- /dev/null
+++ b/tools/crm_mon.8.inc
@@ -0,0 +1,17 @@
+[synopsis]
+crm_mon [options]
+
+/number of different formats/
+.SH OPTIONS
+
+/less descriptive in output./
+.SH NOTES
+
+/period specification./
+.SH OUTPUT CONTROL
+
+/command line arguments./
+.SH TIME SPECIFICATION
+
+/or --exclude=list./
+.SH INTERACTIVE USE
diff --git a/tools/crm_mon.c b/tools/crm_mon.c
new file mode 100644
index 0000000..c20766c
--- /dev/null
+++ b/tools/crm_mon.c
@@ -0,0 +1,2287 @@
+/*
+ * Copyright 2004-2023 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 <sys/param.h>
+
+#include <crm/crm.h>
+
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <signal.h>
+#include <sys/utsname.h>
+
+#include <crm/msg_xml.h>
+#include <crm/services.h>
+#include <crm/lrmd.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/internal.h> // pcmk__ends_with_ext()
+#include <crm/common/ipc.h>
+#include <crm/common/mainloop.h>
+#include <crm/common/output.h>
+#include <crm/common/output_internal.h>
+#include <crm/common/results.h>
+#include <crm/common/util.h>
+#include <crm/common/xml.h>
+#include <crm/common/xml_internal.h>
+
+#include <crm/cib/internal.h>
+#include <crm/pengine/status.h>
+#include <crm/pengine/internal.h>
+#include <pacemaker-internal.h>
+#include <crm/stonith-ng.h>
+#include <crm/fencing/internal.h>
+
+#include "crm_mon.h"
+
+#define SUMMARY "Provides a summary of cluster's current state.\n\n" \
+ "Outputs varying levels of detail in a number of different formats."
+
+/*
+ * Definitions indicating which items to print
+ */
+
+static uint32_t show;
+static uint32_t show_opts = pcmk_show_pending;
+
+/*
+ * Definitions indicating how to output
+ */
+
+static mon_output_format_t output_format = mon_output_unset;
+
+/* other globals */
+static GIOChannel *io_channel = NULL;
+static GMainLoop *mainloop = NULL;
+static guint reconnect_timer = 0;
+static mainloop_timer_t *refresh_timer = NULL;
+
+static enum pcmk_pacemakerd_state pcmkd_state = pcmk_pacemakerd_state_invalid;
+static cib_t *cib = NULL;
+static stonith_t *st = NULL;
+static xmlNode *current_cib = NULL;
+
+static GError *error = NULL;
+static pcmk__common_args_t *args = NULL;
+static pcmk__output_t *out = NULL;
+static GOptionContext *context = NULL;
+static gchar **processed_args = NULL;
+
+static time_t last_refresh = 0;
+volatile crm_trigger_t *refresh_trigger = NULL;
+
+static enum pcmk__fence_history fence_history = pcmk__fence_history_none;
+
+int interactive_fence_level = 0;
+
+static pcmk__supported_format_t formats[] = {
+#if CURSES_ENABLED
+ CRM_MON_SUPPORTED_FORMAT_CURSES,
+#endif
+ PCMK__SUPPORTED_FORMAT_HTML,
+ PCMK__SUPPORTED_FORMAT_NONE,
+ PCMK__SUPPORTED_FORMAT_TEXT,
+ PCMK__SUPPORTED_FORMAT_XML,
+ { NULL, NULL, NULL }
+};
+
+PCMK__OUTPUT_ARGS("crm-mon-disconnected", "const char *",
+ "enum pcmk_pacemakerd_state")
+static int
+crm_mon_disconnected_default(pcmk__output_t *out, va_list args)
+{
+ return pcmk_rc_no_output;
+}
+
+PCMK__OUTPUT_ARGS("crm-mon-disconnected", "const char *",
+ "enum pcmk_pacemakerd_state")
+static int
+crm_mon_disconnected_html(pcmk__output_t *out, va_list args)
+{
+ const char *desc = va_arg(args, const char *);
+ enum pcmk_pacemakerd_state state =
+ (enum pcmk_pacemakerd_state) va_arg(args, int);
+
+ if (out->dest != stdout) {
+ out->reset(out);
+ }
+
+ pcmk__output_create_xml_text_node(out, "span", "Not connected to CIB");
+
+ if (desc != NULL) {
+ pcmk__output_create_xml_text_node(out, "span", ": ");
+ pcmk__output_create_xml_text_node(out, "span", desc);
+ }
+
+ if (state != pcmk_pacemakerd_state_invalid) {
+ const char *state_s = pcmk__pcmkd_state_enum2friendly(state);
+
+ pcmk__output_create_xml_text_node(out, "span", " (");
+ pcmk__output_create_xml_text_node(out, "span", state_s);
+ pcmk__output_create_xml_text_node(out, "span", ")");
+ }
+
+ out->finish(out, CRM_EX_DISCONNECT, true, NULL);
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("crm-mon-disconnected", "const char *",
+ "enum pcmk_pacemakerd_state")
+static int
+crm_mon_disconnected_text(pcmk__output_t *out, va_list args)
+{
+ const char *desc = va_arg(args, const char *);
+ enum pcmk_pacemakerd_state state =
+ (enum pcmk_pacemakerd_state) va_arg(args, int);
+ int rc = pcmk_rc_ok;
+
+ if (out->dest != stdout) {
+ out->reset(out);
+ }
+
+ if (state != pcmk_pacemakerd_state_invalid) {
+ rc = out->info(out, "Not connected to CIB%s%s (%s)",
+ (desc != NULL)? ": " : "", pcmk__s(desc, ""),
+ pcmk__pcmkd_state_enum2friendly(state));
+ } else {
+ rc = out->info(out, "Not connected to CIB%s%s",
+ (desc != NULL)? ": " : "", pcmk__s(desc, ""));
+ }
+
+ out->finish(out, CRM_EX_DISCONNECT, true, NULL);
+ return rc;
+}
+
+PCMK__OUTPUT_ARGS("crm-mon-disconnected", "const char *",
+ "enum pcmk_pacemakerd_state")
+static int
+crm_mon_disconnected_xml(pcmk__output_t *out, va_list args)
+{
+ const char *desc = va_arg(args, const char *);
+ enum pcmk_pacemakerd_state state =
+ (enum pcmk_pacemakerd_state) va_arg(args, int);
+ const char *state_s = NULL;
+
+ if (out->dest != stdout) {
+ out->reset(out);
+ }
+
+ if (state != pcmk_pacemakerd_state_invalid) {
+ state_s = pcmk_pacemakerd_api_daemon_state_enum2text(state);
+ }
+
+ pcmk__output_create_xml_node(out, "crm-mon-disconnected",
+ XML_ATTR_DESC, desc,
+ "pacemakerd-state", state_s,
+ NULL);
+
+ out->finish(out, CRM_EX_DISCONNECT, true, NULL);
+ return pcmk_rc_ok;
+}
+
+static pcmk__message_entry_t fmt_functions[] = {
+ { "crm-mon-disconnected", "default", crm_mon_disconnected_default },
+ { "crm-mon-disconnected", "html", crm_mon_disconnected_html },
+ { "crm-mon-disconnected", "text", crm_mon_disconnected_text },
+ { "crm-mon-disconnected", "xml", crm_mon_disconnected_xml },
+ { NULL, NULL, NULL },
+};
+
+/* Define exit codes for monitoring-compatible output
+ * For nagios plugins, the possibilities are
+ * OK=0, WARN=1, CRIT=2, and UNKNOWN=3
+ */
+#define MON_STATUS_WARN CRM_EX_ERROR
+#define MON_STATUS_CRIT CRM_EX_INVALID_PARAM
+#define MON_STATUS_UNKNOWN CRM_EX_UNIMPLEMENT_FEATURE
+
+#define RECONNECT_MSECS 5000
+
+struct {
+ guint reconnect_ms;
+ enum mon_exec_mode exec_mode;
+ gboolean fence_connect;
+ gboolean print_pending;
+ gboolean show_bans;
+ gboolean watch_fencing;
+ char *pid_file;
+ char *external_agent;
+ char *external_recipient;
+ char *neg_location_prefix;
+ char *only_node;
+ char *only_rsc;
+ GSList *user_includes_excludes;
+ GSList *includes_excludes;
+} options = {
+ .reconnect_ms = RECONNECT_MSECS,
+ .exec_mode = mon_exec_unset,
+ .fence_connect = TRUE,
+};
+
+static crm_exit_t clean_up(crm_exit_t exit_code);
+static void crm_diff_update(const char *event, xmlNode * msg);
+static void clean_up_on_connection_failure(int rc);
+static int mon_refresh_display(gpointer user_data);
+static int setup_cib_connection(void);
+static int setup_fencer_connection(void);
+static int setup_api_connections(void);
+static void mon_st_callback_event(stonith_t * st, stonith_event_t * e);
+static void mon_st_callback_display(stonith_t * st, stonith_event_t * e);
+static void refresh_after_event(gboolean data_updated, gboolean enforce);
+
+static uint32_t
+all_includes(mon_output_format_t fmt) {
+ if (fmt == mon_output_monitor || fmt == mon_output_plain || fmt == mon_output_console) {
+ return ~pcmk_section_options;
+ } else {
+ return pcmk_section_all;
+ }
+}
+
+static uint32_t
+default_includes(mon_output_format_t fmt) {
+ switch (fmt) {
+ case mon_output_monitor:
+ case mon_output_plain:
+ case mon_output_console:
+ case mon_output_html:
+ case mon_output_cgi:
+ return pcmk_section_summary
+ |pcmk_section_nodes
+ |pcmk_section_resources
+ |pcmk_section_failures;
+
+ case mon_output_xml:
+ return all_includes(fmt);
+
+ default:
+ return 0;
+ }
+}
+
+struct {
+ const char *name;
+ uint32_t bit;
+} sections[] = {
+ { "attributes", pcmk_section_attributes },
+ { "bans", pcmk_section_bans },
+ { "counts", pcmk_section_counts },
+ { "dc", pcmk_section_dc },
+ { "failcounts", pcmk_section_failcounts },
+ { "failures", pcmk_section_failures },
+ { PCMK__VALUE_FENCING, pcmk_section_fencing_all },
+ { "fencing-failed", pcmk_section_fence_failed },
+ { "fencing-pending", pcmk_section_fence_pending },
+ { "fencing-succeeded", pcmk_section_fence_worked },
+ { "maint-mode", pcmk_section_maint_mode },
+ { "nodes", pcmk_section_nodes },
+ { "operations", pcmk_section_operations },
+ { "options", pcmk_section_options },
+ { "resources", pcmk_section_resources },
+ { "stack", pcmk_section_stack },
+ { "summary", pcmk_section_summary },
+ { "tickets", pcmk_section_tickets },
+ { "times", pcmk_section_times },
+ { NULL }
+};
+
+static uint32_t
+find_section_bit(const char *name) {
+ for (int i = 0; sections[i].name != NULL; i++) {
+ if (pcmk__str_eq(sections[i].name, name, pcmk__str_casei)) {
+ return sections[i].bit;
+ }
+ }
+
+ return 0;
+}
+
+static gboolean
+apply_exclude(const gchar *excludes, GError **error) {
+ char **parts = NULL;
+ gboolean result = TRUE;
+
+ parts = g_strsplit(excludes, ",", 0);
+ for (char **s = parts; *s != NULL; s++) {
+ uint32_t bit = find_section_bit(*s);
+
+ if (pcmk__str_eq(*s, "all", pcmk__str_none)) {
+ show = 0;
+ } else if (pcmk__str_eq(*s, PCMK__VALUE_NONE, pcmk__str_none)) {
+ show = all_includes(output_format);
+ } else if (bit != 0) {
+ show &= ~bit;
+ } else {
+ g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ "--exclude options: all, attributes, bans, counts, dc, "
+ "failcounts, failures, fencing, fencing-failed, "
+ "fencing-pending, fencing-succeeded, maint-mode, nodes, "
+ PCMK__VALUE_NONE ", operations, options, resources, "
+ "stack, summary, tickets, times");
+ result = FALSE;
+ break;
+ }
+ }
+ g_strfreev(parts);
+ return result;
+}
+
+static gboolean
+apply_include(const gchar *includes, GError **error) {
+ char **parts = NULL;
+ gboolean result = TRUE;
+
+ parts = g_strsplit(includes, ",", 0);
+ for (char **s = parts; *s != NULL; s++) {
+ uint32_t bit = find_section_bit(*s);
+
+ if (pcmk__str_eq(*s, "all", pcmk__str_none)) {
+ show = all_includes(output_format);
+ } else if (pcmk__starts_with(*s, "bans")) {
+ show |= pcmk_section_bans;
+ if (options.neg_location_prefix != NULL) {
+ free(options.neg_location_prefix);
+ options.neg_location_prefix = NULL;
+ }
+
+ if (strlen(*s) > 4 && (*s)[4] == ':') {
+ options.neg_location_prefix = strdup(*s+5);
+ }
+ } else if (pcmk__str_any_of(*s, "default", "defaults", NULL)) {
+ show |= default_includes(output_format);
+ } else if (pcmk__str_eq(*s, PCMK__VALUE_NONE, pcmk__str_none)) {
+ show = 0;
+ } else if (bit != 0) {
+ show |= bit;
+ } else {
+ g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ "--include options: all, attributes, bans[:PREFIX], counts, dc, "
+ "default, failcounts, failures, fencing, fencing-failed, "
+ "fencing-pending, fencing-succeeded, maint-mode, nodes, "
+ PCMK__VALUE_NONE ", operations, options, resources, "
+ "stack, summary, tickets, times");
+ result = FALSE;
+ break;
+ }
+ }
+ g_strfreev(parts);
+ return result;
+}
+
+static gboolean
+apply_include_exclude(GSList *lst, GError **error) {
+ gboolean rc = TRUE;
+ GSList *node = lst;
+
+ while (node != NULL) {
+ char *s = node->data;
+
+ if (pcmk__starts_with(s, "--include=")) {
+ rc = apply_include(s+10, error);
+ } else if (pcmk__starts_with(s, "-I=")) {
+ rc = apply_include(s+3, error);
+ } else if (pcmk__starts_with(s, "--exclude=")) {
+ rc = apply_exclude(s+10, error);
+ } else if (pcmk__starts_with(s, "-U=")) {
+ rc = apply_exclude(s+3, error);
+ }
+
+ if (rc != TRUE) {
+ break;
+ }
+
+ node = node->next;
+ }
+
+ return rc;
+}
+
+static gboolean
+user_include_exclude_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ char *s = crm_strdup_printf("%s=%s", option_name, optarg);
+
+ options.user_includes_excludes = g_slist_append(options.user_includes_excludes, s);
+ return TRUE;
+}
+
+static gboolean
+include_exclude_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ char *s = crm_strdup_printf("%s=%s", option_name, optarg);
+
+ options.includes_excludes = g_slist_append(options.includes_excludes, s);
+ return TRUE;
+}
+
+static gboolean
+as_cgi_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ pcmk__str_update(&args->output_ty, "html");
+ output_format = mon_output_cgi;
+ options.exec_mode = mon_exec_one_shot;
+ return TRUE;
+}
+
+static gboolean
+as_html_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ pcmk__str_update(&args->output_dest, optarg);
+ pcmk__str_update(&args->output_ty, "html");
+ output_format = mon_output_html;
+ umask(S_IWGRP | S_IWOTH); // World-readable HTML
+ return TRUE;
+}
+
+static gboolean
+as_simple_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ pcmk__str_update(&args->output_ty, "text");
+ output_format = mon_output_monitor;
+ options.exec_mode = mon_exec_one_shot;
+ return TRUE;
+}
+
+static gboolean
+as_xml_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ pcmk__str_update(&args->output_ty, "xml");
+ output_format = mon_output_legacy_xml;
+ return TRUE;
+}
+
+static gboolean
+fence_history_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ if (optarg == NULL) {
+ interactive_fence_level = 2;
+ } else {
+ pcmk__scan_min_int(optarg, &interactive_fence_level, 0);
+ }
+
+ switch (interactive_fence_level) {
+ case 3:
+ options.fence_connect = TRUE;
+ fence_history = pcmk__fence_history_full;
+ return include_exclude_cb("--include", PCMK__VALUE_FENCING, data,
+ err);
+
+ case 2:
+ options.fence_connect = TRUE;
+ fence_history = pcmk__fence_history_full;
+ return include_exclude_cb("--include", PCMK__VALUE_FENCING, data,
+ err);
+
+ case 1:
+ options.fence_connect = TRUE;
+ fence_history = pcmk__fence_history_full;
+ return include_exclude_cb("--include", "fencing-failed,fencing-pending", data, err);
+
+ case 0:
+ options.fence_connect = FALSE;
+ fence_history = pcmk__fence_history_none;
+ return include_exclude_cb("--exclude", PCMK__VALUE_FENCING, data,
+ err);
+
+ default:
+ g_set_error(err, PCMK__EXITC_ERROR, CRM_EX_INVALID_PARAM, "Fence history must be 0-3");
+ return FALSE;
+ }
+}
+
+static gboolean
+group_by_node_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ show_opts |= pcmk_show_rscs_by_node;
+ return TRUE;
+}
+
+static gboolean
+hide_headers_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ return user_include_exclude_cb("--exclude", "summary", data, err);
+}
+
+static gboolean
+inactive_resources_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ show_opts |= pcmk_show_inactive_rscs;
+ return TRUE;
+}
+
+static gboolean
+no_curses_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ pcmk__str_update(&args->output_ty, "text");
+ output_format = mon_output_plain;
+ return TRUE;
+}
+
+static gboolean
+print_brief_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ show_opts |= pcmk_show_brief;
+ return TRUE;
+}
+
+static gboolean
+print_detail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ show_opts |= pcmk_show_details;
+ return TRUE;
+}
+
+static gboolean
+print_description_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ show_opts |= pcmk_show_description;
+ return TRUE;
+}
+
+static gboolean
+print_timing_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ show_opts |= pcmk_show_timing;
+ return user_include_exclude_cb("--include", "operations", data, err);
+}
+
+static gboolean
+reconnect_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ int rc = crm_get_msec(optarg);
+
+ if (rc == -1) {
+ g_set_error(err, PCMK__EXITC_ERROR, CRM_EX_INVALID_PARAM, "Invalid value for -i: %s", optarg);
+ return FALSE;
+ } else {
+ options.reconnect_ms = crm_parse_interval_spec(optarg);
+
+ if (options.exec_mode != mon_exec_daemonized) {
+ // Reconnect interval applies to daemonized too, so don't override
+ options.exec_mode = mon_exec_update;
+ }
+ }
+
+ return TRUE;
+}
+
+/*!
+ * \internal
+ * \brief Enable one-shot mode
+ *
+ * \param[in] option_name Name of option being parsed (ignored)
+ * \param[in] optarg Value to be parsed (ignored)
+ * \param[in] data User data (ignored)
+ * \param[out] err Where to store error (ignored)
+ */
+static gboolean
+one_shot_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **err)
+{
+ options.exec_mode = mon_exec_one_shot;
+ return TRUE;
+}
+
+/*!
+ * \internal
+ * \brief Enable daemonized mode
+ *
+ * \param[in] option_name Name of option being parsed (ignored)
+ * \param[in] optarg Value to be parsed (ignored)
+ * \param[in] data User data (ignored)
+ * \param[out] err Where to store error (ignored)
+ */
+static gboolean
+daemonize_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **err)
+{
+ options.exec_mode = mon_exec_daemonized;
+ return TRUE;
+}
+
+static gboolean
+show_attributes_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ return user_include_exclude_cb("--include", "attributes", data, err);
+}
+
+static gboolean
+show_bans_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ if (optarg != NULL) {
+ char *s = crm_strdup_printf("bans:%s", optarg);
+ gboolean rc = user_include_exclude_cb("--include", s, data, err);
+ free(s);
+ return rc;
+ } else {
+ return user_include_exclude_cb("--include", "bans", data, err);
+ }
+}
+
+static gboolean
+show_failcounts_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ return user_include_exclude_cb("--include", "failcounts", data, err);
+}
+
+static gboolean
+show_operations_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ return user_include_exclude_cb("--include", "failcounts,operations", data, err);
+}
+
+static gboolean
+show_tickets_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ return user_include_exclude_cb("--include", "tickets", data, err);
+}
+
+static gboolean
+use_cib_file_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ setenv("CIB_file", optarg, 1);
+ options.exec_mode = mon_exec_one_shot;
+ return TRUE;
+}
+
+#define INDENT " "
+
+/* *INDENT-OFF* */
+static GOptionEntry addl_entries[] = {
+ { "interval", 'i', 0, G_OPTION_ARG_CALLBACK, reconnect_cb,
+ "Update frequency (default is 5 seconds)",
+ "TIMESPEC" },
+
+ { "one-shot", '1', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
+ one_shot_cb,
+ "Display the cluster status once and exit",
+ NULL },
+
+ { "daemonize", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
+ daemonize_cb,
+ "Run in the background as a daemon.\n"
+ INDENT "Requires at least one of --output-to and --external-agent.",
+ NULL },
+
+ { "pid-file", 'p', 0, G_OPTION_ARG_FILENAME, &options.pid_file,
+ "(Advanced) Daemon pid file location",
+ "FILE" },
+
+ { "external-agent", 'E', 0, G_OPTION_ARG_FILENAME, &options.external_agent,
+ "A program to run when resource operations take place",
+ "FILE" },
+
+ { "external-recipient", 'e', 0, G_OPTION_ARG_STRING, &options.external_recipient,
+ "A recipient for your program (assuming you want the program to send something to someone).",
+ "RCPT" },
+
+ { "watch-fencing", 'W', 0, G_OPTION_ARG_NONE, &options.watch_fencing,
+ "Listen for fencing events. For use with --external-agent.",
+ NULL },
+
+ { "xml-file", 'x', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, use_cib_file_cb,
+ NULL,
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry display_entries[] = {
+ { "include", 'I', 0, G_OPTION_ARG_CALLBACK, user_include_exclude_cb,
+ "A list of sections to include in the output.\n"
+ INDENT "See `Output Control` help for more information.",
+ "SECTION(s)" },
+
+ { "exclude", 'U', 0, G_OPTION_ARG_CALLBACK, user_include_exclude_cb,
+ "A list of sections to exclude from the output.\n"
+ INDENT "See `Output Control` help for more information.",
+ "SECTION(s)" },
+
+ { "node", 0, 0, G_OPTION_ARG_STRING, &options.only_node,
+ "When displaying information about nodes, show only what's related to the given\n"
+ INDENT "node, or to all nodes tagged with the given tag",
+ "NODE" },
+
+ { "resource", 0, 0, G_OPTION_ARG_STRING, &options.only_rsc,
+ "When displaying information about resources, show only what's related to the given\n"
+ INDENT "resource, or to all resources tagged with the given tag",
+ "RSC" },
+
+ { "group-by-node", 'n', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, group_by_node_cb,
+ "Group resources by node",
+ NULL },
+
+ { "inactive", 'r', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, inactive_resources_cb,
+ "Display inactive resources",
+ NULL },
+
+ { "failcounts", 'f', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, show_failcounts_cb,
+ "Display resource fail counts",
+ NULL },
+
+ { "operations", 'o', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, show_operations_cb,
+ "Display resource operation history",
+ NULL },
+
+ { "timing-details", 't', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, print_timing_cb,
+ "Display resource operation history with timing details",
+ NULL },
+
+ { "tickets", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, show_tickets_cb,
+ "Display cluster tickets",
+ NULL },
+
+ { "fence-history", 'm', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, fence_history_cb,
+ "Show fence history:\n"
+ INDENT "0=off, 1=failures and pending (default without option),\n"
+ INDENT "2=add successes (default without value for option),\n"
+ INDENT "3=show full history without reduction to most recent of each flavor",
+ "LEVEL" },
+
+ { "neg-locations", 'L', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, show_bans_cb,
+ "Display negative location constraints [optionally filtered by id prefix]",
+ NULL },
+
+ { "show-node-attributes", 'A', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, show_attributes_cb,
+ "Display node attributes",
+ NULL },
+
+ { "hide-headers", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, hide_headers_cb,
+ "Hide all headers",
+ NULL },
+
+ { "show-detail", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, print_detail_cb,
+ "Show more details (node IDs, individual clone instances)",
+ NULL },
+
+ { "show-description", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, print_description_cb,
+ "Show resource descriptions",
+ NULL },
+
+ { "brief", 'b', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, print_brief_cb,
+ "Brief output",
+ NULL },
+
+ { "pending", 'j', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.print_pending,
+ "Display pending state if 'record-pending' is enabled",
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry deprecated_entries[] = {
+ { "as-html", 'h', G_OPTION_FLAG_FILENAME, G_OPTION_ARG_CALLBACK, as_html_cb,
+ "Write cluster status to the named HTML file.\n"
+ INDENT "Use --output-as=html --output-to=FILE instead.",
+ "FILE" },
+
+ { "as-xml", 'X', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, as_xml_cb,
+ "Write cluster status as XML to stdout. This will enable one-shot mode.\n"
+ INDENT "Use --output-as=xml instead.",
+ NULL },
+
+ { "simple-status", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
+ as_simple_cb,
+ "Display the cluster status once as a simple one line output\n"
+ INDENT "(suitable for nagios)",
+ NULL },
+
+ { "disable-ncurses", 'N', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, no_curses_cb,
+ "Disable the use of ncurses.\n"
+ INDENT "Use --output-as=text instead.",
+ NULL },
+
+ { "web-cgi", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, as_cgi_cb,
+ "Web mode with output suitable for CGI (preselected when run as *.cgi).\n"
+ INDENT "Use --output-as=html --html-cgi instead.",
+ NULL },
+
+ { NULL }
+};
+/* *INDENT-ON* */
+
+/* Reconnect to the CIB and fencing agent after reconnect_ms has passed. This sounds
+ * like it would be more broadly useful, but only ever happens after a disconnect via
+ * mon_cib_connection_destroy.
+ */
+static gboolean
+reconnect_after_timeout(gpointer data)
+{
+#if CURSES_ENABLED
+ if (output_format == mon_output_console) {
+ clear();
+ refresh();
+ }
+#endif
+
+ out->transient(out, "Reconnecting...");
+ if (setup_api_connections() == pcmk_rc_ok) {
+ // Trigger redrawing the screen (needs reconnect_timer == 0)
+ reconnect_timer = 0;
+ refresh_after_event(FALSE, TRUE);
+ return G_SOURCE_REMOVE;
+ }
+
+ out->message(out, "crm-mon-disconnected",
+ "Latest connection attempt failed", pcmkd_state);
+
+ reconnect_timer = g_timeout_add(options.reconnect_ms,
+ reconnect_after_timeout, NULL);
+ return G_SOURCE_REMOVE;
+}
+
+/* Called from various places when we are disconnected from the CIB or from the
+ * fencing agent. If the CIB connection is still valid, this function will also
+ * attempt to sign off and reconnect.
+ */
+static void
+mon_cib_connection_destroy(gpointer user_data)
+{
+ const char *msg = "Connection to the cluster lost";
+
+ pcmkd_state = pcmk_pacemakerd_state_invalid;
+
+ /* No crm-mon-disconnected message for console; a working implementation
+ * is not currently worth the effort
+ */
+ out->transient(out, "%s", msg);
+
+ out->message(out, "crm-mon-disconnected", msg, pcmkd_state);
+
+ if (refresh_timer != NULL) {
+ /* we'll trigger a refresh after reconnect */
+ mainloop_timer_stop(refresh_timer);
+ }
+ if (reconnect_timer) {
+ /* we'll trigger a new reconnect-timeout at the end */
+ g_source_remove(reconnect_timer);
+ reconnect_timer = 0;
+ }
+
+ /* the client API won't properly reconnect notifications if they are still
+ * in the table - so remove them
+ */
+ stonith_api_delete(st);
+ st = NULL;
+
+ if (cib) {
+ cib->cmds->signoff(cib);
+ reconnect_timer = g_timeout_add(options.reconnect_ms,
+ reconnect_after_timeout, NULL);
+ }
+}
+
+/* Signal handler installed into the mainloop for normal program shutdown */
+static void
+mon_shutdown(int nsig)
+{
+ clean_up(CRM_EX_OK);
+}
+
+#if CURSES_ENABLED
+static volatile sighandler_t ncurses_winch_handler;
+
+/* Signal handler installed the regular way (not into the main loop) for when
+ * the screen is resized. Commonly, this happens when running in an xterm and
+ * the user changes its size.
+ */
+static void
+mon_winresize(int nsig)
+{
+ static int not_done;
+ int lines = 0, cols = 0;
+
+ if (!not_done++) {
+ if (ncurses_winch_handler)
+ /* the original ncurses WINCH signal handler does the
+ * magic of retrieving the new window size;
+ * otherwise, we'd have to use ioctl or tgetent */
+ (*ncurses_winch_handler) (SIGWINCH);
+ getmaxyx(stdscr, lines, cols);
+ resizeterm(lines, cols);
+ /* Alert the mainloop code we'd like the refresh_trigger to run next
+ * time the mainloop gets around to checking.
+ */
+ mainloop_set_trigger((crm_trigger_t *) refresh_trigger);
+ }
+ not_done--;
+}
+#endif
+
+static int
+setup_fencer_connection(void)
+{
+ int rc = pcmk_ok;
+
+ if (options.fence_connect && st == NULL) {
+ st = stonith_api_new();
+ }
+
+ if (!options.fence_connect || st == NULL || st->state != stonith_disconnected) {
+ return rc;
+ }
+
+ rc = st->cmds->connect(st, crm_system_name, NULL);
+ if (rc == pcmk_ok) {
+ crm_trace("Setting up stonith callbacks");
+ if (options.watch_fencing) {
+ st->cmds->register_notification(st, T_STONITH_NOTIFY_DISCONNECT,
+ mon_st_callback_event);
+ st->cmds->register_notification(st, T_STONITH_NOTIFY_FENCE, mon_st_callback_event);
+ } else {
+ st->cmds->register_notification(st, T_STONITH_NOTIFY_DISCONNECT,
+ mon_st_callback_display);
+ st->cmds->register_notification(st, T_STONITH_NOTIFY_HISTORY, mon_st_callback_display);
+ }
+ } else {
+ stonith_api_delete(st);
+ st = NULL;
+ }
+
+ return rc;
+}
+
+static int
+setup_cib_connection(void)
+{
+ int rc = pcmk_rc_ok;
+
+ CRM_CHECK(cib != NULL, return EINVAL);
+
+ if (cib->state != cib_disconnected) {
+ // Already connected with notifications registered for CIB updates
+ return rc;
+ }
+
+ rc = cib__signon_query(out, &cib, &current_cib);
+
+ if (rc == pcmk_rc_ok) {
+ rc = pcmk_legacy2rc(cib->cmds->set_connection_dnotify(cib,
+ mon_cib_connection_destroy));
+ if (rc == EPROTONOSUPPORT) {
+ out->err(out,
+ "CIB client does not support connection loss "
+ "notifications; crm_mon will be unable to reconnect after "
+ "connection loss");
+ rc = pcmk_rc_ok;
+ }
+
+ if (rc == pcmk_rc_ok) {
+ cib->cmds->del_notify_callback(cib, T_CIB_DIFF_NOTIFY,
+ crm_diff_update);
+ rc = pcmk_legacy2rc(cib->cmds->add_notify_callback(cib,
+ T_CIB_DIFF_NOTIFY, crm_diff_update));
+ }
+
+ if (rc != pcmk_rc_ok) {
+ if (rc == EPROTONOSUPPORT) {
+ out->err(out,
+ "CIB client does not support CIB diff "
+ "notifications");
+ } else {
+ out->err(out, "CIB diff notification setup failed");
+ }
+
+ out->err(out, "Cannot monitor CIB changes; exiting");
+ cib__clean_up_connection(&cib);
+ stonith_api_delete(st);
+ st = NULL;
+ }
+ }
+ return rc;
+}
+
+/* This is used to set up the fencing options after the interactive UI has been stared.
+ * fence_history_cb can't be used because it builds up a list of includes/excludes that
+ * then have to be processed with apply_include_exclude and that could affect other
+ * things.
+ */
+static void
+set_fencing_options(int level)
+{
+ switch (level) {
+ case 3:
+ options.fence_connect = TRUE;
+ fence_history = pcmk__fence_history_full;
+ show |= pcmk_section_fencing_all;
+ break;
+
+ case 2:
+ options.fence_connect = TRUE;
+ fence_history = pcmk__fence_history_full;
+ show |= pcmk_section_fencing_all;
+ break;
+
+ case 1:
+ options.fence_connect = TRUE;
+ fence_history = pcmk__fence_history_full;
+ show |= pcmk_section_fence_failed | pcmk_section_fence_pending;
+ break;
+
+ default:
+ interactive_fence_level = 0;
+ options.fence_connect = FALSE;
+ fence_history = pcmk__fence_history_none;
+ show &= ~pcmk_section_fencing_all;
+ break;
+ }
+}
+
+static int
+setup_api_connections(void)
+{
+ int rc = pcmk_rc_ok;
+
+ CRM_CHECK(cib != NULL, return EINVAL);
+
+ if (cib->state != cib_disconnected) {
+ return rc;
+ }
+
+ if (cib->variant == cib_native) {
+ rc = pcmk__pacemakerd_status(out, crm_system_name,
+ options.reconnect_ms / 2, false,
+ &pcmkd_state);
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
+
+ switch (pcmkd_state) {
+ case pcmk_pacemakerd_state_running:
+ case pcmk_pacemakerd_state_remote:
+ case pcmk_pacemakerd_state_shutting_down:
+ /* Fencer and CIB may still be available while shutting down or
+ * running on a Pacemaker Remote node
+ */
+ break;
+ default:
+ // Fencer and CIB are definitely unavailable
+ return ENOTCONN;
+ }
+
+ setup_fencer_connection();
+ }
+
+ rc = setup_cib_connection();
+ return rc;
+}
+
+#if CURSES_ENABLED
+static const char *
+get_option_desc(char c)
+{
+ const char *desc = "No help available";
+
+ for (GOptionEntry *entry = display_entries; entry != NULL; entry++) {
+ if (entry->short_name == c) {
+ desc = entry->description;
+ break;
+ }
+ }
+ return desc;
+}
+
+#define print_option_help(out, option, condition) \
+ curses_formatted_printf(out, "%c %c: \t%s\n", ((condition)? '*': ' '), option, get_option_desc(option));
+
+/* This function is called from the main loop when there is something to be read
+ * on stdin, like an interactive user's keystroke. All it does is read the keystroke,
+ * set flags (or show the page showing which keystrokes are valid), and redraw the
+ * screen. It does not do anything with connections to the CIB or fencing agent
+ * agent what would happen in mon_refresh_display.
+ */
+static gboolean
+detect_user_input(GIOChannel *channel, GIOCondition condition, gpointer user_data)
+{
+ int c;
+ gboolean config_mode = FALSE;
+
+ while (1) {
+
+ /* Get user input */
+ c = getchar();
+
+ switch (c) {
+ case 'm':
+ interactive_fence_level++;
+ if (interactive_fence_level > 3) {
+ interactive_fence_level = 0;
+ }
+
+ set_fencing_options(interactive_fence_level);
+ break;
+ case 'c':
+ show ^= pcmk_section_tickets;
+ break;
+ case 'f':
+ show ^= pcmk_section_failcounts;
+ break;
+ case 'n':
+ show_opts ^= pcmk_show_rscs_by_node;
+ break;
+ case 'o':
+ show ^= pcmk_section_operations;
+ if (!pcmk_is_set(show, pcmk_section_operations)) {
+ show_opts &= ~pcmk_show_timing;
+ }
+ break;
+ case 'r':
+ show_opts ^= pcmk_show_inactive_rscs;
+ break;
+ case 'R':
+ show_opts ^= pcmk_show_details;
+#ifdef PCMK__COMPAT_2_0
+ // Keep failed action output the same as 2.0.x
+ show_opts |= pcmk_show_failed_detail;
+#endif
+ break;
+ case 't':
+ show_opts ^= pcmk_show_timing;
+ if (pcmk_is_set(show_opts, pcmk_show_timing)) {
+ show |= pcmk_section_operations;
+ }
+ break;
+ case 'A':
+ show ^= pcmk_section_attributes;
+ break;
+ case 'L':
+ show ^= pcmk_section_bans;
+ break;
+ case 'D':
+ /* If any header is shown, clear them all, otherwise set them all */
+ if (pcmk_any_flags_set(show, pcmk_section_summary)) {
+ show &= ~pcmk_section_summary;
+ } else {
+ show |= pcmk_section_summary;
+ }
+ /* Regardless, we don't show options in console mode. */
+ show &= ~pcmk_section_options;
+ break;
+ case 'b':
+ show_opts ^= pcmk_show_brief;
+ break;
+ case 'j':
+ show_opts ^= pcmk_show_pending;
+ break;
+ case '?':
+ config_mode = TRUE;
+ break;
+ default:
+ /* All other keys just redraw the screen. */
+ goto refresh;
+ }
+
+ if (!config_mode)
+ goto refresh;
+
+ clear();
+ refresh();
+
+ curses_formatted_printf(out, "%s", "Display option change mode\n");
+ print_option_help(out, 'c', pcmk_is_set(show, pcmk_section_tickets));
+ print_option_help(out, 'f', pcmk_is_set(show, pcmk_section_failcounts));
+ print_option_help(out, 'n', pcmk_is_set(show_opts, pcmk_show_rscs_by_node));
+ print_option_help(out, 'o', pcmk_is_set(show, pcmk_section_operations));
+ print_option_help(out, 'r', pcmk_is_set(show_opts, pcmk_show_inactive_rscs));
+ print_option_help(out, 't', pcmk_is_set(show_opts, pcmk_show_timing));
+ print_option_help(out, 'A', pcmk_is_set(show, pcmk_section_attributes));
+ print_option_help(out, 'L', pcmk_is_set(show, pcmk_section_bans));
+ print_option_help(out, 'D', !pcmk_is_set(show, pcmk_section_summary));
+#ifdef PCMK__COMPAT_2_0
+ print_option_help(out, 'R', pcmk_any_flags_set(show_opts, pcmk_show_details & ~pcmk_show_failed_detail));
+#else
+ print_option_help(out, 'R', pcmk_any_flags_set(show_opts, pcmk_show_details));
+#endif
+ print_option_help(out, 'b', pcmk_is_set(show_opts, pcmk_show_brief));
+ print_option_help(out, 'j', pcmk_is_set(show_opts, pcmk_show_pending));
+ curses_formatted_printf(out, "%d m: \t%s\n", interactive_fence_level, get_option_desc('m'));
+ curses_formatted_printf(out, "%s", "\nToggle fields via field letter, type any other key to return\n");
+ }
+
+refresh:
+ refresh_after_event(FALSE, TRUE);
+
+ return TRUE;
+}
+#endif // CURSES_ENABLED
+
+// Basically crm_signal_handler(SIGCHLD, SIG_IGN) plus the SA_NOCLDWAIT flag
+static void
+avoid_zombies(void)
+{
+ struct sigaction sa;
+
+ memset(&sa, 0, sizeof(struct sigaction));
+ if (sigemptyset(&sa.sa_mask) < 0) {
+ crm_warn("Cannot avoid zombies: %s", pcmk_rc_str(errno));
+ return;
+ }
+ sa.sa_handler = SIG_IGN;
+ sa.sa_flags = SA_RESTART|SA_NOCLDWAIT;
+ if (sigaction(SIGCHLD, &sa, NULL) < 0) {
+ crm_warn("Cannot avoid zombies: %s", pcmk_rc_str(errno));
+ }
+}
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
+ GOptionContext *context = NULL;
+
+ GOptionEntry extra_prog_entries[] = {
+ { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &(args->quiet),
+ "Be less descriptive in output.",
+ NULL },
+
+ { NULL }
+ };
+
+#if CURSES_ENABLED
+ const char *fmts = "console (default), html, text, xml, none";
+#else
+ const char *fmts = "text (default), html, xml, none";
+#endif // CURSES_ENABLED
+ const char *desc = NULL;
+
+ desc = "Notes:\n\n"
+ "If this program is called as crm_mon.cgi, --output-as=html and\n"
+ "--html-cgi are automatically added to the command line\n"
+ "arguments.\n\n"
+
+ "Time Specification:\n\n"
+ "The TIMESPEC in any command line option can be specified in many\n"
+ "different formats. It can be an integer number of seconds, a\n"
+ "number plus units (us/usec/ms/msec/s/sec/m/min/h/hr), or an ISO\n"
+ "8601 period specification.\n\n"
+
+ "Output Control:\n\n"
+ "By default, a particular set of sections are written to the\n"
+ "output destination. The default varies based on the output\n"
+ "format: XML includes all sections by default, while other output\n"
+ "formats include less. This set can be modified with the --include\n"
+ "and --exclude command line options. Each option may be passed\n"
+ "multiple times, and each can specify a comma-separated list of\n"
+ "sections. The options are applied to the default set, in order\n"
+ "from left to right as they are passed on the command line. For a\n"
+ "list of valid sections, pass --include=list or --exclude=list.\n\n"
+
+ "Interactive Use:\n\n"
+#if CURSES_ENABLED
+ "When run interactively, crm_mon can be told to hide and show\n"
+ "various sections of output. To see a help screen explaining the\n"
+ "options, press '?'. Any key stroke aside from those listed will\n"
+ "cause the screen to refresh.\n\n"
+#else
+ "The local installation of Pacemaker was built without support for\n"
+ "interactive (console) mode. A curses library must be available at\n"
+ "build time to support interactive mode.\n\n"
+#endif // CURSES_ENABLED
+
+ "Examples:\n\n"
+#if CURSES_ENABLED
+ "Display the cluster status on the console with updates as they\n"
+ "occur:\n\n"
+ "\tcrm_mon\n\n"
+#endif // CURSES_ENABLED
+
+ "Display the cluster status once and exit:\n\n"
+ "\tcrm_mon -1\n\n"
+
+ "Display the cluster status, group resources by node, and include\n"
+ "inactive resources in the list:\n\n"
+ "\tcrm_mon --group-by-node --inactive\n\n"
+
+ "Start crm_mon as a background daemon and have it write the\n"
+ "cluster status to an HTML file:\n\n"
+ "\tcrm_mon --daemonize --output-as html "
+ "--output-to /path/to/docroot/filename.html\n\n"
+
+ "Display the cluster status as XML:\n\n"
+ "\tcrm_mon --output-as xml\n\n";
+
+ context = pcmk__build_arg_context(args, fmts, group, NULL);
+ pcmk__add_main_args(context, extra_prog_entries);
+ g_option_context_set_description(context, desc);
+
+ pcmk__add_arg_group(context, "display", "Display Options:",
+ "Show display options", display_entries);
+ pcmk__add_arg_group(context, "additional", "Additional Options:",
+ "Show additional options", addl_entries);
+ pcmk__add_arg_group(context, "deprecated", "Deprecated Options:",
+ "Show deprecated options", deprecated_entries);
+
+ return context;
+}
+
+/* If certain format options were specified, we want to set some extra
+ * options. We can just process these like they were given on the
+ * command line.
+ */
+static void
+add_output_args(void) {
+ GError *err = NULL;
+
+ if (output_format == mon_output_plain) {
+ if (!pcmk__force_args(context, &err, "%s --text-fancy", g_get_prgname())) {
+ g_propagate_error(&error, err);
+ clean_up(CRM_EX_USAGE);
+ }
+ } else if (output_format == mon_output_cgi) {
+ if (!pcmk__force_args(context, &err, "%s --html-cgi", g_get_prgname())) {
+ g_propagate_error(&error, err);
+ clean_up(CRM_EX_USAGE);
+ }
+ } else if (output_format == mon_output_xml) {
+ if (!pcmk__force_args(context, &err, "%s --xml-simple-list --xml-substitute", g_get_prgname())) {
+ g_propagate_error(&error, err);
+ clean_up(CRM_EX_USAGE);
+ }
+ } else if (output_format == mon_output_legacy_xml) {
+ output_format = mon_output_xml;
+ if (!pcmk__force_args(context, &err, "%s --xml-legacy --xml-substitute", g_get_prgname())) {
+ g_propagate_error(&error, err);
+ clean_up(CRM_EX_USAGE);
+ }
+ }
+}
+
+/*!
+ * \internal
+ * \brief Set output format based on \p --output-as arguments and mode arguments
+ *
+ * When the deprecated output format arguments (\p --as-cgi, \p --as-html,
+ * \p --simple-status, \p --as-xml) are parsed, callback functions set
+ * \p output_format (and the umask if appropriate). If none of the deprecated
+ * arguments were specified, this function does the same based on the current
+ * \p --output-as arguments and the \p --one-shot and \p --daemonize arguments.
+ *
+ * \param[in,out] args Command line arguments
+ */
+static void
+reconcile_output_format(pcmk__common_args_t *args)
+{
+ if (output_format != mon_output_unset) {
+ /* One of the deprecated arguments was used, and we're finished. Note
+ * that this means the deprecated arguments take precedence.
+ */
+ return;
+ }
+
+ if (pcmk__str_eq(args->output_ty, "none", pcmk__str_none)) {
+ output_format = mon_output_none;
+
+ } else if (pcmk__str_eq(args->output_ty, "html", pcmk__str_none)) {
+ output_format = mon_output_html;
+ umask(S_IWGRP | S_IWOTH); // World-readable HTML
+
+ } else if (pcmk__str_eq(args->output_ty, "xml", pcmk__str_none)) {
+ output_format = mon_output_xml;
+
+#if CURSES_ENABLED
+ } else if (pcmk__str_eq(args->output_ty, "console",
+ pcmk__str_null_matches)) {
+ /* Console is the default format if no conflicting options are given.
+ *
+ * Use text output instead if one of the following conditions is met:
+ * * We've requested daemonized or one-shot mode (console output is
+ * incompatible with modes other than mon_exec_update)
+ * * We requested the version, which is effectively one-shot
+ * * We specified a non-stdout output destination (console mode is
+ * compatible only with stdout)
+ */
+ if ((options.exec_mode == mon_exec_daemonized)
+ || (options.exec_mode == mon_exec_one_shot)
+ || args->version
+ || !pcmk__str_eq(args->output_dest, "-", pcmk__str_null_matches)) {
+
+ pcmk__str_update(&args->output_ty, "text");
+ output_format = mon_output_plain;
+ } else {
+ pcmk__str_update(&args->output_ty, "console");
+ output_format = mon_output_console;
+ crm_enable_stderr(FALSE);
+ }
+#endif // CURSES_ENABLED
+
+ } else if (pcmk__str_eq(args->output_ty, "text", pcmk__str_null_matches)) {
+ /* Text output was explicitly requested, or it's the default because
+ * curses is not enabled
+ */
+ pcmk__str_update(&args->output_ty, "text");
+ output_format = mon_output_plain;
+ }
+
+ // Otherwise, invalid format. Let pcmk__output_new() throw an error.
+}
+
+/*!
+ * \internal
+ * \brief Set execution mode to the output format's default if appropriate
+ *
+ * \param[in,out] args Command line arguments
+ */
+static void
+set_default_exec_mode(const pcmk__common_args_t *args)
+{
+ if (output_format == mon_output_console) {
+ /* Update is the only valid mode for console, but set here instead of
+ * reconcile_output_format() for isolation and consistency
+ */
+ options.exec_mode = mon_exec_update;
+
+ } else if (options.exec_mode == mon_exec_unset) {
+ // Default to one-shot mode for all other formats
+ options.exec_mode = mon_exec_one_shot;
+
+ } else if ((options.exec_mode == mon_exec_update)
+ && pcmk__str_eq(args->output_dest, "-",
+ pcmk__str_null_matches)) {
+ // If not using console format, update mode cannot be used with stdout
+ options.exec_mode = mon_exec_one_shot;
+ }
+}
+
+static void
+clean_up_on_connection_failure(int rc)
+{
+ if (output_format == mon_output_monitor) {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_ERROR, "CLUSTER CRIT: Connection to cluster failed: %s",
+ pcmk_rc_str(rc));
+ clean_up(MON_STATUS_CRIT);
+ } else if (rc == ENOTCONN) {
+ if (pcmkd_state == pcmk_pacemakerd_state_remote) {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_ERROR, "Error: remote-node not connected to cluster");
+ } else {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_ERROR, "Error: cluster is not available on this node");
+ }
+ } else {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_ERROR, "Connection to cluster failed: %s", pcmk_rc_str(rc));
+ }
+
+ clean_up(pcmk_rc2exitc(rc));
+}
+
+static void
+one_shot(void)
+{
+ int rc = pcmk__status(out, cib, fence_history, show, show_opts,
+ options.only_node, options.only_rsc,
+ options.neg_location_prefix,
+ output_format == mon_output_monitor, 0);
+
+ if (rc == pcmk_rc_ok) {
+ clean_up(pcmk_rc2exitc(rc));
+ } else {
+ clean_up_on_connection_failure(rc);
+ }
+}
+
+static void
+exit_on_invalid_cib(void)
+{
+ if (cib != NULL) {
+ return;
+ }
+
+ // Shouldn't really be possible
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_ERROR, "Invalid CIB source");
+ clean_up(CRM_EX_ERROR);
+}
+
+int
+main(int argc, char **argv)
+{
+ int rc = pcmk_rc_ok;
+ GOptionGroup *output_group = NULL;
+
+ args = pcmk__new_common_args(SUMMARY);
+ context = build_arg_context(args, &output_group);
+ pcmk__register_formats(output_group, formats);
+
+ options.pid_file = strdup("/tmp/ClusterMon.pid");
+ pcmk__cli_init_logging("crm_mon", 0);
+
+ // Avoid needing to wait for subprocesses forked for -E/--external-agent
+ avoid_zombies();
+
+ if (pcmk__ends_with_ext(argv[0], ".cgi")) {
+ output_format = mon_output_cgi;
+ options.exec_mode = mon_exec_one_shot;
+ }
+
+ processed_args = pcmk__cmdline_preproc(argv, "ehimpxEILU");
+
+ fence_history_cb("--fence-history", "1", NULL, NULL);
+
+ /* Set an HTML title regardless of what format we will eventually use. This can't
+ * be done in add_output_args. That function is called after command line
+ * arguments are processed in the next block, which means it'll override whatever
+ * title the user provides. Doing this here means the user can give their own
+ * title on the command line.
+ */
+ if (!pcmk__force_args(context, &error, "%s --html-title \"Cluster Status\"",
+ g_get_prgname())) {
+ return clean_up(CRM_EX_USAGE);
+ }
+
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ return clean_up(CRM_EX_USAGE);
+ }
+
+ for (int i = 0; i < args->verbosity; i++) {
+ crm_bump_log_level(argc, argv);
+ }
+
+ if (!args->version) {
+ if (args->quiet) {
+ include_exclude_cb("--exclude", "times", NULL, NULL);
+ }
+
+ if (options.watch_fencing) {
+ fence_history_cb("--fence-history", "0", NULL, NULL);
+ options.fence_connect = TRUE;
+ }
+
+ /* create the cib-object early to be able to do further
+ * decisions based on the cib-source
+ */
+ cib = cib_new();
+
+ exit_on_invalid_cib();
+
+ switch (cib->variant) {
+ case cib_native:
+ // Everything (fencer, CIB, pcmkd status) should be available
+ break;
+
+ case cib_file:
+ // Live fence history is not meaningful
+ fence_history_cb("--fence-history", "0", NULL, NULL);
+
+ /* Notifications are unsupported; nothing to monitor
+ * @COMPAT: Let setup_cib_connection() handle this by exiting?
+ */
+ options.exec_mode = mon_exec_one_shot;
+ break;
+
+ case cib_remote:
+ // We won't receive any fencing updates
+ fence_history_cb("--fence-history", "0", NULL, NULL);
+ break;
+
+ default:
+ /* something is odd */
+ exit_on_invalid_cib();
+ break;
+ }
+
+ if ((options.exec_mode == mon_exec_daemonized)
+ && !options.external_agent
+ && pcmk__str_eq(args->output_dest, "-", pcmk__str_null_matches)) {
+
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ "--daemonize requires at least one of --output-to "
+ "(with value not set to '-') and --external-agent");
+ return clean_up(CRM_EX_USAGE);
+ }
+ }
+
+ reconcile_output_format(args);
+ set_default_exec_mode(args);
+ add_output_args();
+
+ /* output_format MUST NOT BE CHANGED AFTER THIS POINT. */
+
+ rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_ERROR, "Error creating output format %s: %s",
+ args->output_ty, pcmk_rc_str(rc));
+ return clean_up(CRM_EX_ERROR);
+ }
+
+ /* If we had a valid format for pcmk__output_new(), output_format should be
+ * set by now.
+ */
+ CRM_ASSERT(output_format != mon_output_unset);
+
+ if (options.exec_mode == mon_exec_daemonized) {
+ if (!options.external_agent && (output_format == mon_output_none)) {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ "--daemonize requires --external-agent if used with "
+ "--output-as=none");
+ return clean_up(CRM_EX_USAGE);
+ }
+ crm_enable_stderr(FALSE);
+ cib_delete(cib);
+ cib = NULL;
+ pcmk__daemonize(crm_system_name, options.pid_file);
+ cib = cib_new();
+ exit_on_invalid_cib();
+ }
+
+ show = default_includes(output_format);
+
+ /* Apply --include/--exclude flags we used internally. There's no error reporting
+ * here because this would be a programming error.
+ */
+ apply_include_exclude(options.includes_excludes, &error);
+
+ /* And now apply any --include/--exclude flags the user gave on the command line.
+ * These are done in a separate pass from the internal ones because we want to
+ * make sure whatever the user specifies overrides whatever we do.
+ */
+ if (!apply_include_exclude(options.user_includes_excludes, &error)) {
+ return clean_up(CRM_EX_USAGE);
+ }
+
+ /* Sync up the initial value of interactive_fence_level with whatever was set with
+ * --include/--exclude= options.
+ */
+ if (pcmk_all_flags_set(show, pcmk_section_fencing_all)) {
+ interactive_fence_level = 3;
+ } else if (pcmk_is_set(show, pcmk_section_fence_worked)) {
+ interactive_fence_level = 2;
+ } else if (pcmk_any_flags_set(show, pcmk_section_fence_failed | pcmk_section_fence_pending)) {
+ interactive_fence_level = 1;
+ } else {
+ interactive_fence_level = 0;
+ }
+
+ pcmk__register_lib_messages(out);
+ crm_mon_register_messages(out);
+ pe__register_messages(out);
+ stonith__register_messages(out);
+
+ // Messages internal to this file, nothing curses-specific
+ pcmk__register_messages(out, fmt_functions);
+
+ if (args->version) {
+ out->version(out, false);
+ return clean_up(CRM_EX_OK);
+ }
+
+ /* Extra sanity checks when in CGI mode */
+ if (output_format == mon_output_cgi) {
+ if (cib->variant == cib_file) {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "CGI mode used with CIB file");
+ return clean_up(CRM_EX_USAGE);
+ } else if (options.external_agent != NULL) {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "CGI mode cannot be used with --external-agent");
+ return clean_up(CRM_EX_USAGE);
+ } else if (options.exec_mode == mon_exec_daemonized) {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "CGI mode cannot be used with -d");
+ return clean_up(CRM_EX_USAGE);
+ }
+ }
+
+ if (output_format == mon_output_xml) {
+ show_opts |= pcmk_show_inactive_rscs | pcmk_show_timing;
+ }
+
+ if ((output_format == mon_output_html || output_format == mon_output_cgi) &&
+ out->dest != stdout) {
+ pcmk__html_add_header("meta", "http-equiv", "refresh", "content",
+ pcmk__itoa(options.reconnect_ms / 1000), NULL);
+ }
+
+#ifdef PCMK__COMPAT_2_0
+ // Keep failed action output the same as 2.0.x
+ show_opts |= pcmk_show_failed_detail;
+#endif
+
+ crm_info("Starting %s", crm_system_name);
+
+ cib__set_output(cib, out);
+
+ if (options.exec_mode == mon_exec_one_shot) {
+ one_shot();
+ }
+
+ out->message(out, "crm-mon-disconnected",
+ "Waiting for initial connection", pcmkd_state);
+ do {
+ out->transient(out, "Connecting to cluster...");
+ rc = setup_api_connections();
+
+ if (rc != pcmk_rc_ok) {
+ if ((rc == ENOTCONN) || (rc == ECONNREFUSED)) {
+ out->transient(out, "Connection failed. Retrying in %ums...",
+ options.reconnect_ms);
+ }
+
+ // Give some time to view all output even if we won't retry
+ pcmk__sleep_ms(options.reconnect_ms);
+#if CURSES_ENABLED
+ if (output_format == mon_output_console) {
+ clear();
+ refresh();
+ }
+#endif
+ }
+ } while ((rc == ENOTCONN) || (rc == ECONNREFUSED));
+
+ if (rc != pcmk_rc_ok) {
+ clean_up_on_connection_failure(rc);
+ }
+
+ set_fencing_options(interactive_fence_level);
+ mon_refresh_display(NULL);
+
+ mainloop = g_main_loop_new(NULL, FALSE);
+
+ mainloop_add_signal(SIGTERM, mon_shutdown);
+ mainloop_add_signal(SIGINT, mon_shutdown);
+#if CURSES_ENABLED
+ if (output_format == mon_output_console) {
+ ncurses_winch_handler = crm_signal_handler(SIGWINCH, mon_winresize);
+ if (ncurses_winch_handler == SIG_DFL ||
+ ncurses_winch_handler == SIG_IGN || ncurses_winch_handler == SIG_ERR)
+ ncurses_winch_handler = NULL;
+
+ io_channel = g_io_channel_unix_new(STDIN_FILENO);
+ g_io_add_watch(io_channel, G_IO_IN, detect_user_input, NULL);
+ }
+#endif
+
+ /* When refresh_trigger->trigger is set to TRUE, call mon_refresh_display. In
+ * this file, that is anywhere mainloop_set_trigger is called.
+ */
+ refresh_trigger = mainloop_add_trigger(G_PRIORITY_LOW, mon_refresh_display, NULL);
+
+ g_main_loop_run(mainloop);
+ g_main_loop_unref(mainloop);
+
+ if (io_channel != NULL) {
+ g_io_channel_shutdown(io_channel, TRUE, NULL);
+ }
+
+ crm_info("Exiting %s", crm_system_name);
+
+ return clean_up(CRM_EX_OK);
+}
+
+static int
+send_custom_trap(const char *node, const char *rsc, const char *task, int target_rc, int rc,
+ int status, const char *desc)
+{
+ pid_t pid;
+
+ /*setenv needs chars, these are ints */
+ char *rc_s = pcmk__itoa(rc);
+ char *status_s = pcmk__itoa(status);
+ char *target_rc_s = pcmk__itoa(target_rc);
+
+ crm_debug("Sending external notification to '%s' via '%s'", options.external_recipient, options.external_agent);
+
+ if(rsc) {
+ setenv("CRM_notify_rsc", rsc, 1);
+ }
+ if (options.external_recipient) {
+ setenv("CRM_notify_recipient", options.external_recipient, 1);
+ }
+ setenv("CRM_notify_node", node, 1);
+ setenv("CRM_notify_task", task, 1);
+ setenv("CRM_notify_desc", desc, 1);
+ setenv("CRM_notify_rc", rc_s, 1);
+ setenv("CRM_notify_target_rc", target_rc_s, 1);
+ setenv("CRM_notify_status", status_s, 1);
+
+ pid = fork();
+ if (pid == -1) {
+ crm_perror(LOG_ERR, "notification fork() failed.");
+ }
+ if (pid == 0) {
+ /* crm_debug("notification: I am the child. Executing the nofitication program."); */
+ execl(options.external_agent, options.external_agent, NULL);
+ exit(CRM_EX_ERROR);
+ }
+
+ crm_trace("Finished running custom notification program '%s'.", options.external_agent);
+ free(target_rc_s);
+ free(status_s);
+ free(rc_s);
+ return 0;
+}
+
+static int
+handle_rsc_op(xmlNode *xml, void *userdata)
+{
+ const char *node_id = (const char *) userdata;
+ int rc = -1;
+ int status = -1;
+ int target_rc = -1;
+ gboolean notify = TRUE;
+
+ char *rsc = NULL;
+ char *task = NULL;
+ const char *desc = NULL;
+ const char *magic = NULL;
+ const char *id = NULL;
+ const char *node = NULL;
+
+ xmlNode *n = xml;
+ xmlNode * rsc_op = xml;
+
+ if(strcmp((const char*)xml->name, XML_LRM_TAG_RSC_OP) != 0) {
+ pcmk__xe_foreach_child(xml, NULL, handle_rsc_op, (void *) node_id);
+ return pcmk_rc_ok;
+ }
+
+ id = pe__xe_history_key(rsc_op);
+
+ magic = crm_element_value(rsc_op, XML_ATTR_TRANSITION_MAGIC);
+ if (magic == NULL) {
+ /* non-change */
+ return pcmk_rc_ok;
+ }
+
+ if (!decode_transition_magic(magic, NULL, NULL, NULL, &status, &rc,
+ &target_rc)) {
+ crm_err("Invalid event %s detected for %s", magic, id);
+ return pcmk_rc_ok;
+ }
+
+ if (parse_op_key(id, &rsc, &task, NULL) == FALSE) {
+ crm_err("Invalid event detected for %s", id);
+ goto bail;
+ }
+
+ node = crm_element_value(rsc_op, XML_LRM_ATTR_TARGET);
+
+ while (n != NULL && !pcmk__str_eq(XML_CIB_TAG_STATE, TYPE(n), pcmk__str_casei)) {
+ n = n->parent;
+ }
+
+ if(node == NULL && n) {
+ node = crm_element_value(n, XML_ATTR_UNAME);
+ }
+
+ if (node == NULL && n) {
+ node = ID(n);
+ }
+
+ if (node == NULL) {
+ node = node_id;
+ }
+
+ if (node == NULL) {
+ crm_err("No node detected for event %s (%s)", magic, id);
+ goto bail;
+ }
+
+ /* look up where we expected it to be? */
+ desc = pcmk_rc_str(pcmk_rc_ok);
+ if ((status == PCMK_EXEC_DONE) && (target_rc == rc)) {
+ crm_notice("%s of %s on %s completed: %s", task, rsc, node, desc);
+ if (rc == PCMK_OCF_NOT_RUNNING) {
+ notify = FALSE;
+ }
+
+ } else if (status == PCMK_EXEC_DONE) {
+ desc = services_ocf_exitcode_str(rc);
+ crm_warn("%s of %s on %s failed: %s", task, rsc, node, desc);
+
+ } else {
+ desc = pcmk_exec_status_str(status);
+ crm_warn("%s of %s on %s failed: %s", task, rsc, node, desc);
+ }
+
+ if (notify && options.external_agent) {
+ send_custom_trap(node, rsc, task, target_rc, rc, status, desc);
+ }
+
+ bail:
+ free(rsc);
+ free(task);
+ return pcmk_rc_ok;
+}
+
+/* This function is just a wrapper around mainloop_set_trigger so that it can be
+ * called from a mainloop directly. It's simply another way of ensuring the screen
+ * gets redrawn.
+ */
+static gboolean
+mon_trigger_refresh(gpointer user_data)
+{
+ mainloop_set_trigger((crm_trigger_t *) refresh_trigger);
+ return FALSE;
+}
+
+static int
+handle_op_for_node(xmlNode *xml, void *userdata)
+{
+ const char *node = crm_element_value(xml, XML_ATTR_UNAME);
+
+ if (node == NULL) {
+ node = ID(xml);
+ }
+
+ handle_rsc_op(xml, (void *) node);
+ return pcmk_rc_ok;
+}
+
+static void
+crm_diff_update_v2(const char *event, xmlNode * msg)
+{
+ xmlNode *change = NULL;
+ xmlNode *diff = get_message_xml(msg, F_CIB_UPDATE_RESULT);
+
+ for (change = pcmk__xml_first_child(diff); change != NULL;
+ change = pcmk__xml_next(change)) {
+ const char *name = NULL;
+ const char *op = crm_element_value(change, XML_DIFF_OP);
+ const char *xpath = crm_element_value(change, XML_DIFF_PATH);
+ xmlNode *match = NULL;
+ const char *node = NULL;
+
+ if(op == NULL) {
+ continue;
+
+ } else if(strcmp(op, "create") == 0) {
+ match = change->children;
+
+ } else if(strcmp(op, "move") == 0) {
+ continue;
+
+ } else if(strcmp(op, "delete") == 0) {
+ continue;
+
+ } else if(strcmp(op, "modify") == 0) {
+ match = first_named_child(change, XML_DIFF_RESULT);
+ if(match) {
+ match = match->children;
+ }
+ }
+
+ if(match) {
+ name = (const char *)match->name;
+ }
+
+ crm_trace("Handling %s operation for %s %p, %s", op, xpath, match, name);
+ if(xpath == NULL) {
+ /* Version field, ignore */
+
+ } else if(name == NULL) {
+ crm_debug("No result for %s operation to %s", op, xpath);
+ CRM_ASSERT(strcmp(op, "delete") == 0 || strcmp(op, "move") == 0);
+
+ } else if(strcmp(name, XML_TAG_CIB) == 0) {
+ pcmk__xe_foreach_child(first_named_child(match, XML_CIB_TAG_STATUS),
+ NULL, handle_op_for_node, NULL);
+
+ } else if(strcmp(name, XML_CIB_TAG_STATUS) == 0) {
+ pcmk__xe_foreach_child(match, NULL, handle_op_for_node, NULL);
+
+ } else if(strcmp(name, XML_CIB_TAG_STATE) == 0) {
+ node = crm_element_value(match, XML_ATTR_UNAME);
+ if (node == NULL) {
+ node = ID(match);
+ }
+ handle_rsc_op(match, (void *) node);
+
+ } else if(strcmp(name, XML_CIB_TAG_LRM) == 0) {
+ node = ID(match);
+ handle_rsc_op(match, (void *) node);
+
+ } else if(strcmp(name, XML_LRM_TAG_RESOURCES) == 0) {
+ char *local_node = pcmk__xpath_node_id(xpath, "lrm");
+
+ handle_rsc_op(match, local_node);
+ free(local_node);
+
+ } else if(strcmp(name, XML_LRM_TAG_RESOURCE) == 0) {
+ char *local_node = pcmk__xpath_node_id(xpath, "lrm");
+
+ handle_rsc_op(match, local_node);
+ free(local_node);
+
+ } else if(strcmp(name, XML_LRM_TAG_RSC_OP) == 0) {
+ char *local_node = pcmk__xpath_node_id(xpath, "lrm");
+
+ handle_rsc_op(match, local_node);
+ free(local_node);
+
+ } else {
+ crm_trace("Ignoring %s operation for %s %p, %s", op, xpath, match, name);
+ }
+ }
+}
+
+static void
+crm_diff_update_v1(const char *event, xmlNode * msg)
+{
+ /* Process operation updates */
+ xmlXPathObject *xpathObj = xpath_search(msg,
+ "//" F_CIB_UPDATE_RESULT "//" XML_TAG_DIFF_ADDED
+ "//" XML_LRM_TAG_RSC_OP);
+ int lpc = 0, max = numXpathResults(xpathObj);
+
+ for (lpc = 0; lpc < max; lpc++) {
+ xmlNode *rsc_op = getXpathResult(xpathObj, lpc);
+
+ handle_rsc_op(rsc_op, NULL);
+ }
+ freeXpathObject(xpathObj);
+}
+
+static void
+crm_diff_update(const char *event, xmlNode * msg)
+{
+ int rc = -1;
+ static bool stale = FALSE;
+ gboolean cib_updated = FALSE;
+ xmlNode *diff = get_message_xml(msg, F_CIB_UPDATE_RESULT);
+
+ out->progress(out, false);
+
+ if (current_cib != NULL) {
+ rc = xml_apply_patchset(current_cib, diff, TRUE);
+
+ switch (rc) {
+ case -pcmk_err_diff_resync:
+ case -pcmk_err_diff_failed:
+ crm_notice("[%s] Patch aborted: %s (%d)", event, pcmk_strerror(rc), rc);
+ free_xml(current_cib); current_cib = NULL;
+ break;
+ case pcmk_ok:
+ cib_updated = TRUE;
+ break;
+ default:
+ crm_notice("[%s] ABORTED: %s (%d)", event, pcmk_strerror(rc), rc);
+ free_xml(current_cib); current_cib = NULL;
+ }
+ }
+
+ if (current_cib == NULL) {
+ crm_trace("Re-requesting the full cib");
+ cib->cmds->query(cib, NULL, &current_cib, cib_scope_local | cib_sync_call);
+ }
+
+ if (options.external_agent) {
+ int format = 0;
+ crm_element_value_int(diff, "format", &format);
+ switch(format) {
+ case 1:
+ crm_diff_update_v1(event, msg);
+ break;
+ case 2:
+ crm_diff_update_v2(event, msg);
+ break;
+ default:
+ crm_err("Unknown patch format: %d", format);
+ }
+ }
+
+ if (current_cib == NULL) {
+ if(!stale) {
+ out->info(out, "--- Stale data ---");
+ }
+ stale = TRUE;
+ return;
+ }
+
+ stale = FALSE;
+ refresh_after_event(cib_updated, FALSE);
+}
+
+static int
+mon_refresh_display(gpointer user_data)
+{
+ int rc = pcmk_rc_ok;
+
+ last_refresh = time(NULL);
+
+ if (output_format == mon_output_none) {
+ return G_SOURCE_REMOVE;
+ }
+
+ if (fence_history == pcmk__fence_history_full &&
+ !pcmk_all_flags_set(show, pcmk_section_fencing_all) &&
+ output_format != mon_output_xml) {
+ fence_history = pcmk__fence_history_reduced;
+ }
+
+ // Get an up-to-date pacemakerd status for the cluster summary
+ if (cib->variant == cib_native) {
+ pcmk__pacemakerd_status(out, crm_system_name, options.reconnect_ms / 2,
+ false, &pcmkd_state);
+ }
+
+ if (out->dest != stdout) {
+ out->reset(out);
+ }
+
+ rc = pcmk__output_cluster_status(out, st, cib, current_cib, pcmkd_state,
+ fence_history, show, show_opts,
+ options.only_node,options.only_rsc,
+ options.neg_location_prefix,
+ output_format == mon_output_monitor);
+
+ if (output_format == mon_output_monitor && rc != pcmk_rc_ok) {
+ clean_up(MON_STATUS_WARN);
+ return G_SOURCE_REMOVE;
+ } else if (rc == pcmk_rc_schema_validation) {
+ clean_up(CRM_EX_CONFIG);
+ return G_SOURCE_REMOVE;
+ }
+
+ if (out->dest != stdout) {
+ out->finish(out, CRM_EX_OK, true, NULL);
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+/* This function is called for fencing events (see setup_fencer_connection() for
+ * which ones) when --watch-fencing is used on the command line
+ */
+static void
+mon_st_callback_event(stonith_t * st, stonith_event_t * e)
+{
+ if (st->state == stonith_disconnected) {
+ /* disconnect cib as well and have everything reconnect */
+ mon_cib_connection_destroy(NULL);
+ } else if (options.external_agent) {
+ char *desc = stonith__event_description(e);
+
+ send_custom_trap(e->target, NULL, e->operation, pcmk_ok, e->result, 0, desc);
+ free(desc);
+ }
+}
+
+/* Cause the screen to be redrawn (via mainloop_set_trigger) when various conditions are met:
+ *
+ * - If the last update occurred more than reconnect_ms ago (defaults to 5s, but
+ * can be changed via the -i command line option), or
+ * - After every 10 CIB updates, or
+ * - If it's been 2s since the last update
+ *
+ * This function sounds like it would be more broadly useful, but it is only called when a
+ * fencing event is received or a CIB diff occurrs.
+ */
+static void
+refresh_after_event(gboolean data_updated, gboolean enforce)
+{
+ static int updates = 0;
+ time_t now = time(NULL);
+
+ if (data_updated) {
+ updates++;
+ }
+
+ if(refresh_timer == NULL) {
+ refresh_timer = mainloop_timer_add("refresh", 2000, FALSE, mon_trigger_refresh, NULL);
+ }
+
+ if (reconnect_timer > 0) {
+ /* we will receive a refresh request after successful reconnect */
+ mainloop_timer_stop(refresh_timer);
+ return;
+ }
+
+ /* as we're not handling initial failure of fencer-connection as
+ * fatal give it a retry here
+ * not getting here if cib-reconnection is already on the way
+ */
+ setup_fencer_connection();
+
+ if (enforce ||
+ ((now - last_refresh) > (options.reconnect_ms / 1000)) ||
+ updates >= 10) {
+ mainloop_set_trigger((crm_trigger_t *) refresh_trigger);
+ mainloop_timer_stop(refresh_timer);
+ updates = 0;
+
+ } else {
+ mainloop_timer_start(refresh_timer);
+ }
+}
+
+/* This function is called for fencing events (see setup_fencer_connection() for
+ * which ones) when --watch-fencing is NOT used on the command line
+ */
+static void
+mon_st_callback_display(stonith_t * st, stonith_event_t * e)
+{
+ if (st->state == stonith_disconnected) {
+ /* disconnect cib as well and have everything reconnect */
+ mon_cib_connection_destroy(NULL);
+ } else {
+ out->progress(out, false);
+ refresh_after_event(TRUE, FALSE);
+ }
+}
+
+/*
+ * De-init ncurses, disconnect from the CIB manager, disconnect fencing,
+ * deallocate memory and show usage-message if requested.
+ *
+ * We don't actually return, but nominally returning crm_exit_t allows a usage
+ * like "return clean_up(exit_code);" which helps static analysis understand the
+ * code flow.
+ */
+static crm_exit_t
+clean_up(crm_exit_t exit_code)
+{
+ /* Quitting crm_mon is much more complicated than it ought to be. */
+
+ /* (1) Close connections, free things, etc. */
+ cib__clean_up_connection(&cib);
+ stonith_api_delete(st);
+ free(options.neg_location_prefix);
+ free(options.only_node);
+ free(options.only_rsc);
+ free(options.pid_file);
+ g_slist_free_full(options.includes_excludes, free);
+
+ g_strfreev(processed_args);
+
+ /* (2) If this is abnormal termination and we're in curses mode, shut down
+ * curses first. Any messages displayed to the screen before curses is shut
+ * down will be lost because doing the shut down will also restore the
+ * screen to whatever it looked like before crm_mon was started.
+ */
+ if (((error != NULL) || (exit_code == CRM_EX_USAGE))
+ && (output_format == mon_output_console)
+ && (out != NULL)) {
+
+ out->finish(out, exit_code, false, NULL);
+ pcmk__output_free(out);
+ out = NULL;
+ }
+
+ /* (3) If this is a command line usage related failure, print the usage
+ * message.
+ */
+ if (exit_code == CRM_EX_USAGE && (output_format == mon_output_console || output_format == mon_output_plain)) {
+ char *help = g_option_context_get_help(context, TRUE, NULL);
+
+ fprintf(stderr, "%s", help);
+ g_free(help);
+ }
+
+ pcmk__free_arg_context(context);
+
+ /* (4) If this is any kind of error, print the error out and exit. Make
+ * sure to handle situations both before and after formatted output is
+ * set up. We want errors to appear formatted if at all possible.
+ */
+ if (error != NULL) {
+ if (out != NULL) {
+ out->err(out, "%s: %s", g_get_prgname(), error->message);
+ out->finish(out, exit_code, true, NULL);
+ pcmk__output_free(out);
+ } else {
+ fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message);
+ }
+
+ g_clear_error(&error);
+ crm_exit(exit_code);
+ }
+
+ /* (5) Print formatted output to the screen if we made it far enough in
+ * crm_mon to be able to do so.
+ */
+ if (out != NULL) {
+ if (options.exec_mode != mon_exec_daemonized) {
+ out->finish(out, exit_code, true, NULL);
+ }
+
+ pcmk__output_free(out);
+ pcmk__unregister_formats();
+ }
+
+ crm_exit(exit_code);
+}
diff --git a/tools/crm_mon.h b/tools/crm_mon.h
new file mode 100644
index 0000000..a505f50
--- /dev/null
+++ b/tools/crm_mon.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2019-2023 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.
+ */
+
+#ifndef CRM_MON__H
+#define CRM_MON__H
+
+#include <crm_internal.h>
+
+#include <glib.h>
+
+#include <crm/common/output_internal.h>
+#include <crm/pengine/pe_types.h>
+#include <crm/stonith-ng.h>
+
+/*
+ * The man pages for both curses and ncurses suggest inclusion of "curses.h".
+ * We believe the following to be acceptable and portable.
+ */
+
+# if defined(HAVE_LIBNCURSES) || defined(HAVE_LIBCURSES)
+# if defined(HAVE_NCURSES_H) && !defined(HAVE_INCOMPATIBLE_PRINTW)
+# include <ncurses.h>
+# define CURSES_ENABLED 1
+# elif defined(HAVE_NCURSES_NCURSES_H) && !defined(HAVE_INCOMPATIBLE_PRINTW)
+# include <ncurses/ncurses.h>
+# define CURSES_ENABLED 1
+# elif defined(HAVE_CURSES_H) && !defined(HAVE_INCOMPATIBLE_PRINTW)
+# include <curses.h>
+# define CURSES_ENABLED 1
+# elif defined(HAVE_CURSES_CURSES_H) && !defined(HAVE_INCOMPATIBLE_PRINTW)
+# include <curses/curses.h>
+# define CURSES_ENABLED 1
+# else
+# define CURSES_ENABLED 0
+# endif
+# else
+# define CURSES_ENABLED 0
+# endif
+
+typedef enum mon_output_format_e {
+ mon_output_unset,
+ mon_output_none,
+ mon_output_monitor,
+ mon_output_plain,
+ mon_output_console,
+ mon_output_xml,
+ mon_output_legacy_xml,
+ mon_output_html,
+ mon_output_cgi
+} mon_output_format_t;
+
+enum mon_exec_mode {
+ mon_exec_unset,
+ mon_exec_daemonized,
+ mon_exec_one_shot,
+ mon_exec_update,
+};
+
+void crm_mon_register_messages(pcmk__output_t *out);
+
+#if CURSES_ENABLED
+pcmk__output_t *crm_mon_mk_curses_output(char **argv);
+void curses_formatted_printf(pcmk__output_t *out, const char *format, ...) G_GNUC_PRINTF(2, 3);
+void curses_formatted_vprintf(pcmk__output_t *out, const char *format, va_list args) G_GNUC_PRINTF(2, 0);
+void curses_indented_printf(pcmk__output_t *out, const char *format, ...) G_GNUC_PRINTF(2, 3);
+void curses_indented_vprintf(pcmk__output_t *out, const char *format, va_list args) G_GNUC_PRINTF(2, 0);
+
+extern GOptionEntry crm_mon_curses_output_entries[];
+#define CRM_MON_SUPPORTED_FORMAT_CURSES { "console", crm_mon_mk_curses_output, crm_mon_curses_output_entries }
+#endif
+
+#endif
diff --git a/tools/crm_mon.service.in b/tools/crm_mon.service.in
new file mode 100644
index 0000000..7ae6649
--- /dev/null
+++ b/tools/crm_mon.service.in
@@ -0,0 +1,17 @@
+[Unit]
+Description=Daemon for pacemaker monitor
+Documentation=man:crm_mon
+After=pacemaker.service
+
+[Service]
+Type=forking
+EnvironmentFile=-@CONFIGDIR@/crm_mon
+ExecStart=@sbindir@/crm_mon $OPTIONS
+Restart=always
+
+# crm_perror() writes directly to stderr, so ignore it here to avoid
+# double-logging with the wrong format
+StandardError=null
+
+[Install]
+WantedBy=multi-user.target
diff --git a/tools/crm_mon.upstart.in b/tools/crm_mon.upstart.in
new file mode 100644
index 0000000..eb4c956
--- /dev/null
+++ b/tools/crm_mon.upstart.in
@@ -0,0 +1,35 @@
+# crm_mon - Daemon for pacemaker monitor
+#
+#
+
+kill timeout 3600
+respawn
+respawn limit 10 3600
+
+expect fork
+
+env prog=crm_mon
+env sysconf=@CONFIGDIR@/crm_mon
+env rpm_lockdir=@localstatedir@/lock/subsys
+env deb_lockdir=@localstatedir@/lock
+
+
+script
+ [ -f "$sysconf" ] && . "$sysconf"
+ exec $prog $OPTIONS
+end script
+
+post-start script
+ [ -f "$sysconf" ] && . "$sysconf"
+ [ -z "$LOCK_FILE" -a -d "$rpm_lockdir" ] && LOCK_FILE="$rpm_lockdir/$prog"
+ [ -z "$LOCK_FILE" -a -d "$deb_lockdir" ] && LOCK_FILE="$deb_lockdir/$prog"
+ touch "$LOCK_FILE"
+end script
+
+post-stop script
+ [ -f "$sysconf" ] && . "$sysconf"
+ [ -z "$LOCK_FILE" -a -d "$rpm_lockdir" ] && LOCK_FILE="$rpm_lockdir/$prog"
+ [ -z "$LOCK_FILE" -a -d "$deb_lockdir" ] && LOCK_FILE="$deb_lockdir/$prog"
+ rm -f "$LOCK_FILE"
+end script
+
diff --git a/tools/crm_mon_curses.c b/tools/crm_mon_curses.c
new file mode 100644
index 0000000..769c7c9
--- /dev/null
+++ b/tools/crm_mon_curses.c
@@ -0,0 +1,490 @@
+/*
+ * Copyright 2019-2022 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 <stdarg.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <crm/crm.h>
+#include <crm/common/output.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/stonith-ng.h>
+#include <crm/fencing/internal.h>
+#include <crm/pengine/internal.h>
+#include <glib.h>
+#include <pacemaker-internal.h>
+
+#include "crm_mon.h"
+
+#if CURSES_ENABLED
+
+GOptionEntry crm_mon_curses_output_entries[] = {
+ { NULL }
+};
+
+typedef struct curses_list_data_s {
+ unsigned int len;
+ char *singular_noun;
+ char *plural_noun;
+} curses_list_data_t;
+
+typedef struct private_data_s {
+ GQueue *parent_q;
+} private_data_t;
+
+static void
+curses_free_priv(pcmk__output_t *out) {
+ private_data_t *priv = NULL;
+
+ if (out == NULL || out->priv == NULL) {
+ return;
+ }
+
+ priv = out->priv;
+
+ g_queue_free(priv->parent_q);
+ free(priv);
+ out->priv = NULL;
+}
+
+static bool
+curses_init(pcmk__output_t *out) {
+ private_data_t *priv = NULL;
+
+ CRM_ASSERT(out != NULL);
+
+ /* If curses_init was previously called on this output struct, just return. */
+ if (out->priv != NULL) {
+ return true;
+ } else {
+ out->priv = calloc(1, sizeof(private_data_t));
+ if (out->priv == NULL) {
+ return false;
+ }
+
+ priv = out->priv;
+ }
+
+ priv->parent_q = g_queue_new();
+
+ initscr();
+ cbreak();
+ noecho();
+
+ return true;
+}
+
+static void
+curses_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) {
+ CRM_ASSERT(out != NULL);
+
+ echo();
+ nocbreak();
+ endwin();
+}
+
+static void
+curses_reset(pcmk__output_t *out) {
+ CRM_ASSERT(out != NULL);
+
+ curses_free_priv(out);
+ curses_init(out);
+}
+
+static void
+curses_subprocess_output(pcmk__output_t *out, int exit_status,
+ const char *proc_stdout, const char *proc_stderr) {
+ CRM_ASSERT(out != NULL);
+
+ if (proc_stdout != NULL) {
+ printw("%s\n", proc_stdout);
+ }
+
+ if (proc_stderr != NULL) {
+ printw("%s\n", proc_stderr);
+ }
+
+ clrtoeol();
+ refresh();
+}
+
+/* curses_version is defined in curses.h, so we can't use that name here.
+ * This function is empty because we create a text object instead of a console
+ * object if version is requested, so this is never called.
+ */
+static void
+curses_ver(pcmk__output_t *out, bool extended) {
+ CRM_ASSERT(out != NULL);
+}
+
+G_GNUC_PRINTF(2, 3)
+static void
+curses_error(pcmk__output_t *out, const char *format, ...) {
+ va_list ap;
+
+ CRM_ASSERT(out != NULL);
+
+ /* Informational output does not get indented, to separate it from other
+ * potentially indented list output.
+ */
+ va_start(ap, format);
+ vw_printw(stdscr, format, ap);
+ va_end(ap);
+
+ /* Add a newline. */
+ addch('\n');
+
+ clrtoeol();
+ refresh();
+ sleep(2);
+}
+
+G_GNUC_PRINTF(2, 3)
+static int
+curses_info(pcmk__output_t *out, const char *format, ...) {
+ va_list ap;
+
+ CRM_ASSERT(out != NULL);
+
+ if (out->is_quiet(out)) {
+ return pcmk_rc_no_output;
+ }
+
+ /* Informational output does not get indented, to separate it from other
+ * potentially indented list output.
+ */
+ va_start(ap, format);
+ vw_printw(stdscr, format, ap);
+ va_end(ap);
+
+ /* Add a newline. */
+ addch('\n');
+
+ clrtoeol();
+ refresh();
+ return pcmk_rc_ok;
+}
+
+static void
+curses_output_xml(pcmk__output_t *out, const char *name, const char *buf) {
+ CRM_ASSERT(out != NULL);
+ curses_indented_printf(out, "%s", buf);
+}
+
+G_GNUC_PRINTF(4, 5)
+static void
+curses_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun,
+ const char *format, ...) {
+ private_data_t *priv = NULL;
+ curses_list_data_t *new_list = NULL;
+ va_list ap;
+
+ CRM_ASSERT(out != NULL && out->priv != NULL);
+ priv = out->priv;
+
+ /* Empty formats can be used to create a new level of indentation, but without
+ * displaying some sort of list header. In that case we need to not do any of
+ * this stuff. vw_printw will act weird if told to print a NULL.
+ */
+ if (format != NULL) {
+ va_start(ap, format);
+
+ curses_indented_vprintf(out, format, ap);
+ printw(":\n");
+
+ va_end(ap);
+ }
+
+ new_list = calloc(1, sizeof(curses_list_data_t));
+ new_list->len = 0;
+ pcmk__str_update(&new_list->singular_noun, singular_noun);
+ pcmk__str_update(&new_list->plural_noun, plural_noun);
+
+ g_queue_push_tail(priv->parent_q, new_list);
+}
+
+G_GNUC_PRINTF(3, 4)
+static void
+curses_list_item(pcmk__output_t *out, const char *id, const char *format, ...) {
+ va_list ap;
+
+ CRM_ASSERT(out != NULL);
+
+ va_start(ap, format);
+
+ if (id != NULL) {
+ curses_indented_printf(out, "%s: ", id);
+ vw_printw(stdscr, format, ap);
+ } else {
+ curses_indented_vprintf(out, format, ap);
+ }
+
+ addch('\n');
+ va_end(ap);
+
+ out->increment_list(out);
+}
+
+static void
+curses_increment_list(pcmk__output_t *out) {
+ private_data_t *priv = NULL;
+ gpointer tail;
+
+ CRM_ASSERT(out != NULL && out->priv != NULL);
+ priv = out->priv;
+
+ tail = g_queue_peek_tail(priv->parent_q);
+ CRM_ASSERT(tail != NULL);
+ ((curses_list_data_t *) tail)->len++;
+}
+
+static void
+curses_end_list(pcmk__output_t *out) {
+ private_data_t *priv = NULL;
+ curses_list_data_t *node = NULL;
+
+ CRM_ASSERT(out != NULL && out->priv != NULL);
+ priv = out->priv;
+
+ node = g_queue_pop_tail(priv->parent_q);
+
+ if (node->singular_noun != NULL && node->plural_noun != NULL) {
+ if (node->len == 1) {
+ curses_indented_printf(out, "%d %s found\n", node->len, node->singular_noun);
+ } else {
+ curses_indented_printf(out, "%d %s found\n", node->len, node->plural_noun);
+ }
+ }
+
+ free(node);
+}
+
+static bool
+curses_is_quiet(pcmk__output_t *out) {
+ CRM_ASSERT(out != NULL);
+ return out->quiet;
+}
+
+static void
+curses_spacer(pcmk__output_t *out) {
+ CRM_ASSERT(out != NULL);
+ addch('\n');
+}
+
+static void
+curses_progress(pcmk__output_t *out, bool end) {
+ CRM_ASSERT(out != NULL);
+
+ if (end) {
+ printw(".\n");
+ } else {
+ addch('.');
+ }
+}
+
+static void
+curses_prompt(const char *prompt, bool do_echo, char **dest)
+{
+ int rc = OK;
+
+ CRM_ASSERT(prompt != NULL && dest != NULL);
+
+ /* This is backwards from the text version of this function on purpose. We
+ * disable echo by default in curses_init, so we need to enable it here if
+ * asked for.
+ */
+ if (do_echo) {
+ rc = echo();
+ }
+
+ if (rc == OK) {
+ printw("%s: ", prompt);
+
+ if (*dest != NULL) {
+ free(*dest);
+ }
+
+ *dest = calloc(1, 1024);
+ /* On older systems, scanw is defined as taking a char * for its first argument,
+ * while newer systems rightly want a const char *. Accomodate both here due
+ * to building with -Werror.
+ */
+ rc = scanw((NCURSES_CONST char *) "%1023s", *dest);
+ addch('\n');
+ }
+
+ if (rc < 1) {
+ free(*dest);
+ *dest = NULL;
+ }
+
+ if (do_echo) {
+ noecho();
+ }
+}
+
+pcmk__output_t *
+crm_mon_mk_curses_output(char **argv) {
+ pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
+
+ if (retval == NULL) {
+ return NULL;
+ }
+
+ retval->fmt_name = "console";
+ retval->request = pcmk__quote_cmdline(argv);
+
+ retval->init = curses_init;
+ retval->free_priv = curses_free_priv;
+ retval->finish = curses_finish;
+ retval->reset = curses_reset;
+
+ retval->register_message = pcmk__register_message;
+ retval->message = pcmk__call_message;
+
+ retval->subprocess_output = curses_subprocess_output;
+ retval->version = curses_ver;
+ retval->err = curses_error;
+ retval->info = curses_info;
+ retval->transient = curses_info;
+ retval->output_xml = curses_output_xml;
+
+ retval->begin_list = curses_begin_list;
+ retval->list_item = curses_list_item;
+ retval->increment_list = curses_increment_list;
+ retval->end_list = curses_end_list;
+
+ retval->is_quiet = curses_is_quiet;
+ retval->spacer = curses_spacer;
+ retval->progress = curses_progress;
+ retval->prompt = curses_prompt;
+
+ return retval;
+}
+
+G_GNUC_PRINTF(2, 0)
+void
+curses_formatted_vprintf(pcmk__output_t *out, const char *format, va_list args) {
+ vw_printw(stdscr, format, args);
+
+ clrtoeol();
+ refresh();
+}
+
+G_GNUC_PRINTF(2, 3)
+void
+curses_formatted_printf(pcmk__output_t *out, const char *format, ...) {
+ va_list ap;
+
+ va_start(ap, format);
+ curses_formatted_vprintf(out, format, ap);
+ va_end(ap);
+}
+
+G_GNUC_PRINTF(2, 0)
+void
+curses_indented_vprintf(pcmk__output_t *out, const char *format, va_list args) {
+ int level = 0;
+ private_data_t *priv = NULL;
+
+ CRM_ASSERT(out != NULL && out->priv != NULL);
+
+ priv = out->priv;
+
+ level = g_queue_get_length(priv->parent_q);
+
+ for (int i = 0; i < level; i++) {
+ printw(" ");
+ }
+
+ if (level > 0) {
+ printw("* ");
+ }
+
+ curses_formatted_vprintf(out, format, args);
+}
+
+G_GNUC_PRINTF(2, 3)
+void
+curses_indented_printf(pcmk__output_t *out, const char *format, ...) {
+ va_list ap;
+
+ va_start(ap, format);
+ curses_indented_vprintf(out, format, ap);
+ va_end(ap);
+}
+
+PCMK__OUTPUT_ARGS("maint-mode", "unsigned long long int")
+static int
+cluster_maint_mode_console(pcmk__output_t *out, va_list args) {
+ unsigned long long flags = va_arg(args, unsigned long long);
+
+ if (pcmk_is_set(flags, pe_flag_maintenance_mode)) {
+ curses_formatted_printf(out, "\n *** Resource management is DISABLED ***\n");
+ curses_formatted_printf(out, " The cluster will not attempt to start, stop or recover services\n");
+ return pcmk_rc_ok;
+ } else if (pcmk_is_set(flags, pe_flag_stop_everything)) {
+ curses_formatted_printf(out, "\n *** Resource management is DISABLED ***\n");
+ curses_formatted_printf(out, " The cluster will keep all resources stopped\n");
+ return pcmk_rc_ok;
+ } else {
+ return pcmk_rc_no_output;
+ }
+}
+
+PCMK__OUTPUT_ARGS("cluster-status", "pe_working_set_t *",
+ "enum pcmk_pacemakerd_state", "crm_exit_t",
+ "stonith_history_t *", "enum pcmk__fence_history", "uint32_t",
+ "uint32_t", "const char *", "GList *", "GList *")
+static int
+cluster_status_console(pcmk__output_t *out, va_list args) {
+ int rc = pcmk_rc_no_output;
+
+ clear();
+ rc = pcmk__cluster_status_text(out, args);
+ refresh();
+ return rc;
+}
+
+PCMK__OUTPUT_ARGS("stonith-event", "stonith_history_t *", "bool", "bool",
+ "const char *", "uint32_t")
+static int
+stonith_event_console(pcmk__output_t *out, va_list args)
+{
+ stonith_history_t *event = va_arg(args, stonith_history_t *);
+ bool full_history = va_arg(args, int);
+ bool completed_only G_GNUC_UNUSED = va_arg(args, int);
+ const char *succeeded = va_arg(args, const char *);
+ uint32_t show_opts = va_arg(args, uint32_t);
+
+ gchar *desc = stonith__history_description(event, full_history, succeeded,
+ show_opts);
+
+
+ curses_indented_printf(out, "%s\n", desc);
+ g_free(desc);
+ return pcmk_rc_ok;
+}
+
+static pcmk__message_entry_t fmt_functions[] = {
+ { "cluster-status", "console", cluster_status_console },
+ { "maint-mode", "console", cluster_maint_mode_console },
+ { "stonith-event", "console", stonith_event_console },
+
+ { NULL, NULL, NULL }
+};
+
+#endif
+
+void
+crm_mon_register_messages(pcmk__output_t *out) {
+#if CURSES_ENABLED
+ pcmk__register_messages(out, fmt_functions);
+#endif
+}
diff --git a/tools/crm_node.8.inc b/tools/crm_node.8.inc
new file mode 100644
index 0000000..84cef12
--- /dev/null
+++ b/tools/crm_node.8.inc
@@ -0,0 +1,5 @@
+[synopsis]
+crm_node [command] [options]
+
+/level node information/
+.SH OPTIONS
diff --git a/tools/crm_node.c b/tools/crm_node.c
new file mode 100644
index 0000000..ac2a190
--- /dev/null
+++ b/tools/crm_node.c
@@ -0,0 +1,601 @@
+/*
+ * Copyright 2004-2023 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 <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <sys/types.h>
+
+#include <crm/crm.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
+#include <crm/common/mainloop.h>
+#include <crm/msg_xml.h>
+#include <crm/cib.h>
+#include <crm/cib/internal.h>
+#include <crm/common/ipc_controld.h>
+#include <crm/common/attrd_internal.h>
+
+#define SUMMARY "crm_node - Tool for displaying low-level node information"
+
+struct {
+ gboolean corosync;
+ gboolean dangerous_cmd;
+ gboolean force_flag;
+ char command;
+ int nodeid;
+ char *target_uname;
+} options = {
+ .command = '\0',
+ .force_flag = FALSE
+};
+
+gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean name_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean remove_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+
+static GMainLoop *mainloop = NULL;
+static crm_exit_t exit_code = CRM_EX_OK;
+
+#define INDENT " "
+
+static GOptionEntry command_entries[] = {
+ { "cluster-id", 'i', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display this node's cluster id",
+ NULL },
+ { "list", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display all known members (past and present) of this cluster",
+ NULL },
+ { "name", 'n', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the name used by the cluster for this node",
+ NULL },
+ { "partition", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the members of this partition",
+ NULL },
+ { "quorum", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display a 1 if our partition has quorum, 0 if not",
+ NULL },
+ { "name-for-id", 'N', 0, G_OPTION_ARG_CALLBACK, name_cb,
+ "Display the name used by the cluster for the node with the specified ID",
+ "ID" },
+ { "remove", 'R', 0, G_OPTION_ARG_CALLBACK, remove_cb,
+ "(Advanced) Remove the (stopped) node with the specified name from Pacemaker's\n"
+ INDENT "configuration and caches (the node must already have been removed from\n"
+ INDENT "the underlying cluster stack configuration",
+ "NAME" },
+
+ { NULL }
+};
+
+static GOptionEntry addl_entries[] = {
+ { "force", 'f', 0, G_OPTION_ARG_NONE, &options.force_flag,
+ NULL,
+ NULL },
+#if SUPPORT_COROSYNC
+ /* Unused and deprecated */
+ { "corosync", 'C', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.corosync,
+ NULL,
+ NULL },
+#endif
+
+ // @TODO add timeout option for when IPC replies are needed
+
+ { NULL }
+};
+
+gboolean
+command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (pcmk__str_eq("-i", option_name, pcmk__str_casei) || pcmk__str_eq("--cluster-id", option_name, pcmk__str_casei)) {
+ options.command = 'i';
+ } else if (pcmk__str_eq("-l", option_name, pcmk__str_casei) || pcmk__str_eq("--list", option_name, pcmk__str_casei)) {
+ options.command = 'l';
+ } else if (pcmk__str_eq("-n", option_name, pcmk__str_casei) || pcmk__str_eq("--name", option_name, pcmk__str_casei)) {
+ options.command = 'n';
+ } else if (pcmk__str_eq("-p", option_name, pcmk__str_casei) || pcmk__str_eq("--partition", option_name, pcmk__str_casei)) {
+ options.command = 'p';
+ } else if (pcmk__str_eq("-q", option_name, pcmk__str_casei) || pcmk__str_eq("--quorum", option_name, pcmk__str_casei)) {
+ options.command = 'q';
+ } else {
+ g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_INVALID_PARAM, "Unknown param passed to command_cb: %s\n", option_name);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+name_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.command = 'N';
+ pcmk__scan_min_int(optarg, &(options.nodeid), 0);
+ return TRUE;
+}
+
+gboolean
+remove_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (optarg == NULL) {
+ crm_err("-R option requires an argument");
+ g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_INVALID_PARAM, "-R option requires an argument");
+ return FALSE;
+ }
+
+ options.command = 'R';
+ options.dangerous_cmd = TRUE;
+ pcmk__str_update(&options.target_uname, optarg);
+ return TRUE;
+}
+
+static gint
+sort_node(gconstpointer a, gconstpointer b)
+{
+ const pcmk_controld_api_node_t *node_a = a;
+ const pcmk_controld_api_node_t *node_b = b;
+
+ return pcmk__numeric_strcasecmp((node_a->uname? node_a->uname : ""),
+ (node_b->uname? node_b->uname : ""));
+}
+
+static void
+controller_event_cb(pcmk_ipc_api_t *controld_api,
+ enum pcmk_ipc_event event_type, crm_exit_t status,
+ void *event_data, void *user_data)
+{
+ pcmk_controld_api_reply_t *reply = event_data;
+
+ switch (event_type) {
+ case pcmk_ipc_event_disconnect:
+ if (exit_code == CRM_EX_DISCONNECT) { // Unexpected
+ fprintf(stderr, "error: Lost connection to controller\n");
+ }
+ goto done;
+ break;
+
+ case pcmk_ipc_event_reply:
+ break;
+
+ default:
+ return;
+ }
+
+ if (status != CRM_EX_OK) {
+ fprintf(stderr, "error: Bad reply from controller: %s\n",
+ crm_exit_str(status));
+ goto done;
+ }
+
+ // Parse desired info from reply and display to user
+ switch (options.command) {
+ case 'i':
+ if (reply->reply_type != pcmk_controld_reply_info) {
+ fprintf(stderr,
+ "error: Unknown reply type %d from controller\n",
+ reply->reply_type);
+ goto done;
+ }
+ if (reply->data.node_info.id == 0) {
+ fprintf(stderr,
+ "error: Controller reply did not contain node ID\n");
+ exit_code = CRM_EX_PROTOCOL;
+ goto done;
+ }
+ printf("%d\n", reply->data.node_info.id);
+ break;
+
+ case 'n':
+ case 'N':
+ if (reply->reply_type != pcmk_controld_reply_info) {
+ fprintf(stderr,
+ "error: Unknown reply type %d from controller\n",
+ reply->reply_type);
+ goto done;
+ }
+ if (reply->data.node_info.uname == NULL) {
+ fprintf(stderr, "Node is not known to cluster\n");
+ exit_code = CRM_EX_NOHOST;
+ goto done;
+ }
+ printf("%s\n", reply->data.node_info.uname);
+ break;
+
+ case 'q':
+ if (reply->reply_type != pcmk_controld_reply_info) {
+ fprintf(stderr,
+ "error: Unknown reply type %d from controller\n",
+ reply->reply_type);
+ goto done;
+ }
+ printf("%d\n", reply->data.node_info.have_quorum);
+ if (!(reply->data.node_info.have_quorum)) {
+ exit_code = CRM_EX_QUORUM;
+ goto done;
+ }
+ break;
+
+ case 'l':
+ case 'p':
+ if (reply->reply_type != pcmk_controld_reply_nodes) {
+ fprintf(stderr,
+ "error: Unknown reply type %d from controller\n",
+ reply->reply_type);
+ goto done;
+ }
+ reply->data.nodes = g_list_sort(reply->data.nodes, sort_node);
+ for (GList *node_iter = reply->data.nodes;
+ node_iter != NULL; node_iter = node_iter->next) {
+
+ pcmk_controld_api_node_t *node = node_iter->data;
+ const char *uname = (node->uname? node->uname : "");
+ const char *state = (node->state? node->state : "");
+
+ if (options.command == 'l') {
+ printf("%lu %s %s\n",
+ (unsigned long) node->id, uname, state);
+
+ // i.e. CRM_NODE_MEMBER, but we don't want to include cluster.h
+ } else if (!strcmp(state, "member")) {
+ printf("%s ", uname);
+ }
+ }
+ if (options.command == 'p') {
+ printf("\n");
+ }
+ break;
+
+ default:
+ fprintf(stderr, "internal error: Controller reply not expected\n");
+ exit_code = CRM_EX_SOFTWARE;
+ goto done;
+ }
+
+ // Success
+ exit_code = CRM_EX_OK;
+done:
+ pcmk_disconnect_ipc(controld_api);
+ pcmk_quit_main_loop(mainloop, 10);
+}
+
+static void
+run_controller_mainloop(uint32_t nodeid, bool list_nodes)
+{
+ pcmk_ipc_api_t *controld_api = NULL;
+ int rc;
+
+ // Set disconnect exit code to handle unexpected disconnects
+ exit_code = CRM_EX_DISCONNECT;
+
+ // Create controller IPC object
+ rc = pcmk_new_ipc_api(&controld_api, pcmk_ipc_controld);
+ if (rc != pcmk_rc_ok) {
+ fprintf(stderr, "error: Could not connect to controller: %s\n",
+ pcmk_rc_str(rc));
+ return;
+ }
+ pcmk_register_ipc_callback(controld_api, controller_event_cb, NULL);
+
+ // Connect to controller
+ rc = pcmk_connect_ipc(controld_api, pcmk_ipc_dispatch_main);
+ if (rc != pcmk_rc_ok) {
+ fprintf(stderr, "error: Could not connect to controller: %s\n",
+ pcmk_rc_str(rc));
+ exit_code = pcmk_rc2exitc(rc);
+ return;
+ }
+
+ if (list_nodes) {
+ rc = pcmk_controld_api_list_nodes(controld_api);
+ } else {
+ rc = pcmk_controld_api_node_info(controld_api, nodeid);
+ }
+ if (rc != pcmk_rc_ok) {
+ fprintf(stderr, "error: Could not ping controller: %s\n",
+ pcmk_rc_str(rc));
+ pcmk_disconnect_ipc(controld_api);
+ exit_code = pcmk_rc2exitc(rc);
+ return;
+ }
+
+ // Run main loop to get controller reply via controller_event_cb()
+ mainloop = g_main_loop_new(NULL, FALSE);
+ g_main_loop_run(mainloop);
+ g_main_loop_unref(mainloop);
+ mainloop = NULL;
+ pcmk_free_ipc_api(controld_api);
+}
+
+static void
+print_node_name(void)
+{
+ // Check environment first (i.e. when called by resource agent)
+ const char *name = getenv("OCF_RESKEY_" CRM_META "_" XML_LRM_ATTR_TARGET);
+
+ if (name != NULL) {
+ printf("%s\n", name);
+ exit_code = CRM_EX_OK;
+ return;
+
+ } else {
+ /* Otherwise ask the controller.
+ * FIXME: Use pcmk__query_node_name() after conversion to formatted
+ * output.
+ */
+ run_controller_mainloop(0, false);
+ }
+}
+
+static int
+cib_remove_node(long id, const char *name)
+{
+ int rc;
+ cib_t *cib = NULL;
+ xmlNode *node = NULL;
+ xmlNode *node_state = NULL;
+
+ crm_trace("Removing %s from the CIB", name);
+
+ if(name == NULL && id == 0) {
+ return -ENOTUNIQ;
+ }
+
+ node = create_xml_node(NULL, XML_CIB_TAG_NODE);
+ node_state = create_xml_node(NULL, XML_CIB_TAG_STATE);
+
+ crm_xml_add(node, XML_ATTR_UNAME, name);
+ crm_xml_add(node_state, XML_ATTR_UNAME, name);
+ if (id > 0) {
+ crm_xml_set_id(node, "%ld", id);
+ crm_xml_add(node_state, XML_ATTR_ID, ID(node));
+ }
+
+ cib = cib_new();
+ cib->cmds->signon(cib, crm_system_name, cib_command);
+
+ rc = cib->cmds->remove(cib, XML_CIB_TAG_NODES, node, cib_sync_call);
+ if (rc != pcmk_ok) {
+ printf("Could not remove %s[%ld] from " XML_CIB_TAG_NODES ": %s",
+ name, id, pcmk_strerror(rc));
+ }
+ rc = cib->cmds->remove(cib, XML_CIB_TAG_STATUS, node_state, cib_sync_call);
+ if (rc != pcmk_ok) {
+ printf("Could not remove %s[%ld] from " XML_CIB_TAG_STATUS ": %s",
+ name, id, pcmk_strerror(rc));
+ }
+
+ cib__clean_up_connection(&cib);
+ return rc;
+}
+
+static int
+controller_remove_node(const char *node_name, long nodeid)
+{
+ pcmk_ipc_api_t *controld_api = NULL;
+ int rc;
+
+ // Create controller IPC object
+ rc = pcmk_new_ipc_api(&controld_api, pcmk_ipc_controld);
+ if (rc != pcmk_rc_ok) {
+ fprintf(stderr, "error: Could not connect to controller: %s\n",
+ pcmk_rc_str(rc));
+ return ENOTCONN;
+ }
+
+ // Connect to controller (without main loop)
+ rc = pcmk_connect_ipc(controld_api, pcmk_ipc_dispatch_sync);
+ if (rc != pcmk_rc_ok) {
+ fprintf(stderr, "error: Could not connect to controller: %s\n",
+ pcmk_rc_str(rc));
+ pcmk_free_ipc_api(controld_api);
+ return rc;
+ }
+
+ rc = pcmk_ipc_purge_node(controld_api, node_name, nodeid);
+ if (rc != pcmk_rc_ok) {
+ fprintf(stderr,
+ "error: Could not clear node from controller's cache: %s\n",
+ pcmk_rc_str(rc));
+ }
+
+ pcmk_free_ipc_api(controld_api);
+ return pcmk_rc_ok;
+}
+
+static int
+tools_remove_node_cache(const char *node_name, long nodeid, const char *target)
+{
+ int rc = -1;
+ crm_ipc_t *conn = NULL;
+ xmlNode *cmd = NULL;
+
+ conn = crm_ipc_new(target, 0);
+ if (!conn) {
+ return -ENOTCONN;
+ }
+ if (!crm_ipc_connect(conn)) {
+ crm_perror(LOG_ERR, "Connection to %s failed", target);
+ crm_ipc_destroy(conn);
+ return -ENOTCONN;
+ }
+
+ crm_trace("Removing %s[%ld] from the %s membership cache",
+ node_name, nodeid, target);
+
+ if(pcmk__str_eq(target, T_ATTRD, pcmk__str_casei)) {
+ cmd = create_xml_node(NULL, __func__);
+
+ crm_xml_add(cmd, F_TYPE, T_ATTRD);
+ crm_xml_add(cmd, F_ORIG, crm_system_name);
+
+ crm_xml_add(cmd, PCMK__XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE);
+
+ pcmk__xe_add_node(cmd, node_name, nodeid);
+
+ } else { // Fencer or pacemakerd
+ cmd = create_request(CRM_OP_RM_NODE_CACHE, NULL, NULL, target,
+ crm_system_name, NULL);
+ if (nodeid > 0) {
+ crm_xml_set_id(cmd, "%ld", nodeid);
+ }
+ crm_xml_add(cmd, XML_ATTR_UNAME, node_name);
+ }
+
+ rc = crm_ipc_send(conn, cmd, 0, 0, NULL);
+ crm_debug("%s peer cache cleanup for %s (%ld): %d",
+ target, node_name, nodeid, rc);
+
+ if (rc > 0) {
+ // @TODO Should this be done just once after all the rest?
+ rc = cib_remove_node(nodeid, node_name);
+ }
+
+ if (conn) {
+ crm_ipc_close(conn);
+ crm_ipc_destroy(conn);
+ }
+ free_xml(cmd);
+ return rc > 0 ? 0 : rc;
+}
+
+static void
+remove_node(const char *target_uname)
+{
+ int rc;
+ int d = 0;
+ long nodeid = 0;
+ const char *node_name = NULL;
+ char *endptr = NULL;
+ const char *daemons[] = {
+ "stonith-ng",
+ T_ATTRD,
+ CRM_SYSTEM_MCP,
+ };
+
+ // Check whether node was specified by name or numeric ID
+ errno = 0;
+ nodeid = strtol(target_uname, &endptr, 10);
+ if ((errno != 0) || (endptr == target_uname) || (*endptr != '\0')
+ || (nodeid <= 0)) {
+ // It's not a positive integer, so assume it's a node name
+ nodeid = 0;
+ node_name = target_uname;
+ }
+
+ rc = controller_remove_node(node_name, nodeid);
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ return;
+ }
+
+ for (d = 0; d < PCMK__NELEM(daemons); d++) {
+ if (tools_remove_node_cache(node_name, nodeid, daemons[d])) {
+ crm_err("Failed to connect to %s to remove node '%s'",
+ daemons[d], target_uname);
+ exit_code = CRM_EX_ERROR;
+ return;
+ }
+ }
+ exit_code = CRM_EX_OK;
+}
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args, GOptionGroup *group) {
+ GOptionContext *context = NULL;
+
+ GOptionEntry extra_prog_entries[] = {
+ { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &(args->quiet),
+ "Be less descriptive in output.",
+ NULL },
+
+ { NULL }
+ };
+
+ context = pcmk__build_arg_context(args, NULL, &group, NULL);
+
+ /* Add the -q option, which cannot be part of the globally supported options
+ * because some tools use that flag for something else.
+ */
+ pcmk__add_main_args(context, extra_prog_entries);
+
+ pcmk__add_arg_group(context, "commands", "Commands:",
+ "Show command help", command_entries);
+ pcmk__add_arg_group(context, "additional", "Additional Options:",
+ "Show additional options", addl_entries);
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ GError *error = NULL;
+
+ GOptionGroup *output_group = NULL;
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, "NR");
+ GOptionContext *context = build_arg_context(args, output_group);
+
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("crm_node", args->verbosity);
+
+ if (args->version) {
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+ /* FIXME: When crm_node is converted to use formatted output, this can go. */
+ pcmk__cli_help('v');
+ }
+
+ if (options.command == 0) {
+ char *help = g_option_context_get_help(context, TRUE, NULL);
+
+ fprintf(stderr, "%s", help);
+ g_free(help);
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ if (options.dangerous_cmd && options.force_flag == FALSE) {
+ fprintf(stderr, "The supplied command is considered dangerous."
+ " To prevent accidental destruction of the cluster,"
+ " the --force flag is required in order to proceed.\n");
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ switch (options.command) {
+ case 'n':
+ print_node_name();
+ break;
+ case 'R':
+ remove_node(options.target_uname);
+ break;
+ case 'i':
+ case 'q':
+ case 'N':
+ /* FIXME: Use pcmk__query_node_name() after conversion to formatted
+ * output
+ */
+ run_controller_mainloop(options.nodeid, false);
+ break;
+ case 'l':
+ case 'p':
+ run_controller_mainloop(0, true);
+ break;
+ default:
+ break;
+ }
+
+done:
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+
+ pcmk__output_and_clear_error(&error, NULL);
+ return crm_exit(exit_code);
+}
diff --git a/tools/crm_report.in b/tools/crm_report.in
new file mode 100644
index 0000000..1818879
--- /dev/null
+++ b/tools/crm_report.in
@@ -0,0 +1,481 @@
+#!/bin/sh
+#
+# Copyright 2010-2019 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.
+#
+
+TEMP=`@GETOPT_PATH@ \
+ -o hv?xl:f:t:n:T:L:p:c:dSCu:D:MVse: \
+ --long help,corosync,cts:,cts-log:,dest:,node:,nodes:,from:,to:,sos-mode,logfile:,as-directory,single-node,cluster:,user:,max-depth:,version,features,rsh: \
+ -n 'crm_report' -- "$@"`
+# The quotes around $TEMP are essential
+eval set -- "$TEMP"
+
+progname=$(basename "$0")
+rsh="ssh -T"
+tests=""
+nodes=""
+compress=1
+cluster="any"
+ssh_user="root"
+search_logs=1
+sos_mode=0
+report_data=`dirname $0`
+maxdepth=5
+
+extra_logs=""
+sanitize_patterns="passw.*"
+log_patterns="CRIT: ERROR:"
+
+usage() {
+cat<<EOF
+$progname - Create archive of everything needed when reporting cluster problems
+
+
+Usage: $progname [options] [DEST]
+
+Required option:
+ -f, --from TIME time prior to problems beginning
+ (as "YYYY-M-D H:M:S" including the quotes)
+
+Options:
+ -V increase verbosity (may be specified multiple times)
+ -h, --help display this message
+ -v, --version display software version
+ --features display software features
+ -t, --to TIME time at which all problems were resolved
+ (as "YYYY-M-D H:M:S" including the quotes; default "now")
+ -T, --cts TEST CTS test or set of tests to extract
+ --cts-log CTS master logfile
+ -n, --nodes NODES node names for this cluster (only needed if cluster is
+ not active on this host; accepts -n "a b" or -n a -n b)
+ -M do not search for cluster logs
+ -l, --logfile FILE log file to collect (in addition to detected logs if -M
+ is not specified; may be specified multiple times)
+ -p PATT additional regular expression to match variables to be
+ masked in output (default: "passw.*")
+ -L PATT additional regular expression to match in log files for
+ analysis (default: $log_patterns)
+ -S, --single-node don't attempt to collect data from other nodes
+ -c, --cluster TYPE force the cluster type instead of detecting
+ (currently only corosync is supported)
+ -C, --corosync force the cluster type to be corosync
+ -u, --user USER username to use when collecting data from other nodes
+ (default root)
+ -D, --max-depth search depth to use when attempting to locate files
+ -e, --rsh command to use to run commands on other nodes
+ (default ssh -T)
+ -d, --as-directory leave result as a directory tree instead of archiving
+ --sos-mode use defaults suitable for being called by sosreport tool
+ (behavior subject to change and not useful to end users)
+ DEST, --dest DEST custom destination directory or file name
+
+$progname works best when run from a cluster node on a running cluster,
+but can be run from a stopped cluster node or a Pacemaker Remote node.
+
+If neither --nodes nor --single-node is given, $progname will guess the
+node list, but may have trouble detecting Pacemaker Remote nodes.
+Unless --single-node is given, the node names (whether specified by --nodes
+or detected automatically) must be resolvable and reachable via the command
+specified by -e/--rsh using the user specified by -u/--user.
+
+Examples:
+ $progname -f "2011-12-14 13:05:00" unexplained-apache-failure
+ $progname -f 2011-12-14 -t 2011-12-15 something-that-took-multiple-days
+ $progname -f 13:05:00 -t 13:12:00 brief-outage
+EOF
+}
+
+case "$1" in
+ -v|--version) echo "$progname @VERSION@-@BUILD_VERSION@"; exit 0;;
+ --features) echo "@VERSION@-@BUILD_VERSION@: @PCMK_FEATURES@"; exit 0;;
+ --|-h|--help) usage; exit 0;;
+esac
+
+# Prefer helpers in the same directory if they exist, to simplify development
+if [ ! -f $report_data/report.common ]; then
+ report_data=@datadir@/@PACKAGE@
+else
+ echo "Using local helpers"
+fi
+
+. $report_data/report.common
+
+while true; do
+ case "$1" in
+ -x) set -x; shift;;
+ -V) verbose=`expr $verbose + 1`; shift;;
+ -T|--cts) tests="$tests $2"; shift; shift;;
+ --cts-log) ctslog="$2"; shift; shift;;
+ -f|--from) start_time=`get_time "$2"`; shift; shift;;
+ -t|--to) end_time=`get_time "$2"`; shift; shift;;
+ -n|--node|--nodes) nodes="$nodes $2"; shift; shift;;
+ -S|--single-node) nodes="$host"; shift;;
+ -l|--logfile) extra_logs="$extra_logs $2"; shift; shift;;
+ -p) sanitize_patterns="$sanitize_patterns $2"; shift; shift;;
+ -L) log_patterns="$log_patterns `echo $2 | sed 's/ /\\\W/g'`"; shift; shift;;
+ -d|--as-directory) compress=0; shift;;
+ -C|--corosync) cluster="corosync"; shift;;
+ -c|--cluster) cluster="$2"; shift; shift;;
+ -e|--rsh) rsh="$2"; shift; shift;;
+ -u|--user) ssh_user="$2"; shift; shift;;
+ -D|--max-depth) maxdepth="$2"; shift; shift;;
+ -M) search_logs=0; shift;;
+ --sos-mode) sos_mode=1; nodes="$host"; shift;;
+ --dest) DESTDIR=$2; shift; shift;;
+ --) if [ ! -z $2 ]; then DESTDIR=$2; fi; break;;
+ -h|--help) usage; exit 0;;
+ # Options for compatibility with hb_report
+ -s) shift;;
+
+ *) echo "Unknown argument: $1"; usage; exit 1;;
+ esac
+done
+
+
+collect_data() {
+ label="$1"
+ start=`expr $2 - 10`
+ end=`expr $3 + 10`
+ masterlog=$4
+
+ if [ "x$DESTDIR" != x ]; then
+ echo $DESTDIR | grep -e "^/" -qs
+ if [ $? = 0 ]; then
+ l_base=$DESTDIR
+ else
+ l_base="`pwd`/$DESTDIR"
+ fi
+ debug "Using custom scratch dir: $l_base"
+ r_base=`basename $l_base`
+ else
+ l_base=$HOME/$label
+ r_base=$label
+ fi
+
+ if [ -e $l_base ]; then
+ fatal "Output directory $l_base already exists, specify an alternate name with --dest"
+ fi
+ mkdir -p $l_base
+
+ if [ "x$masterlog" != "x" ]; then
+ dumplogset "$masterlog" $start $end > "$l_base/$HALOG_F"
+ fi
+
+ for node in $nodes; do
+ cat <<EOF >$l_base/.env
+LABEL="$label"
+REPORT_HOME="$r_base"
+REPORT_MASTER="$host"
+REPORT_TARGET="$node"
+LOG_START=$start
+LOG_END=$end
+REMOVE=1
+SANITIZE="$sanitize_patterns"
+CLUSTER=$cluster
+LOG_PATTERNS="$log_patterns"
+EXTRA_LOGS="$extra_logs"
+SEARCH_LOGS=$search_logs
+SOS_MODE=$sos_mode
+verbose=$verbose
+maxdepth=$maxdepth
+EOF
+
+ if [ $host = $node ]; then
+ cat <<EOF >>$l_base/.env
+REPORT_HOME="$l_base"
+EOF
+ cat $l_base/.env $report_data/report.common $report_data/report.collector > $l_base/collector
+ bash $l_base/collector
+ else
+ cat $l_base/.env $report_data/report.common $report_data/report.collector \
+ | $rsh -l $ssh_user $node -- "mkdir -p $r_base; cat > $r_base/collector; bash $r_base/collector" | (cd $l_base && tar mxf -)
+ fi
+ done
+
+ analyze $l_base > $l_base/$ANALYSIS_F
+ if [ -f $l_base/$HALOG_F ]; then
+ node_events $l_base/$HALOG_F > $l_base/$EVENTS_F
+ fi
+
+ for node in $nodes; do
+ cat $l_base/$node/$ANALYSIS_F >> $l_base/$ANALYSIS_F
+ if [ -s $l_base/$node/$EVENTS_F ]; then
+ cat $l_base/$node/$EVENTS_F >> $l_base/$EVENTS_F
+ elif [ -s $l_base/$HALOG_F ]; then
+ awk "\$4==\"$nodes\"" $l_base/$EVENTS_F >> $l_base/$n/$EVENTS_F
+ fi
+ done
+
+ log " "
+ if [ $compress = 1 ]; then
+ fname=`shrink $l_base`
+ rm -rf $l_base
+ log "Collected results are available in $fname"
+ log " "
+ log "Please create a bug entry at"
+ log " @BUG_URL@"
+ log "Include a description of your problem and attach this tarball"
+ log " "
+ log "Thank you for taking time to create this report."
+ else
+ log "Collected results are available in $l_base"
+ fi
+ log " "
+}
+
+#
+# check if files have same content in the cluster
+#
+cibdiff() {
+ d1=$(dirname $1)
+ d2=$(dirname $2)
+
+ if [ -f "$d1/RUNNING" ] && [ ! -f "$d2/RUNNING" ]; then
+ DIFF_OK=0
+ elif [ -f "$d1/STOPPED" ] && [ ! -f "$d2/STOPPED" ]; then
+ DIFF_OK=0
+ else
+ DIFF_OK=1
+ fi
+
+ if [ $DIFF_OK -eq 1 ]; then
+ if which crm_diff > /dev/null 2>&1; then
+ crm_diff -c -n $1 -o $2
+ else
+ info "crm_diff(8) not found, cannot diff CIBs"
+ fi
+ else
+ echo "can't compare cibs from running and stopped systems"
+ fi
+}
+
+diffcheck() {
+ [ -f "$1" ] || {
+ echo "$1 does not exist"
+ return 1
+ }
+ [ -f "$2" ] || {
+ echo "$2 does not exist"
+ return 1
+ }
+ case $(basename "$1") in
+ $CIB_F) cibdiff $1 $2 ;;
+ *) diff -u $1 $2 ;;
+ esac
+}
+
+#
+# remove duplicates if files are same, make links instead
+#
+consolidate() {
+ for n in $nodes; do
+ if [ -f $1/$2 ]; then
+ rm $1/$n/$2
+ else
+ mv $1/$n/$2 $1
+ fi
+ ln -s ../$2 $1/$n
+ done
+}
+
+analyze_one() {
+ rc=0
+ node0=""
+ for n in $nodes; do
+ if [ "$node0" ]; then
+ diffcheck $1/$node0/$2 $1/$n/$2
+ rc=$(($rc+$?))
+ else
+ node0=$n
+ fi
+ done
+ return $rc
+}
+
+analyze() {
+ flist="$MEMBERSHIP_F $CIB_F $CRM_MON_F $SYSINFO_F"
+ for f in $flist; do
+ printf "Diff $f... "
+ ls $1/*/$f >/dev/null 2>&1 || {
+ echo "no $1/*/$f :/"
+ continue
+ }
+ if analyze_one $1 $f; then
+ echo "OK"
+ [ "$f" != $CIB_F ] && consolidate $1 $f
+ else
+ echo ""
+ fi
+ done
+}
+
+do_cts() {
+ test_sets=`echo $tests | tr ',' ' '`
+ for test_set in $test_sets; do
+
+ start_time=0
+ start_test=`echo $test_set | tr '-' ' ' | awk '{print $1}'`
+
+ end_time=0
+ end_test=`echo $test_set | tr '-' ' ' | awk '{print $2}'`
+
+ if [ x$end_test = x ]; then
+ msg="Extracting test $start_test"
+ label="CTS-$start_test-`date +"%b-%d-%Y"`"
+ end_test=`expr $start_test + 1`
+ else
+ msg="Extracting tests $start_test to $end_test"
+ label="CTS-$start_test-$end_test-`date +"%b-%d-%Y"`"
+ end_test=`expr $end_test + 1`
+ fi
+
+ if [ $start_test = 0 ]; then
+ start_pat="BEGINNING [0-9].* TESTS"
+ else
+ start_pat="Running test.*\[ *$start_test\]"
+ fi
+
+ if [ x$ctslog = x ]; then
+ ctslog=`findmsg 1 "$start_pat"`
+
+ if [ x$ctslog = x ]; then
+ fatal "No CTS control file detected"
+ else
+ log "Using CTS control file: $ctslog"
+ fi
+ fi
+
+ line=`grep -n "$start_pat" $ctslog | tail -1 | sed 's/:.*//'`
+ if [ ! -z "$line" ]; then
+ start_time=`linetime $ctslog $line`
+ fi
+
+ line=`grep -n "Running test.*\[ *$end_test\]" $ctslog | tail -1 | sed 's/:.*//'`
+ if [ ! -z "$line" ]; then
+ end_time=`linetime $ctslog $line`
+ fi
+
+ if [ -z "$nodes" ]; then
+ nodes=`grep CTS: $ctslog | grep -v debug: | grep " \* " | sed s:.*\\\*::g | sort -u | tr '\\n' ' '`
+ log "Calculated node list: $nodes"
+ fi
+
+ if [ $end_time -lt $start_time ]; then
+ debug "Test didn't complete, grabbing everything up to now"
+ end_time=`date +%s`
+ fi
+
+ if [ $start_time != 0 ];then
+ log "$msg (`time2str $start_time` to `time2str $end_time`)"
+ collect_data $label $start_time $end_time $ctslog
+ else
+ fatal "$msg failed: not found"
+ fi
+ done
+}
+
+node_names_from_xml() {
+ awk '
+ /uname/ {
+ for( i=1; i<=NF; i++ )
+ if( $i~/^uname=/ ) {
+ sub("uname=.","",$i);
+ sub("\".*","",$i);
+ print $i;
+ next;
+ }
+ }
+ ' | tr '\n' ' '
+}
+
+getnodes() {
+ cluster="$1"
+
+ # 1. Live (cluster nodes or Pacemaker Remote nodes)
+ # TODO: This will not detect Pacemaker Remote nodes unless they
+ # have ever had a permanent node attribute set, because it only
+ # searches the nodes section. It should also search the config
+ # for resources that create Pacemaker Remote nodes.
+ cib_nodes=$(cibadmin -Ql -o nodes 2>/dev/null)
+ if [ $? -eq 0 ]; then
+ debug "Querying CIB for nodes"
+ echo "$cib_nodes" | node_names_from_xml
+ return
+ fi
+
+ # 2. Saved
+ if [ -f "@CRM_CONFIG_DIR@/cib.xml" ]; then
+ debug "Querying on-disk CIB for nodes"
+ grep "node " "@CRM_CONFIG_DIR@/cib.xml" | node_names_from_xml
+ return
+ fi
+
+ # 3. logs
+ # TODO: Look for something like crm_update_peer
+}
+
+if [ $compress -eq 1 ]; then
+ require_tar
+fi
+
+if [ "x$tests" != "x" ]; then
+ do_cts
+
+elif [ "x$start_time" != "x" ]; then
+ masterlog=""
+
+ if [ -z "$sanitize_patterns" ]; then
+ log "WARNING: The tarball produced by this program may contain"
+ log " sensitive information such as passwords."
+ log ""
+ log "We will attempt to remove such information if you use the"
+ log "-p option. For example: -p \"pass.*\" -p \"user.*\""
+ log ""
+ log "However, doing this may reduce the ability for the recipients"
+ log "to diagnose issues and generally provide assistance."
+ log ""
+ log "IT IS YOUR RESPONSIBILITY TO PROTECT SENSITIVE DATA FROM EXPOSURE"
+ log ""
+ fi
+
+ # If user didn't specify a cluster stack, make a best guess if possible.
+ if [ -z "$cluster" ] || [ "$cluster" = "any" ]; then
+ cluster=$(get_cluster_type)
+ fi
+
+ # If user didn't specify node(s), make a best guess if possible.
+ if [ -z "$nodes" ]; then
+ nodes=`getnodes $cluster`
+ if [ -n "$nodes" ]; then
+ log "Calculated node list: $nodes"
+ else
+ fatal "Cannot determine nodes; specify --nodes or --single-node"
+ fi
+ fi
+
+ if
+ echo $nodes | grep -qs $host
+ then
+ debug "We are a cluster node"
+ else
+ debug "We are a log master"
+ masterlog=`findmsg 1 "pacemaker-controld\\|CTS"`
+ fi
+
+
+ if [ -z $end_time ]; then
+ end_time=`perl -e 'print time()'`
+ fi
+ label="pcmk-`date +"%a-%d-%b-%Y"`"
+ log "Collecting data from $nodes (`time2str $start_time` to `time2str $end_time`)"
+ collect_data $label $start_time $end_time $masterlog
+else
+ fatal "Not sure what to do, no tests or time ranges to extract"
+fi
+
+# vim: set expandtab tabstop=8 softtabstop=4 shiftwidth=4 textwidth=80:
diff --git a/tools/crm_resource.8.inc b/tools/crm_resource.8.inc
new file mode 100644
index 0000000..a92fac6
--- /dev/null
+++ b/tools/crm_resource.8.inc
@@ -0,0 +1,5 @@
+[synopsis]
+crm_resource <query>|<command> [options]
+
+/Pacemaker cluster resources/
+.SH OPTIONS
diff --git a/tools/crm_resource.c b/tools/crm_resource.c
new file mode 100644
index 0000000..f351c26
--- /dev/null
+++ b/tools/crm_resource.c
@@ -0,0 +1,2182 @@
+/*
+ * Copyright 2004-2023 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 <crm_resource.h>
+#include <crm/lrmd_internal.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/ipc_attrd_internal.h>
+#include <crm/common/lists_internal.h>
+#include <crm/common/output.h>
+#include <pacemaker-internal.h>
+
+#include <sys/param.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <time.h>
+
+#include <crm/crm.h>
+#include <crm/stonith-ng.h>
+#include <crm/common/ipc_controld.h>
+#include <crm/cib/internal.h>
+
+#define SUMMARY "crm_resource - perform tasks related to Pacemaker cluster resources"
+
+enum rsc_command {
+ cmd_none = 0, // No command option given (yet)
+ cmd_ban,
+ cmd_cleanup,
+ cmd_clear,
+ cmd_colocations,
+ cmd_cts,
+ cmd_delete,
+ cmd_delete_param,
+ cmd_digests,
+ cmd_execute_agent,
+ cmd_fail,
+ cmd_get_param,
+ cmd_get_property,
+ cmd_list_active_ops,
+ cmd_list_agents,
+ cmd_list_all_ops,
+ cmd_list_alternatives,
+ cmd_list_instances,
+ cmd_list_providers,
+ cmd_list_resources,
+ cmd_list_standards,
+ cmd_locate,
+ cmd_metadata,
+ cmd_move,
+ cmd_query_raw_xml,
+ cmd_query_xml,
+ cmd_refresh,
+ cmd_restart,
+ cmd_set_param,
+ cmd_set_property,
+ cmd_wait,
+ cmd_why,
+};
+
+struct {
+ 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
+ gboolean require_node; // Whether command requires node 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; // --promoted 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, utilization, or element 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
+ int check_level; // Optional value of --validate or --force-check
+
+ // 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,
+ .check_level = -1,
+ .cib_options = cib_sync_call,
+ .require_cib = TRUE,
+ .require_dataset = TRUE,
+ .require_resource = TRUE,
+};
+
+#if 0
+// @COMPAT @TODO enable this at next backward compatibility break
+#define SET_COMMAND(cmd) do { \
+ if (options.rsc_cmd != cmd_none) { \
+ g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE, \
+ "Only one command option may be specified"); \
+ return FALSE; \
+ } \
+ options.rsc_cmd = (cmd); \
+ } while (0)
+#else
+#define SET_COMMAND(cmd) do { \
+ if (options.rsc_cmd != cmd_none) { \
+ reset_options(); \
+ } \
+ options.rsc_cmd = (cmd); \
+ } while (0)
+#endif
+
+gboolean agent_provider_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean attr_set_type_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean class_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean cleanup_refresh_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean delete_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean expired_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean list_agents_cb(const gchar *option_name, const gchar *optarg,
+ gpointer data, GError **error);
+gboolean list_providers_cb(const gchar *option_name, const gchar *optarg,
+ gpointer data, GError **error);
+gboolean list_standards_cb(const gchar *option_name, const gchar *optarg,
+ gpointer data, GError **error);
+gboolean list_alternatives_cb(const gchar *option_name, const gchar *optarg,
+ gpointer data, GError **error);
+gboolean metadata_cb(const gchar *option_name, const gchar *optarg,
+ gpointer data, GError **error);
+gboolean option_cb(const gchar *option_name, const gchar *optarg,
+ gpointer data, GError **error);
+gboolean fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean flag_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean get_param_prop_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean list_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean set_delete_param_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean set_prop_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean timeout_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+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);
+
+static crm_exit_t exit_code = CRM_EX_OK;
+static pcmk__output_t *out = NULL;
+static pcmk__common_args_t *args = NULL;
+
+// Things that should be cleaned up on exit
+static GError *error = NULL;
+static GMainLoop *mainloop = NULL;
+static cib_t *cib_conn = NULL;
+static pcmk_ipc_api_t *controld_api = NULL;
+static pe_working_set_t *data_set = NULL;
+
+#define MESSAGE_TIMEOUT_S 60
+
+#define INDENT " "
+
+static pcmk__supported_format_t formats[] = {
+ PCMK__SUPPORTED_FORMAT_NONE,
+ PCMK__SUPPORTED_FORMAT_TEXT,
+ PCMK__SUPPORTED_FORMAT_XML,
+ { NULL, NULL, NULL }
+};
+
+// Clean up and exit
+static crm_exit_t
+bye(crm_exit_t ec)
+{
+ pcmk__output_and_clear_error(&error, out);
+
+ if (out != NULL) {
+ out->finish(out, ec, true, NULL);
+ pcmk__output_free(out);
+ }
+ pcmk__unregister_formats();
+
+ if (cib_conn != NULL) {
+ cib_t *save_cib_conn = cib_conn;
+
+ cib_conn = NULL; // Ensure we can't free this twice
+ cib__clean_up_connection(&save_cib_conn);
+ }
+
+ if (controld_api != NULL) {
+ pcmk_ipc_api_t *save_controld_api = controld_api;
+
+ controld_api = NULL; // Ensure we can't free this twice
+ pcmk_free_ipc_api(save_controld_api);
+ }
+
+ if (mainloop != NULL) {
+ g_main_loop_unref(mainloop);
+ mainloop = NULL;
+ }
+
+ pe_free_working_set(data_set);
+ data_set = NULL;
+ crm_exit(ec);
+ return ec;
+}
+
+static void
+quit_main_loop(crm_exit_t ec)
+{
+ exit_code = ec;
+ if (mainloop != NULL) {
+ GMainLoop *mloop = mainloop;
+
+ mainloop = NULL; // Don't re-enter this block
+ pcmk_quit_main_loop(mloop, 10);
+ g_main_loop_unref(mloop);
+ }
+}
+
+static gboolean
+resource_ipc_timeout(gpointer data)
+{
+ // Start with newline because "Waiting for ..." message doesn't have one
+ if (error != NULL) {
+ g_clear_error(&error);
+ }
+
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_TIMEOUT,
+ _("Aborting because no messages received in %d seconds"), MESSAGE_TIMEOUT_S);
+
+ quit_main_loop(CRM_EX_TIMEOUT);
+ return FALSE;
+}
+
+static void
+controller_event_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type,
+ crm_exit_t status, void *event_data, void *user_data)
+{
+ switch (event_type) {
+ case pcmk_ipc_event_disconnect:
+ if (exit_code == CRM_EX_DISCONNECT) { // Unexpected
+ crm_info("Connection to controller was terminated");
+ }
+ quit_main_loop(exit_code);
+ break;
+
+ case pcmk_ipc_event_reply:
+ if (status != CRM_EX_OK) {
+ out->err(out, "Error: bad reply from controller: %s",
+ crm_exit_str(status));
+ pcmk_disconnect_ipc(api);
+ quit_main_loop(status);
+ } else {
+ if ((pcmk_controld_api_replies_expected(api) == 0)
+ && mainloop && g_main_loop_is_running(mainloop)) {
+ out->info(out, "... got reply (done)");
+ crm_debug("Got all the replies we expected");
+ pcmk_disconnect_ipc(api);
+ quit_main_loop(CRM_EX_OK);
+ } else {
+ out->info(out, "... got reply");
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+static void
+start_mainloop(pcmk_ipc_api_t *capi)
+{
+ unsigned int count = pcmk_controld_api_replies_expected(capi);
+
+ if (count > 0) {
+ out->info(out, "Waiting for %u %s from the controller",
+ count, pcmk__plural_alt(count, "reply", "replies"));
+ exit_code = CRM_EX_DISCONNECT; // For unexpected disconnects
+ mainloop = g_main_loop_new(NULL, FALSE);
+ g_timeout_add(MESSAGE_TIMEOUT_S * 1000, resource_ipc_timeout, NULL);
+ g_main_loop_run(mainloop);
+ }
+}
+
+static int
+compare_id(gconstpointer a, gconstpointer b)
+{
+ return strcmp((const char *)a, (const char *)b);
+}
+
+static GList *
+build_constraint_list(xmlNode *root)
+{
+ GList *retval = NULL;
+ xmlNode *cib_constraints = NULL;
+ xmlXPathObjectPtr xpathObj = NULL;
+ int ndx = 0;
+
+ cib_constraints = pcmk_find_cib_element(root, XML_CIB_TAG_CONSTRAINTS);
+ xpathObj = xpath_search(cib_constraints, "//" XML_CONS_TAG_RSC_LOCATION);
+
+ for (ndx = 0; ndx < numXpathResults(xpathObj); ndx++) {
+ xmlNode *match = getXpathResult(xpathObj, ndx);
+ retval = g_list_insert_sorted(retval, (gpointer) ID(match), compare_id);
+ }
+
+ freeXpathObject(xpathObj);
+ return retval;
+}
+
+/* short option letters still available: eEJkKXyYZ */
+
+static GOptionEntry query_entries[] = {
+ { "list", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb,
+ "List all cluster resources with status",
+ NULL },
+ { "list-raw", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb,
+ "List IDs of all instantiated resources (individual members\n"
+ INDENT "rather than groups etc.)",
+ NULL },
+ { "list-cts", 'c', G_OPTION_FLAG_HIDDEN|G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb,
+ NULL,
+ NULL },
+ { "list-operations", 'O', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb,
+ "List active resource operations, optionally filtered by\n"
+ INDENT "--resource and/or --node",
+ NULL },
+ { "list-all-operations", 'o', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb,
+ "List all resource operations, optionally filtered by\n"
+ INDENT "--resource and/or --node",
+ NULL },
+ { "list-standards", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
+ list_standards_cb,
+ "List supported standards",
+ NULL },
+ { "list-ocf-providers", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
+ list_providers_cb,
+ "List all available OCF providers",
+ NULL },
+ { "list-agents", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
+ list_agents_cb,
+ "List all agents available for the named standard and/or provider",
+ "STD:PROV" },
+ { "list-ocf-alternatives", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
+ list_alternatives_cb,
+ "List all available providers for the named OCF agent",
+ "AGENT" },
+ { "show-metadata", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
+ metadata_cb,
+ "Show the metadata for the named class:provider:agent",
+ "SPEC" },
+ { "query-xml", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
+ "Show XML configuration of resource (after any template expansion)",
+ NULL },
+ { "query-xml-raw", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
+ "Show XML configuration of resource (before any template expansion)",
+ NULL },
+ { "get-parameter", 'g', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, get_param_prop_cb,
+ "Display named parameter for resource (use instance attribute\n"
+ INDENT "unless --element, --meta, or --utilization is specified)",
+ "PARAM" },
+ { "get-property", 'G', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, get_param_prop_cb,
+ "Display named property of resource ('class', 'type', or 'provider') "
+ "(requires --resource)",
+ "PROPERTY" },
+ { "locate", 'W', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
+ "Show node(s) currently running resource",
+ NULL },
+ { "constraints", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
+ "Display the location and colocation constraints that apply to a\n"
+ INDENT "resource, and if --recursive is specified, to the resources\n"
+ INDENT "directly or indirectly involved in those colocations.\n"
+ INDENT "If the named resource is part of a group, or a clone or\n"
+ INDENT "bundle instance, constraints for the collective resource\n"
+ INDENT "will be shown unless --force is given.",
+ NULL },
+ { "stack", 'A', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
+ "Equivalent to --constraints --recursive",
+ NULL },
+ { "why", 'Y', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, why_cb,
+ "Show why resources are not running, optionally filtered by\n"
+ INDENT "--resource and/or --node",
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry command_entries[] = {
+ { "validate", 0, G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
+ validate_or_force_cb,
+ "Validate resource configuration by calling agent's validate-all\n"
+ INDENT "action. The configuration may be specified either by giving an\n"
+ INDENT "existing resource name with -r, or by specifying --class,\n"
+ INDENT "--agent, and --provider arguments, along with any number of\n"
+ INDENT "--option arguments. An optional LEVEL argument can be given\n"
+ INDENT "to control the level of checking performed.",
+ "LEVEL" },
+ { "cleanup", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, cleanup_refresh_cb,
+ "If resource has any past failures, clear its history and fail\n"
+ INDENT "count. Optionally filtered by --resource, --node, --operation\n"
+ INDENT "and --interval (otherwise all). --operation and --interval\n"
+ INDENT "apply to fail counts, but entire history is always clear, to\n"
+ INDENT "allow current state to be rechecked. If the named resource is\n"
+ INDENT "part of a group, or one numbered instance of a clone or bundled\n"
+ INDENT "resource, the clean-up applies to the whole collective resource\n"
+ INDENT "unless --force is given.",
+ NULL },
+ { "refresh", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, cleanup_refresh_cb,
+ "Delete resource's history (including failures) so its current state\n"
+ INDENT "is rechecked. Optionally filtered by --resource and --node\n"
+ INDENT "(otherwise all). If the named resource is part of a group, or one\n"
+ INDENT "numbered instance of a clone or bundled resource, the refresh\n"
+ INDENT "applies to the whole collective resource unless --force is given.",
+ NULL },
+ { "set-parameter", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, set_delete_param_cb,
+ "Set named parameter for resource (requires -v). Use instance\n"
+ INDENT "attribute unless --element, --meta, or --utilization is "
+ "specified.",
+ "PARAM" },
+ { "delete-parameter", 'd', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, set_delete_param_cb,
+ "Delete named parameter for resource. Use instance attribute\n"
+ INDENT "unless --element, --meta or, --utilization is specified.",
+ "PARAM" },
+ { "set-property", 'S', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, set_prop_cb,
+ "Set named property of resource ('class', 'type', or 'provider') "
+ "(requires -r, -t, -v)",
+ "PROPERTY" },
+
+ { NULL }
+};
+
+static GOptionEntry location_entries[] = {
+ { "move", 'M', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
+ "Create a constraint to move resource. If --node is specified,\n"
+ INDENT "the constraint will be to move to that node, otherwise it\n"
+ INDENT "will be to ban the current node. Unless --force is specified\n"
+ INDENT "this will return an error if the resource is already running\n"
+ INDENT "on the specified node. If --force is specified, this will\n"
+ INDENT "always ban the current node.\n"
+ INDENT "Optional: --lifetime, --promoted. NOTE: This may prevent the\n"
+ INDENT "resource from running on its previous location until the\n"
+ INDENT "implicit constraint expires or is removed with --clear.",
+ NULL },
+ { "ban", 'B', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
+ "Create a constraint to keep resource off a node.\n"
+ INDENT "Optional: --node, --lifetime, --promoted.\n"
+ INDENT "NOTE: This will prevent the resource from running on the\n"
+ INDENT "affected node until the implicit constraint expires or is\n"
+ INDENT "removed with --clear. If --node is not specified, it defaults\n"
+ INDENT "to the node currently running the resource for primitives\n"
+ INDENT "and groups, or the promoted instance of promotable clones with\n"
+ INDENT "promoted-max=1 (all other situations result in an error as\n"
+ INDENT "there is no sane default).",
+ NULL },
+ { "clear", 'U', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
+ "Remove all constraints created by the --ban and/or --move\n"
+ INDENT "commands. Requires: --resource. Optional: --node, --promoted,\n"
+ INDENT "--expired. If --node is not specified, all constraints created\n"
+ INDENT "by --ban and --move will be removed for the named resource. If\n"
+ INDENT "--node and --force are specified, any constraint created by\n"
+ INDENT "--move will be cleared, even if it is not for the specified\n"
+ INDENT "node. If --expired is specified, only those constraints whose\n"
+ INDENT "lifetimes have expired will be removed.",
+ NULL },
+ { "expired", 'e', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, expired_cb,
+ "Modifies the --clear argument to remove constraints with\n"
+ INDENT "expired lifetimes.",
+ NULL },
+ { "lifetime", 'u', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.move_lifetime,
+ "Lifespan (as ISO 8601 duration) of created constraints (with\n"
+ INDENT "-B, -M) see https://en.wikipedia.org/wiki/ISO_8601#Durations)",
+ "TIMESPEC" },
+ { "promoted", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
+ &options.promoted_role_only,
+ "Limit scope of command to promoted role (with -B, -M, -U). For\n"
+ INDENT "-B and -M, previously promoted instances may remain\n"
+ INDENT "active in the unpromoted role.",
+ NULL },
+
+ // Deprecated since 2.1.0
+ { "master", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
+ &options.promoted_role_only,
+ "Deprecated: Use --promoted instead", NULL },
+
+ { NULL }
+};
+
+static GOptionEntry advanced_entries[] = {
+ { "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, delete_cb,
+ "(Advanced) Delete a resource from the CIB. Required: -t",
+ NULL },
+ { "fail", 'F', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, fail_cb,
+ "(Advanced) Tell the cluster this resource has failed",
+ NULL },
+ { "restart", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, restart_cb,
+ "(Advanced) Tell the cluster to restart this resource and\n"
+ INDENT "anything that depends on it",
+ NULL },
+ { "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"
+ INDENT "node. Unless --force is specified, this will refuse to do so if\n"
+ INDENT "the cluster believes the resource is a clone instance already\n"
+ INDENT "running on the local node.",
+ NULL },
+ { "force-stop", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
+ validate_or_force_cb,
+ "(Advanced) Bypass the cluster and stop a resource on the local node",
+ NULL },
+ { "force-start", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
+ validate_or_force_cb,
+ "(Advanced) Bypass the cluster and start a resource on the local\n"
+ INDENT "node. Unless --force is specified, this will refuse to do so if\n"
+ INDENT "the cluster believes the resource is a clone instance already\n"
+ INDENT "running on the local node.",
+ NULL },
+ { "force-promote", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
+ validate_or_force_cb,
+ "(Advanced) Bypass the cluster and promote a resource on the local\n"
+ INDENT "node. Unless --force is specified, this will refuse to do so if\n"
+ INDENT "the cluster believes the resource is a clone instance already\n"
+ INDENT "running on the local node.",
+ NULL },
+ { "force-check", 0, G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
+ validate_or_force_cb,
+ "(Advanced) Bypass the cluster and check the state of a resource on\n"
+ INDENT "the local node. An optional LEVEL argument can be given\n"
+ INDENT "to control the level of checking performed.",
+ "LEVEL" },
+
+ { NULL }
+};
+
+static GOptionEntry addl_entries[] = {
+ { "node", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.host_uname,
+ "Node name",
+ "NAME" },
+ { "recursive", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.recursive,
+ "Follow colocation chains when using --set-parameter or --constraints",
+ NULL },
+ { "resource-type", 't', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.rsc_type,
+ "Resource XML element (primitive, group, etc.) (with -D)",
+ "ELEMENT" },
+ { "parameter-value", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.prop_value,
+ "Value to use with -p",
+ "PARAM" },
+ { "meta", 'm', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attr_set_type_cb,
+ "Use resource meta-attribute instead of instance attribute\n"
+ INDENT "(with -p, -g, -d)",
+ NULL },
+ { "utilization", 'z', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attr_set_type_cb,
+ "Use resource utilization attribute instead of instance attribute\n"
+ INDENT "(with -p, -g, -d)",
+ NULL },
+ { "element", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attr_set_type_cb,
+ "Use resource element attribute instead of instance attribute\n"
+ INDENT "(with -p, -g, -d)",
+ NULL },
+ { "operation", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.operation,
+ "Operation to clear instead of all (with -C -r)",
+ "OPERATION" },
+ { "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" },
+ { "nvpair", 'i', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.prop_id,
+ "(Advanced) XML ID of nvpair element to use (with -p, -d)",
+ "ID" },
+ { "timeout", 'T', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, timeout_cb,
+ "(Advanced) Abort if command does not finish in this time (with\n"
+ INDENT "--restart, --wait, --force-*)",
+ "N" },
+ { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force,
+ "Force the action to be performed. See help for individual commands for\n"
+ INDENT "additional behavior.",
+ NULL },
+ { "xml-file", 'x', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_FILENAME, &options.xml_file,
+ NULL,
+ "FILE" },
+ { "host-uname", 'H', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.host_uname,
+ NULL,
+ "HOST" },
+
+ { NULL }
+};
+
+static void
+reset_options(void) {
+ options.require_crmd = FALSE;
+ options.require_node = FALSE;
+
+ options.require_cib = TRUE;
+ options.require_dataset = TRUE;
+ options.require_resource = TRUE;
+
+ options.find_flags = 0;
+}
+
+gboolean
+agent_provider_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.cmdline_config = TRUE;
+ options.require_resource = FALSE;
+
+ if (pcmk__str_eq(option_name, "--provider", pcmk__str_casei)) {
+ pcmk__str_update(&options.v_provider, optarg);
+ } else {
+ pcmk__str_update(&options.v_agent, optarg);
+ }
+
+ return TRUE;
+}
+
+gboolean
+attr_set_type_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (pcmk__str_any_of(option_name, "-m", "--meta", NULL)) {
+ options.attr_set_type = XML_TAG_META_SETS;
+ } else if (pcmk__str_any_of(option_name, "-z", "--utilization", NULL)) {
+ options.attr_set_type = XML_TAG_UTILIZATION;
+ } else if (pcmk__str_eq(option_name, "--element", pcmk__str_casei)) {
+ options.attr_set_type = ATTR_SET_ELEMENT;
+ }
+ return TRUE;
+}
+
+gboolean
+class_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ pcmk__str_update(&options.v_class, optarg);
+ options.cmdline_config = TRUE;
+ options.require_resource = FALSE;
+ return TRUE;
+}
+
+gboolean
+cleanup_refresh_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (pcmk__str_any_of(option_name, "-C", "--cleanup", NULL)) {
+ SET_COMMAND(cmd_cleanup);
+ } else {
+ SET_COMMAND(cmd_refresh);
+ }
+
+ options.require_resource = FALSE;
+ if (getenv("CIB_file") == NULL) {
+ options.require_crmd = TRUE;
+ }
+ options.find_flags = pe_find_renamed|pe_find_anon;
+ return TRUE;
+}
+
+gboolean
+delete_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ SET_COMMAND(cmd_delete);
+ options.require_dataset = FALSE;
+ options.find_flags = pe_find_renamed|pe_find_any;
+ return TRUE;
+}
+
+gboolean
+expired_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.clear_expired = TRUE;
+ options.require_resource = FALSE;
+ return TRUE;
+}
+
+static void
+get_agent_spec(const gchar *optarg)
+{
+ options.require_cib = FALSE;
+ options.require_dataset = FALSE;
+ options.require_resource = FALSE;
+ pcmk__str_update(&options.agent_spec, optarg);
+}
+
+gboolean
+list_agents_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **error)
+{
+ SET_COMMAND(cmd_list_agents);
+ get_agent_spec(optarg);
+ return TRUE;
+}
+
+gboolean
+list_providers_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **error)
+{
+ SET_COMMAND(cmd_list_providers);
+ get_agent_spec(optarg);
+ return TRUE;
+}
+
+gboolean
+list_standards_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **error)
+{
+ SET_COMMAND(cmd_list_standards);
+ options.require_cib = FALSE;
+ options.require_dataset = FALSE;
+ options.require_resource = FALSE;
+ return TRUE;
+}
+
+gboolean
+list_alternatives_cb(const gchar *option_name, const gchar *optarg,
+ gpointer data, GError **error)
+{
+ SET_COMMAND(cmd_list_alternatives);
+ get_agent_spec(optarg);
+ return TRUE;
+}
+
+gboolean
+metadata_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **error)
+{
+ SET_COMMAND(cmd_metadata);
+ get_agent_spec(optarg);
+ return TRUE;
+}
+
+gboolean
+option_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **error)
+{
+ char *name = NULL;
+ char *value = NULL;
+
+ if (pcmk__scan_nvpair(optarg, &name, &value) != 2) {
+ return FALSE;
+ }
+ if (options.cmdline_params == NULL) {
+ options.cmdline_params = pcmk__strkey_table(free, free);
+ }
+ g_hash_table_replace(options.cmdline_params, name, value);
+ return TRUE;
+}
+
+gboolean
+fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ SET_COMMAND(cmd_fail);
+ options.require_crmd = TRUE;
+ options.require_node = TRUE;
+ return TRUE;
+}
+
+gboolean
+flag_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (pcmk__str_any_of(option_name, "-U", "--clear", NULL)) {
+ SET_COMMAND(cmd_clear);
+ options.find_flags = pe_find_renamed|pe_find_anon;
+ } else if (pcmk__str_any_of(option_name, "-B", "--ban", NULL)) {
+ SET_COMMAND(cmd_ban);
+ options.find_flags = pe_find_renamed|pe_find_anon;
+ } else if (pcmk__str_any_of(option_name, "-M", "--move", NULL)) {
+ SET_COMMAND(cmd_move);
+ options.find_flags = pe_find_renamed|pe_find_anon;
+ } else if (pcmk__str_any_of(option_name, "-q", "--query-xml", NULL)) {
+ SET_COMMAND(cmd_query_xml);
+ options.find_flags = pe_find_renamed|pe_find_any;
+ } else if (pcmk__str_any_of(option_name, "-w", "--query-xml-raw", NULL)) {
+ SET_COMMAND(cmd_query_raw_xml);
+ options.find_flags = pe_find_renamed|pe_find_any;
+ } else if (pcmk__str_any_of(option_name, "-W", "--locate", NULL)) {
+ SET_COMMAND(cmd_locate);
+ options.find_flags = pe_find_renamed|pe_find_anon;
+
+ } else if (pcmk__str_any_of(option_name, "-a", "--constraints", NULL)) {
+ SET_COMMAND(cmd_colocations);
+ options.find_flags = pe_find_renamed|pe_find_anon;
+
+ } else if (pcmk__str_any_of(option_name, "-A", "--stack", NULL)) {
+ SET_COMMAND(cmd_colocations);
+ options.find_flags = pe_find_renamed|pe_find_anon;
+ options.recursive = TRUE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+get_param_prop_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (pcmk__str_any_of(option_name, "-g", "--get-parameter", NULL)) {
+ SET_COMMAND(cmd_get_param);
+ } else {
+ SET_COMMAND(cmd_get_property);
+ }
+
+ pcmk__str_update(&options.prop_name, optarg);
+ options.find_flags = pe_find_renamed|pe_find_any;
+ return TRUE;
+}
+
+gboolean
+list_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (pcmk__str_any_of(option_name, "-c", "--list-cts", NULL)) {
+ SET_COMMAND(cmd_cts);
+ } else if (pcmk__str_any_of(option_name, "-L", "--list", NULL)) {
+ SET_COMMAND(cmd_list_resources);
+ } else if (pcmk__str_any_of(option_name, "-l", "--list-raw", NULL)) {
+ SET_COMMAND(cmd_list_instances);
+ } else if (pcmk__str_any_of(option_name, "-O", "--list-operations", NULL)) {
+ SET_COMMAND(cmd_list_active_ops);
+ } else {
+ SET_COMMAND(cmd_list_all_ops);
+ }
+
+ options.require_resource = FALSE;
+ return TRUE;
+}
+
+gboolean
+set_delete_param_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (pcmk__str_any_of(option_name, "-p", "--set-parameter", NULL)) {
+ SET_COMMAND(cmd_set_param);
+ } else {
+ SET_COMMAND(cmd_delete_param);
+ }
+
+ pcmk__str_update(&options.prop_name, optarg);
+ options.find_flags = pe_find_renamed|pe_find_any;
+ return TRUE;
+}
+
+gboolean
+set_prop_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ SET_COMMAND(cmd_set_property);
+ options.require_dataset = FALSE;
+ pcmk__str_update(&options.prop_name, optarg);
+ options.find_flags = pe_find_renamed|pe_find_any;
+ return TRUE;
+}
+
+gboolean
+timeout_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.timeout_ms = crm_get_msec(optarg);
+ return TRUE;
+}
+
+gboolean
+validate_or_force_cb(const gchar *option_name, const gchar *optarg,
+ gpointer data, GError **error)
+{
+ SET_COMMAND(cmd_execute_agent);
+ if (options.operation) {
+ g_free(options.operation);
+ }
+ options.operation = g_strdup(option_name + 2); // skip "--"
+ options.find_flags = pe_find_renamed|pe_find_anon;
+ if (options.override_params == NULL) {
+ options.override_params = pcmk__strkey_table(free, free);
+ }
+
+ if (optarg != NULL) {
+ if (pcmk__scan_min_int(optarg, &options.check_level, 0) != pcmk_rc_ok) {
+ g_set_error(error, G_OPTION_ERROR, CRM_EX_INVALID_PARAM,
+ _("Invalid check level setting: %s"), optarg);
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+restart_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **error)
+{
+ SET_COMMAND(cmd_restart);
+ options.find_flags = pe_find_renamed|pe_find_anon;
+ return TRUE;
+}
+
+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 = pcmk__strkey_table(free, free);
+ }
+ 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;
+ options.require_dataset = FALSE;
+ return TRUE;
+}
+
+gboolean
+why_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ SET_COMMAND(cmd_why);
+ options.require_resource = FALSE;
+ options.find_flags = pe_find_renamed|pe_find_anon;
+ return TRUE;
+}
+
+static int
+ban_or_move(pcmk__output_t *out, pe_resource_t *rsc, const char *move_lifetime)
+{
+ int rc = pcmk_rc_ok;
+ pe_node_t *current = NULL;
+ unsigned int nactive = 0;
+
+ CRM_CHECK(rsc != NULL, return EINVAL);
+
+ current = pe__find_active_requires(rsc, &nactive);
+
+ if (nactive == 1) {
+ rc = cli_resource_ban(out, options.rsc_id, current->details->uname, move_lifetime, NULL,
+ cib_conn, options.cib_options, options.promoted_role_only);
+
+ } else if (pcmk_is_set(rsc->flags, pe_rsc_promotable)) {
+ int count = 0;
+ GList *iter = NULL;
+
+ current = NULL;
+ for(iter = rsc->children; iter; iter = iter->next) {
+ pe_resource_t *child = (pe_resource_t *)iter->data;
+ enum rsc_role_e child_role = child->fns->state(child, TRUE);
+
+ if (child_role == RSC_ROLE_PROMOTED) {
+ count++;
+ current = pe__current_node(child);
+ }
+ }
+
+ if(count == 1 && current) {
+ rc = cli_resource_ban(out, options.rsc_id, current->details->uname, move_lifetime, NULL,
+ cib_conn, options.cib_options, options.promoted_role_only);
+
+ } else {
+ rc = EINVAL;
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ _("Resource '%s' not moved: active in %d locations (promoted in %d).\n"
+ "To prevent '%s' from running on a specific location, "
+ "specify a node."
+ "To prevent '%s' from being promoted at a specific "
+ "location, specify a node and the --promoted option."),
+ options.rsc_id, nactive, count, options.rsc_id, options.rsc_id);
+ }
+
+ } else {
+ rc = EINVAL;
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ _("Resource '%s' not moved: active in %d locations.\n"
+ "To prevent '%s' from running on a specific location, "
+ "specify a node."),
+ options.rsc_id, nactive, options.rsc_id);
+ }
+
+ return rc;
+}
+
+static void
+cleanup(pcmk__output_t *out, pe_resource_t *rsc, pe_node_t *node)
+{
+ int rc = pcmk_rc_ok;
+
+ if (options.force == FALSE) {
+ rsc = uber_parent(rsc);
+ }
+
+ crm_debug("Erasing failures of %s (%s requested) on %s",
+ rsc->id, options.rsc_id, (options.host_uname? options.host_uname: "all nodes"));
+ rc = cli_resource_delete(controld_api, options.host_uname, rsc, options.operation,
+ options.interval_spec, TRUE, data_set, options.force);
+
+ if ((rc == pcmk_rc_ok) && !out->is_quiet(out)) {
+ // Show any reasons why resource might stay stopped
+ cli_resource_check(out, rsc, node);
+ }
+
+ if (rc == pcmk_rc_ok) {
+ start_mainloop(controld_api);
+ }
+}
+
+static int
+clear_constraints(pcmk__output_t *out, xmlNodePtr *cib_xml_copy)
+{
+ GList *before = NULL;
+ GList *after = NULL;
+ GList *remaining = NULL;
+ GList *ele = NULL;
+ pe_node_t *dest = NULL;
+ int rc = pcmk_rc_ok;
+
+ if (!out->is_quiet(out)) {
+ before = build_constraint_list(data_set->input);
+ }
+
+ if (options.clear_expired) {
+ rc = cli_resource_clear_all_expired(data_set->input, cib_conn, options.cib_options,
+ options.rsc_id, options.host_uname,
+ options.promoted_role_only);
+
+ } else if (options.host_uname) {
+ dest = pe_find_node(data_set->nodes, options.host_uname);
+ if (dest == NULL) {
+ rc = pcmk_rc_node_unknown;
+ if (!out->is_quiet(out)) {
+ g_list_free(before);
+ }
+ return rc;
+ }
+ rc = cli_resource_clear(options.rsc_id, dest->details->uname, NULL,
+ cib_conn, options.cib_options, TRUE, options.force);
+
+ } else {
+ rc = cli_resource_clear(options.rsc_id, NULL, data_set->nodes,
+ cib_conn, options.cib_options, TRUE, options.force);
+ }
+
+ if (!out->is_quiet(out)) {
+ rc = cib_conn->cmds->query(cib_conn, NULL, cib_xml_copy, cib_scope_local | cib_sync_call);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ _("Could not get modified CIB: %s\n"), pcmk_strerror(rc));
+ g_list_free(before);
+ free_xml(*cib_xml_copy);
+ *cib_xml_copy = NULL;
+ return rc;
+ }
+
+ data_set->input = *cib_xml_copy;
+ cluster_status(data_set);
+
+ after = build_constraint_list(data_set->input);
+ remaining = pcmk__subtract_lists(before, after, (GCompareFunc) strcmp);
+
+ for (ele = remaining; ele != NULL; ele = ele->next) {
+ out->info(out, "Removing constraint: %s", (char *) ele->data);
+ }
+
+ g_list_free(before);
+ g_list_free(after);
+ g_list_free(remaining);
+ }
+
+ return rc;
+}
+
+static int
+delete(void)
+{
+ int rc = pcmk_rc_ok;
+ xmlNode *msg_data = NULL;
+
+ if (options.rsc_type == NULL) {
+ rc = ENXIO;
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ _("You need to specify a resource type with -t"));
+ return rc;
+ }
+
+ msg_data = create_xml_node(NULL, options.rsc_type);
+ crm_xml_add(msg_data, XML_ATTR_ID, options.rsc_id);
+
+ rc = cib_conn->cmds->remove(cib_conn, XML_CIB_TAG_RESOURCES, msg_data,
+ options.cib_options);
+ rc = pcmk_legacy2rc(rc);
+ free_xml(msg_data);
+ return rc;
+}
+
+static int
+list_agents(pcmk__output_t *out, const char *agent_spec)
+{
+ int rc = pcmk_rc_ok;
+ char *provider = strchr(agent_spec, ':');
+ lrmd_t *lrmd_conn = NULL;
+ lrmd_list_t *list = NULL;
+
+ rc = lrmd__new(&lrmd_conn, NULL, NULL, 0);
+ if (rc != pcmk_rc_ok) {
+ goto error;
+ }
+
+ if (provider) {
+ *provider++ = 0;
+ }
+
+ rc = lrmd_conn->cmds->list_agents(lrmd_conn, &list, agent_spec, provider);
+
+ if (rc > 0) {
+ rc = out->message(out, "agents-list", list, agent_spec, provider);
+ } else {
+ rc = pcmk_rc_error;
+ }
+
+error:
+ if (rc != pcmk_rc_ok) {
+ if (provider == NULL) {
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ _("No agents found for standard '%s'"), agent_spec);
+ } else {
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ _("No agents found for standard '%s' and provider '%s'"),
+ agent_spec, provider);
+ }
+ }
+
+ lrmd_api_delete(lrmd_conn);
+ return rc;
+}
+
+static int
+list_providers(pcmk__output_t *out, const char *agent_spec)
+{
+ int rc;
+ const char *text = NULL;
+ lrmd_t *lrmd_conn = NULL;
+ lrmd_list_t *list = NULL;
+
+ rc = lrmd__new(&lrmd_conn, NULL, NULL, 0);
+ if (rc != pcmk_rc_ok) {
+ goto error;
+ }
+
+ switch (options.rsc_cmd) {
+ case cmd_list_alternatives:
+ rc = lrmd_conn->cmds->list_ocf_providers(lrmd_conn, agent_spec, &list);
+
+ if (rc > 0) {
+ rc = out->message(out, "alternatives-list", list, agent_spec);
+ } else {
+ rc = pcmk_rc_error;
+ }
+
+ text = "OCF providers";
+ break;
+ case cmd_list_standards:
+ rc = lrmd_conn->cmds->list_standards(lrmd_conn, &list);
+
+ if (rc > 0) {
+ rc = out->message(out, "standards-list", list);
+ } else {
+ rc = pcmk_rc_error;
+ }
+
+ text = "standards";
+ break;
+ case cmd_list_providers:
+ rc = lrmd_conn->cmds->list_ocf_providers(lrmd_conn, agent_spec, &list);
+
+ if (rc > 0) {
+ rc = out->message(out, "providers-list", list, agent_spec);
+ } else {
+ rc = pcmk_rc_error;
+ }
+
+ text = "OCF providers";
+ break;
+ default:
+ g_set_error(&error, PCMK__RC_ERROR, pcmk_rc_error, "Bug");
+ lrmd_api_delete(lrmd_conn);
+ return pcmk_rc_error;
+ }
+
+error:
+ if (rc != pcmk_rc_ok) {
+ if (agent_spec != NULL) {
+ rc = ENXIO;
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ _("No %s found for %s"), text, agent_spec);
+
+ } else {
+ rc = ENXIO;
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ _("No %s found"), text);
+ }
+ }
+
+ lrmd_api_delete(lrmd_conn);
+ return rc;
+}
+
+static int
+populate_working_set(xmlNodePtr *cib_xml_copy)
+{
+ int rc = pcmk_rc_ok;
+
+ if (options.xml_file != NULL) {
+ *cib_xml_copy = filename2xml(options.xml_file);
+ if (*cib_xml_copy == NULL) {
+ rc = pcmk_rc_cib_corrupt;
+ }
+ } else {
+ rc = cib_conn->cmds->query(cib_conn, NULL, cib_xml_copy, cib_scope_local | cib_sync_call);
+ rc = pcmk_legacy2rc(rc);
+ }
+
+ if (rc == pcmk_rc_ok) {
+ data_set = pe_new_working_set();
+ if (data_set == NULL) {
+ rc = ENOMEM;
+ } else {
+ pe__set_working_set_flags(data_set,
+ pe_flag_no_counts|pe_flag_no_compat);
+ data_set->priv = out;
+ rc = update_working_set_xml(data_set, cib_xml_copy);
+ }
+ }
+
+ if (rc != pcmk_rc_ok) {
+ free_xml(*cib_xml_copy);
+ *cib_xml_copy = NULL;
+ return rc;
+ }
+
+ cluster_status(data_set);
+ return pcmk_rc_ok;
+}
+
+static int
+refresh(pcmk__output_t *out)
+{
+ int rc = pcmk_rc_ok;
+ const char *router_node = options.host_uname;
+ int attr_options = pcmk__node_attr_none;
+
+ if (options.host_uname) {
+ pe_node_t *node = pe_find_node(data_set->nodes, options.host_uname);
+
+ if (pe__is_guest_or_remote_node(node)) {
+ node = pe__current_node(node->details->remote_rsc);
+ if (node == NULL) {
+ rc = ENXIO;
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ _("No cluster connection to Pacemaker Remote node %s detected"),
+ options.host_uname);
+ return rc;
+ }
+ router_node = node->details->uname;
+ attr_options |= pcmk__node_attr_remote;
+ }
+ }
+
+ if (controld_api == NULL) {
+ out->info(out, "Dry run: skipping clean-up of %s due to CIB_file",
+ options.host_uname? options.host_uname : "all nodes");
+ rc = pcmk_rc_ok;
+ return rc;
+ }
+
+ crm_debug("Re-checking the state of all resources on %s", options.host_uname?options.host_uname:"all nodes");
+
+ rc = pcmk__attrd_api_clear_failures(NULL, options.host_uname, NULL,
+ NULL, NULL, NULL, attr_options);
+
+ if (pcmk_controld_api_reprobe(controld_api, options.host_uname,
+ router_node) == pcmk_rc_ok) {
+ start_mainloop(controld_api);
+ }
+
+ return rc;
+}
+
+static void
+refresh_resource(pcmk__output_t *out, pe_resource_t *rsc, pe_node_t *node)
+{
+ int rc = pcmk_rc_ok;
+
+ if (options.force == FALSE) {
+ rsc = uber_parent(rsc);
+ }
+
+ crm_debug("Re-checking the state of %s (%s requested) on %s",
+ rsc->id, options.rsc_id, (options.host_uname? options.host_uname: "all nodes"));
+ rc = cli_resource_delete(controld_api, options.host_uname, rsc, NULL, 0,
+ FALSE, data_set, options.force);
+
+ if ((rc == pcmk_rc_ok) && !out->is_quiet(out)) {
+ // Show any reasons why resource might stay stopped
+ cli_resource_check(out, rsc, node);
+ }
+
+ if (rc == pcmk_rc_ok) {
+ start_mainloop(controld_api);
+ }
+}
+
+static int
+set_property(void)
+{
+ int rc = pcmk_rc_ok;
+ xmlNode *msg_data = NULL;
+
+ if (pcmk__str_empty(options.rsc_type)) {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ _("Must specify -t with resource type"));
+ rc = ENXIO;
+ return rc;
+
+ } else if (pcmk__str_empty(options.prop_value)) {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ _("Must supply -v with new value"));
+ rc = ENXIO;
+ return rc;
+ }
+
+ CRM_LOG_ASSERT(options.prop_name != NULL);
+
+ msg_data = create_xml_node(NULL, options.rsc_type);
+ crm_xml_add(msg_data, XML_ATTR_ID, options.rsc_id);
+ crm_xml_add(msg_data, options.prop_name, options.prop_value);
+
+ rc = cib_conn->cmds->modify(cib_conn, XML_CIB_TAG_RESOURCES, msg_data,
+ options.cib_options);
+ rc = pcmk_legacy2rc(rc);
+ free_xml(msg_data);
+
+ return rc;
+}
+
+static int
+show_metadata(pcmk__output_t *out, const char *agent_spec)
+{
+ int rc = pcmk_rc_ok;
+ char *standard = NULL;
+ char *provider = NULL;
+ char *type = NULL;
+ char *metadata = NULL;
+ lrmd_t *lrmd_conn = NULL;
+
+ rc = lrmd__new(&lrmd_conn, NULL, NULL, 0);
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ _("Could not create executor connection"));
+ lrmd_api_delete(lrmd_conn);
+ return rc;
+ }
+
+ rc = crm_parse_agent_spec(agent_spec, &standard, &provider, &type);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc == pcmk_rc_ok) {
+ rc = lrmd_conn->cmds->get_metadata(lrmd_conn, standard,
+ provider, type,
+ &metadata, 0);
+ rc = pcmk_legacy2rc(rc);
+
+ if (metadata) {
+ out->output_xml(out, "metadata", metadata);
+ free(metadata);
+ } else {
+ /* We were given a validly formatted spec, but it doesn't necessarily
+ * match up with anything that exists. Use ENXIO as the return code
+ * here because that maps to an exit code of CRM_EX_NOSUCH, which
+ * probably is the most common reason to get here.
+ */
+ rc = ENXIO;
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ _("Metadata query for %s failed: %s"),
+ agent_spec, pcmk_rc_str(rc));
+ }
+ } else {
+ rc = ENXIO;
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ _("'%s' is not a valid agent specification"), agent_spec);
+ }
+
+ lrmd_api_delete(lrmd_conn);
+ return rc;
+}
+
+static void
+validate_cmdline_config(void)
+{
+ // 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"));
+
+ // 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 can only be used with "
+ "--validate and --force-*"));
+
+ // 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
+ // is valid.
+ } else if (pcmk__str_eq(options.v_class, "stonith", pcmk__str_none)) {
+ if (options.v_provider != NULL) {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ _("stonith does not support providers"));
+
+ } else if (stonith_agent_exists(options.v_agent, 0) == FALSE) {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ _("%s is not a known stonith agent"), options.v_agent ? options.v_agent : "");
+ }
+
+ } else if (resources_agent_exists(options.v_class, options.v_provider, options.v_agent) == FALSE) {
+ g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
+ _("%s:%s:%s is not a known resource"),
+ options.v_class ? options.v_class : "",
+ options.v_provider ? options.v_provider : "",
+ options.v_agent ? options.v_agent : "");
+ }
+
+ if (error != NULL) {
+ return;
+ }
+
+ if (options.cmdline_params == NULL) {
+ options.cmdline_params = pcmk__strkey_table(free, free);
+ }
+ options.require_resource = FALSE;
+ options.require_dataset = FALSE;
+ options.require_cib = FALSE;
+}
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
+ GOptionContext *context = NULL;
+
+ GOptionEntry extra_prog_entries[] = {
+ { "quiet", 'Q', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &(args->quiet),
+ "Be less descriptive in output.",
+ NULL },
+ { "resource", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.rsc_id,
+ "Resource ID",
+ "ID" },
+ { G_OPTION_REMAINING, 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING_ARRAY, &options.remainder,
+ NULL,
+ NULL },
+
+ { NULL }
+ };
+
+ const char *description = "Examples:\n\n"
+ "List the available OCF agents:\n\n"
+ "\t# crm_resource --list-agents ocf\n\n"
+ "List the available OCF agents from the linux-ha project:\n\n"
+ "\t# crm_resource --list-agents ocf:heartbeat\n\n"
+ "Move 'myResource' to a specific node:\n\n"
+ "\t# crm_resource --resource myResource --move --node altNode\n\n"
+ "Allow (but not force) 'myResource' to move back to its original "
+ "location:\n\n"
+ "\t# crm_resource --resource myResource --clear\n\n"
+ "Stop 'myResource' (and anything that depends on it):\n\n"
+ "\t# crm_resource --resource myResource --set-parameter target-role "
+ "--meta --parameter-value Stopped\n\n"
+ "Tell the cluster not to manage 'myResource' (the cluster will not "
+ "attempt to start or stop the\n"
+ "resource under any circumstances; useful when performing maintenance "
+ "tasks on a resource):\n\n"
+ "\t# crm_resource --resource myResource --set-parameter is-managed "
+ "--meta --parameter-value false\n\n"
+ "Erase the operation history of 'myResource' on 'aNode' (the cluster "
+ "will 'forget' the existing\n"
+ "resource state, including any errors, and attempt to recover the"
+ "resource; useful when a resource\n"
+ "had failed permanently and has been repaired by an administrator):\n\n"
+ "\t# crm_resource --resource myResource --cleanup --node aNode\n\n";
+
+ context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
+ g_option_context_set_description(context, description);
+
+ /* Add the -Q option, which cannot be part of the globally supported options
+ * because some tools use that flag for something else.
+ */
+ pcmk__add_main_args(context, extra_prog_entries);
+
+ pcmk__add_arg_group(context, "queries", "Queries:",
+ "Show query help", query_entries);
+ pcmk__add_arg_group(context, "commands", "Commands:",
+ "Show command help", command_entries);
+ pcmk__add_arg_group(context, "locations", "Locations:",
+ "Show location help", location_entries);
+ pcmk__add_arg_group(context, "advanced", "Advanced:",
+ "Show advanced option help", advanced_entries);
+ pcmk__add_arg_group(context, "additional", "Additional Options:",
+ "Show additional options", addl_entries);
+ return context;
+}
+
+int
+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;
+
+ GOptionGroup *output_group = NULL;
+ gchar **processed_args = NULL;
+ GOptionContext *context = NULL;
+
+ /*
+ * Parse command line arguments
+ */
+
+ args = pcmk__new_common_args(SUMMARY);
+ processed_args = pcmk__cmdline_preproc(argv, "GHINSTdginpstuvx");
+ context = build_arg_context(args, &output_group);
+
+ pcmk__register_formats(output_group, formats);
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("crm_resource", args->verbosity);
+
+ rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
+ if (rc != pcmk_rc_ok) {
+ exit_code = CRM_EX_ERROR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Error creating output format %s: %s"),
+ args->output_ty, pcmk_rc_str(rc));
+ goto done;
+ }
+
+ pe__register_messages(out);
+ crm_resource_register_messages(out);
+ lrmd__register_messages(out);
+ pcmk__register_lib_messages(out);
+
+ out->quiet = args->quiet;
+
+ crm_log_args(argc, argv);
+
+ /*
+ * Validate option combinations
+ */
+
+ // If the user didn't explicitly specify a command, list resources
+ if (options.rsc_cmd == cmd_none) {
+ options.rsc_cmd = cmd_list_resources;
+ options.require_resource = FALSE;
+ }
+
+ // --expired without --clear/-U doesn't make sense
+ if (options.clear_expired && (options.rsc_cmd != cmd_clear)) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("--expired requires --clear or -U"));
+ goto done;
+ }
+
+ if ((options.remainder != NULL) && (options.override_params != NULL)) {
+ // Commands that use positional arguments will create override_params
+ for (gchar **s = options.remainder; *s; s++) {
+ char *name = calloc(1, strlen(*s));
+ char *value = calloc(1, strlen(*s));
+ int rc = sscanf(*s, "%[^=]=%s", name, value);
+
+ if (rc == 2) {
+ g_hash_table_replace(options.override_params, name, value);
+
+ } else {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ _("Error parsing '%s' as a name=value pair"),
+ argv[optind]);
+ free(value);
+ free(name);
+ goto done;
+ }
+ }
+
+ } else if (options.remainder != NULL) {
+ gchar **strv = NULL;
+ gchar *msg = NULL;
+ int i = 1;
+ int len = 0;
+
+ for (gchar **s = options.remainder; *s; s++) {
+ len++;
+ }
+
+ CRM_ASSERT(len > 0);
+
+ /* Add 1 for the strv[0] string below, and add another 1 for the NULL
+ * at the end of the array so g_strjoinv knows when to stop.
+ */
+ strv = calloc(len+2, sizeof(char *));
+ strv[0] = strdup("non-option ARGV-elements:\n");
+
+ for (gchar **s = options.remainder; *s; s++) {
+ strv[i] = crm_strdup_printf("[%d of %d] %s\n", i, len, *s);
+ i++;
+ }
+
+ strv[i] = NULL;
+
+ exit_code = CRM_EX_USAGE;
+ msg = g_strjoinv("", strv);
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "%s", msg);
+ g_free(msg);
+
+ /* Don't try to free the last element, which is just NULL. */
+ for(i = 0; i < len+1; i++) {
+ free(strv[i]);
+ }
+ free(strv);
+
+ goto done;
+ }
+
+ if (pcmk__str_eq(args->output_ty, "xml", pcmk__str_none)) {
+ /* Kind of a hack to display XML lists using a real tag instead of <list>. This just
+ * saves from having to write custom messages to build the lists around all these things
+ */
+ switch (options.rsc_cmd) {
+ case cmd_execute_agent:
+ case cmd_list_resources:
+ case cmd_query_xml:
+ case cmd_query_raw_xml:
+ case cmd_list_active_ops:
+ case cmd_list_all_ops:
+ case cmd_colocations:
+ pcmk__force_args(context, &error, "%s --xml-simple-list --xml-substitute", g_get_prgname());
+ break;
+
+ default:
+ pcmk__force_args(context, &error, "%s --xml-substitute", g_get_prgname());
+ break;
+ }
+ } else if (pcmk__str_eq(args->output_ty, "text", pcmk__str_null_matches)) {
+ if ((options.rsc_cmd == cmd_colocations) ||
+ options.rsc_cmd == cmd_list_resources) {
+ pcmk__force_args(context, &error, "%s --text-fancy", g_get_prgname());
+ }
+ }
+
+ if (args->version) {
+ out->version(out, false);
+ goto done;
+ }
+
+ 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();
+ 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 (options.require_resource && (options.rsc_id == NULL)) {
+ 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)) {
+ 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.find_flags && options.rsc_id) {
+ options.require_dataset = TRUE;
+ }
+
+ // Establish a connection to the CIB if needed
+ if (options.require_cib) {
+ cib_conn = cib_new();
+ if ((cib_conn == NULL) || (cib_conn->cmds == NULL)) {
+ exit_code = CRM_EX_DISCONNECT;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ _("Could not create CIB connection"));
+ goto done;
+ }
+ rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command);
+ rc = pcmk_legacy2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ _("Could not connect to the CIB: %s"), pcmk_rc_str(rc));
+ goto done;
+ }
+ }
+
+ /* Populate working set from XML file if specified or CIB query otherwise */
+ if (options.require_dataset) {
+ rc = populate_working_set(&cib_xml_copy);
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ goto done;
+ }
+ }
+
+ // If command requires that resource exist if specified, find it
+ if (options.find_flags && options.rsc_id) {
+ rsc = pe_find_resource_with_flags(data_set->resources, options.rsc_id,
+ options.find_flags);
+ if (rsc == NULL) {
+ exit_code = CRM_EX_NOSUCH;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ _("Resource '%s' not found"), options.rsc_id);
+ goto done;
+ }
+
+ /* The --ban, --clear, --move, and --restart commands do not work with
+ * instances of clone resourcs.
+ */
+ if (strchr(options.rsc_id, ':') != NULL && pe_rsc_is_clone(rsc->parent) &&
+ (options.rsc_cmd == cmd_ban || options.rsc_cmd == cmd_clear ||
+ options.rsc_cmd == cmd_move || options.rsc_cmd == cmd_restart)) {
+ exit_code = CRM_EX_INVALID_PARAM;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ _("Cannot operate on clone resource instance '%s'"), options.rsc_id);
+ goto done;
+ }
+ }
+
+ // 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);
+
+ if (node == NULL) {
+ exit_code = CRM_EX_NOSUCH;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ _("Node '%s' not found"), options.host_uname);
+ goto done;
+ }
+ }
+
+ // Establish a connection to the controller if needed
+ if (options.require_crmd) {
+ rc = pcmk_new_ipc_api(&controld_api, pcmk_ipc_controld);
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ _("Error connecting to the controller: %s"), pcmk_rc_str(rc));
+ goto done;
+ }
+ pcmk_register_ipc_callback(controld_api, controller_event_callback,
+ NULL);
+ rc = pcmk_connect_ipc(controld_api, pcmk_ipc_dispatch_main);
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ _("Error connecting to the controller: %s"), pcmk_rc_str(rc));
+ goto done;
+ }
+ }
+
+ /*
+ * Handle requested command
+ */
+
+ switch (options.rsc_cmd) {
+ case cmd_list_resources: {
+ GList *all = NULL;
+ all = g_list_prepend(all, (gpointer) "*");
+ rc = out->message(out, "resource-list", data_set,
+ pcmk_show_inactive_rscs | pcmk_show_rsc_only | pcmk_show_pending,
+ true, all, all, false);
+ g_list_free(all);
+
+ if (rc == pcmk_rc_no_output) {
+ rc = ENXIO;
+ }
+ break;
+ }
+
+ case cmd_list_instances:
+ rc = out->message(out, "resource-names-list", data_set->resources);
+
+ if (rc != pcmk_rc_ok) {
+ rc = ENXIO;
+ }
+
+ break;
+
+ case cmd_list_standards:
+ case cmd_list_providers:
+ case cmd_list_alternatives:
+ rc = list_providers(out, options.agent_spec);
+ break;
+
+ case cmd_list_agents:
+ rc = list_agents(out, options.agent_spec);
+ break;
+
+ case cmd_metadata:
+ rc = show_metadata(out, options.agent_spec);
+ break;
+
+ case cmd_restart:
+ /* We don't pass data_set because rsc needs to stay valid for the
+ * entire lifetime of cli_resource_restart(), but it will reset and
+ * update the working set multiple times, so it needs to use its own
+ * copy.
+ */
+ rc = cli_resource_restart(out, rsc, node, options.move_lifetime,
+ options.timeout_ms, cib_conn,
+ options.cib_options, options.promoted_role_only,
+ options.force);
+ break;
+
+ case cmd_wait:
+ rc = wait_till_stable(out, options.timeout_ms, cib_conn);
+ break;
+
+ case cmd_execute_agent:
+ if (options.cmdline_config) {
+ exit_code = cli_resource_execute_from_params(out, NULL,
+ options.v_class, options.v_provider, options.v_agent,
+ options.operation, options.cmdline_params,
+ options.override_params, options.timeout_ms,
+ args->verbosity, options.force, options.check_level);
+ } else {
+ exit_code = cli_resource_execute(rsc, options.rsc_id,
+ options.operation, options.override_params,
+ options.timeout_ms, cib_conn, data_set,
+ args->verbosity, options.force, options.check_level);
+ }
+ goto done;
+
+ 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);
+ }
+ break;
+
+ case cmd_colocations:
+ rc = out->message(out, "locations-and-colocations", rsc, data_set,
+ options.recursive, (bool) options.force);
+ break;
+
+ case cmd_cts:
+ rc = pcmk_rc_ok;
+ g_list_foreach(data_set->resources, (GFunc) cli_resource_print_cts, out);
+ cli_resource_print_cts_constraints(data_set);
+ break;
+
+ case cmd_fail:
+ rc = cli_resource_fail(controld_api, options.host_uname,
+ options.rsc_id, data_set);
+ if (rc == pcmk_rc_ok) {
+ start_mainloop(controld_api);
+ }
+ break;
+
+ case cmd_list_active_ops:
+ rc = cli_resource_print_operations(options.rsc_id,
+ options.host_uname, TRUE,
+ data_set);
+ break;
+
+ case cmd_list_all_ops:
+ rc = cli_resource_print_operations(options.rsc_id,
+ options.host_uname, FALSE,
+ data_set);
+ break;
+
+ case cmd_locate: {
+ GList *nodes = cli_resource_search(rsc, options.rsc_id, data_set);
+ rc = out->message(out, "resource-search-list", nodes, options.rsc_id);
+ g_list_free_full(nodes, free);
+ break;
+ }
+
+ case cmd_query_xml:
+ rc = cli_resource_print(rsc, data_set, true);
+ break;
+
+ case cmd_query_raw_xml:
+ rc = cli_resource_print(rsc, data_set, false);
+ break;
+
+ case cmd_why:
+ if ((options.host_uname != NULL) && (node == NULL)) {
+ rc = pcmk_rc_node_unknown;
+ } else {
+ rc = out->message(out, "resource-reasons-list",
+ data_set->resources, rsc, node);
+ }
+ break;
+
+ case cmd_clear:
+ rc = clear_constraints(out, &cib_xml_copy);
+ break;
+
+ case cmd_move:
+ if (options.host_uname == NULL) {
+ rc = ban_or_move(out, rsc, options.move_lifetime);
+ } else {
+ rc = cli_resource_move(rsc, options.rsc_id, options.host_uname,
+ options.move_lifetime, cib_conn,
+ options.cib_options, data_set,
+ options.promoted_role_only,
+ options.force);
+ }
+
+ if (rc == EINVAL) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ break;
+
+ case cmd_ban:
+ if (options.host_uname == NULL) {
+ rc = ban_or_move(out, rsc, options.move_lifetime);
+ } else if (node == NULL) {
+ rc = pcmk_rc_node_unknown;
+ } else {
+ rc = cli_resource_ban(out, options.rsc_id, node->details->uname,
+ options.move_lifetime, NULL, cib_conn,
+ options.cib_options,
+ options.promoted_role_only);
+ }
+
+ if (rc == EINVAL) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ break;
+
+ case cmd_get_property:
+ rc = out->message(out, "property-list", rsc, options.prop_name);
+ if (rc == pcmk_rc_no_output) {
+ rc = ENXIO;
+ }
+
+ break;
+
+ case cmd_set_property:
+ rc = set_property();
+ break;
+
+ case cmd_get_param: {
+ unsigned int count = 0;
+ GHashTable *params = NULL;
+ pe_node_t *current = rsc->fns->active_node(rsc, &count, NULL);
+ bool free_params = true;
+ const char* value = NULL;
+
+ if (count > 1) {
+ out->err(out, "%s is active on more than one node,"
+ " returning the default value for %s", rsc->id,
+ pcmk__s(options.prop_name, "unspecified property"));
+ current = NULL;
+ }
+
+ crm_debug("Looking up %s in %s", options.prop_name, rsc->id);
+
+ if (pcmk__str_eq(options.attr_set_type, XML_TAG_ATTR_SETS, pcmk__str_none)) {
+ params = pe_rsc_params(rsc, current, data_set);
+ free_params = false;
+
+ value = g_hash_table_lookup(params, options.prop_name);
+
+ } else if (pcmk__str_eq(options.attr_set_type, XML_TAG_META_SETS, pcmk__str_none)) {
+ params = pcmk__strkey_table(free, free);
+ get_meta_attributes(params, rsc, current, data_set);
+
+ value = g_hash_table_lookup(params, options.prop_name);
+
+ } else if (pcmk__str_eq(options.attr_set_type, ATTR_SET_ELEMENT, pcmk__str_none)) {
+
+ value = crm_element_value(rsc->xml, options.prop_name);
+ free_params = false;
+
+ } else {
+ params = pcmk__strkey_table(free, free);
+ pe__unpack_dataset_nvpairs(rsc->xml, XML_TAG_UTILIZATION, NULL, params,
+ NULL, FALSE, data_set);
+
+ value = g_hash_table_lookup(params, options.prop_name);
+ }
+
+ rc = out->message(out, "attribute-list", rsc, options.prop_name, value);
+ if (free_params) {
+ g_hash_table_destroy(params);
+ }
+
+ break;
+ }
+
+ case cmd_set_param:
+ if (pcmk__str_empty(options.prop_value)) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ _("You need to supply a value with the -v option"));
+ goto done;
+ }
+
+ /* coverity[var_deref_model] False positive */
+ rc = cli_resource_update_attribute(rsc, options.rsc_id,
+ options.prop_set,
+ options.attr_set_type,
+ options.prop_id,
+ options.prop_name,
+ options.prop_value,
+ options.recursive, cib_conn,
+ options.cib_options,
+ options.force);
+ break;
+
+ case cmd_delete_param:
+ /* coverity[var_deref_model] False positive */
+ rc = cli_resource_delete_attribute(rsc, options.rsc_id,
+ options.prop_set,
+ options.attr_set_type,
+ options.prop_id,
+ options.prop_name, cib_conn,
+ options.cib_options,
+ options.force);
+ break;
+
+ case cmd_cleanup:
+ if (rsc == NULL) {
+ rc = cli_cleanup_all(controld_api, options.host_uname,
+ options.operation, options.interval_spec,
+ data_set);
+ if (rc == pcmk_rc_ok) {
+ start_mainloop(controld_api);
+ }
+ } else {
+ cleanup(out, rsc, node);
+ }
+ break;
+
+ case cmd_refresh:
+ if (rsc == NULL) {
+ rc = refresh(out);
+ } else {
+ refresh_resource(out, rsc, node);
+ }
+ break;
+
+ case cmd_delete:
+ rc = delete();
+ break;
+
+ default:
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ _("Unimplemented command: %d"), (int) options.rsc_cmd);
+ goto done;
+ }
+
+ /* Convert rc into an exit code. */
+ if (rc != pcmk_rc_ok && rc != pcmk_rc_no_output) {
+ exit_code = pcmk_rc2exitc(rc);
+ }
+
+ /*
+ * Clean up and exit
+ */
+
+done:
+ /* When we get here, exit_code has been set one of two ways - either at one of
+ * the spots where there's a "goto done" (which itself could have happened either
+ * directly or by calling pcmk_rc2exitc), or just up above after any of the break
+ * statements.
+ *
+ * Thus, we can use just exit_code here to decide what to do.
+ */
+ if (exit_code != CRM_EX_OK && exit_code != CRM_EX_USAGE) {
+ if (error != NULL) {
+ char *msg = crm_strdup_printf("%s\nError performing operation: %s",
+ error->message, crm_exit_str(exit_code));
+ g_clear_error(&error);
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "%s", msg);
+ free(msg);
+ } else {
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ _("Error performing operation: %s"), crm_exit_str(exit_code));
+ }
+ }
+
+ g_free(options.host_uname);
+ g_free(options.interval_spec);
+ g_free(options.move_lifetime);
+ g_free(options.operation);
+ g_free(options.prop_id);
+ free(options.prop_name);
+ g_free(options.prop_set);
+ g_free(options.prop_value);
+ g_free(options.rsc_id);
+ g_free(options.rsc_type);
+ free(options.agent_spec);
+ free(options.v_agent);
+ free(options.v_class);
+ free(options.v_provider);
+ g_free(options.xml_file);
+ g_strfreev(options.remainder);
+
+ if (options.override_params != NULL) {
+ g_hash_table_destroy(options.override_params);
+ }
+
+ /* options.cmdline_params does not need to be destroyed here. See the
+ * comments in cli_resource_execute_from_params.
+ */
+
+ g_strfreev(processed_args);
+ g_option_context_free(context);
+
+ return bye(exit_code);
+}
diff --git a/tools/crm_resource.h b/tools/crm_resource.h
new file mode 100644
index 0000000..dcd6c3d
--- /dev/null
+++ b/tools/crm_resource.h
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2004-2023 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 <stdint.h>
+#include <stdbool.h>
+
+#include <crm/crm.h>
+
+#include <crm/msg_xml.h>
+#include <crm/services.h>
+#include <crm/common/xml.h>
+#include <crm/common/mainloop.h>
+#include <crm/common/output_internal.h>
+
+#include <crm/cib.h>
+#include <crm/common/attrd_internal.h>
+#include <crm/pengine/rules.h>
+#include <crm/pengine/status.h>
+#include <crm/pengine/internal.h>
+#include <pacemaker-internal.h>
+
+#define ATTR_SET_ELEMENT "attr_set_element"
+
+typedef struct node_info_s {
+ const char *node_name;
+ bool promoted;
+} node_info_t;
+
+enum resource_check_flags {
+ rsc_remain_stopped = (1 << 0),
+ rsc_unpromotable = (1 << 1),
+ rsc_unmanaged = (1 << 2),
+ rsc_locked = (1 << 3),
+ rsc_node_health = (1 << 4),
+};
+
+typedef struct resource_checks_s {
+ pe_resource_t *rsc; // Resource being checked
+ uint32_t flags; // Group of enum resource_check_flags
+ const char *lock_node; // Node that resource is shutdown-locked to, if any
+} resource_checks_t;
+
+resource_checks_t *cli_check_resource(pe_resource_t *rsc, char *role_s, char *managed);
+
+/* ban */
+int cli_resource_prefer(pcmk__output_t *out, const char *rsc_id, const char *host,
+ const char *move_lifetime, cib_t * cib_conn, int cib_options,
+ gboolean promoted_role_only);
+int cli_resource_ban(pcmk__output_t *out, const char *rsc_id, const char *host,
+ const char *move_lifetime, GList *allnodes, cib_t * cib_conn,
+ int cib_options, gboolean promoted_role_only);
+int cli_resource_clear(const char *rsc_id, const char *host, GList *allnodes,
+ cib_t * cib_conn, int cib_options, bool clear_ban_constraints, gboolean force);
+int cli_resource_clear_all_expired(xmlNode *root, cib_t *cib_conn, int cib_options,
+ const char *rsc, const char *node, gboolean promoted_role_only);
+
+/* print */
+void cli_resource_print_cts(pe_resource_t * rsc, pcmk__output_t *out);
+void cli_resource_print_cts_constraints(pe_working_set_t * data_set);
+
+int cli_resource_print(pe_resource_t *rsc, pe_working_set_t *data_set, bool expanded);
+int cli_resource_print_operations(const char *rsc_id, const char *host_uname,
+ bool active, pe_working_set_t * data_set);
+
+/* runtime */
+int cli_resource_check(pcmk__output_t *out, pe_resource_t *rsc,
+ pe_node_t *node);
+int cli_resource_fail(pcmk_ipc_api_t *controld_api, const char *host_uname,
+ const char *rsc_id, pe_working_set_t *data_set);
+GList *cli_resource_search(pe_resource_t *rsc, const char *requested_name,
+ pe_working_set_t *data_set);
+int cli_resource_delete(pcmk_ipc_api_t *controld_api, const char *host_uname,
+ const pe_resource_t *rsc, const char *operation,
+ const char *interval_spec, bool just_failures,
+ pe_working_set_t *data_set, gboolean force);
+int cli_cleanup_all(pcmk_ipc_api_t *controld_api, const char *node_name,
+ const char *operation, const char *interval_spec,
+ pe_working_set_t *data_set);
+int cli_resource_restart(pcmk__output_t *out, pe_resource_t *rsc,
+ const pe_node_t *node, const char *move_lifetime,
+ int timeout_ms, cib_t *cib, int cib_options,
+ gboolean promoted_role_only, gboolean force);
+int cli_resource_move(const pe_resource_t *rsc, const char *rsc_id,
+ const char *host_name, const char *move_lifetime,
+ cib_t *cib, int cib_options, pe_working_set_t *data_set,
+ gboolean promoted_role_only, gboolean force);
+crm_exit_t cli_resource_execute_from_params(pcmk__output_t *out, const char *rsc_name,
+ const char *rsc_class, const char *rsc_prov,
+ const char *rsc_type, const char *rsc_action,
+ GHashTable *params, GHashTable *override_hash,
+ int timeout_ms, int resource_verbose,
+ gboolean force, int check_level);
+crm_exit_t cli_resource_execute(pe_resource_t *rsc, const char *requested_name,
+ const char *rsc_action, GHashTable *override_hash,
+ int timeout_ms, cib_t *cib, pe_working_set_t *data_set,
+ int resource_verbose, gboolean force, int check_level);
+
+int cli_resource_update_attribute(pe_resource_t *rsc, const char *requested_name,
+ const char *attr_set, const char *attr_set_type,
+ const char *attr_id, const char *attr_name,
+ const char *attr_value, gboolean recursive,
+ cib_t *cib, int cib_options, gboolean force);
+int cli_resource_delete_attribute(pe_resource_t *rsc, const char *requested_name,
+ const char *attr_set, const char *attr_set_type,
+ const char *attr_id, const char *attr_name,
+ cib_t *cib, int cib_options, gboolean force);
+
+int update_working_set_xml(pe_working_set_t *data_set, xmlNode **xml);
+int wait_till_stable(pcmk__output_t *out, int timeout_ms, cib_t * cib);
+
+bool resource_is_running_on(pe_resource_t *rsc, const char *host);
+
+void crm_resource_register_messages(pcmk__output_t *out);
diff --git a/tools/crm_resource_ban.c b/tools/crm_resource_ban.c
new file mode 100644
index 0000000..b1edac8
--- /dev/null
+++ b/tools/crm_resource_ban.c
@@ -0,0 +1,505 @@
+/*
+ * Copyright 2004-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 <crm_resource.h>
+
+static char *
+parse_cli_lifetime(pcmk__output_t *out, const char *move_lifetime)
+{
+ char *later_s = NULL;
+ crm_time_t *now = NULL;
+ crm_time_t *later = NULL;
+ crm_time_t *duration = NULL;
+
+ if (move_lifetime == NULL) {
+ return NULL;
+ }
+
+ duration = crm_time_parse_duration(move_lifetime);
+ if (duration == NULL) {
+ out->err(out, "Invalid duration specified: %s\n"
+ "Please refer to https://en.wikipedia.org/wiki/ISO_8601#Durations "
+ "for examples of valid durations", move_lifetime);
+ return NULL;
+ }
+
+ now = crm_time_new(NULL);
+ later = crm_time_add(now, duration);
+ if (later == NULL) {
+ out->err(out, "Unable to add %s to current time\n"
+ "Please report to " PACKAGE_BUGREPORT " as possible bug",
+ move_lifetime);
+ crm_time_free(now);
+ crm_time_free(duration);
+ return NULL;
+ }
+
+ crm_time_log(LOG_INFO, "now ", now,
+ crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
+ crm_time_log(LOG_INFO, "later ", later,
+ crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
+ crm_time_log(LOG_INFO, "duration", duration, crm_time_log_date | crm_time_log_timeofday);
+ later_s = crm_time_as_string(later, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
+ out->info(out, "Migration will take effect until: %s", later_s);
+
+ crm_time_free(duration);
+ crm_time_free(later);
+ crm_time_free(now);
+ return later_s;
+}
+
+static const char *
+promoted_role_name(void)
+{
+ /* This is a judgment call for what string to use. @TODO Ideally we'd
+ * use the legacy string if the DC only supports that, and the new one
+ * otherwise. Basing it on --enable-compat-2.0 is a decent guess.
+ */
+#ifdef PCMK__COMPAT_2_0
+ return RSC_ROLE_PROMOTED_LEGACY_S;
+#else
+ return RSC_ROLE_PROMOTED_S;
+#endif
+}
+
+// \return Standard Pacemaker return code
+int
+cli_resource_ban(pcmk__output_t *out, const char *rsc_id, const char *host,
+ const char *move_lifetime, GList *allnodes, cib_t * cib_conn,
+ int cib_options, gboolean promoted_role_only)
+{
+ char *later_s = NULL;
+ int rc = pcmk_rc_ok;
+ xmlNode *fragment = NULL;
+ xmlNode *location = NULL;
+
+ if(host == NULL) {
+ GList *n = allnodes;
+ for(; n && rc == pcmk_rc_ok; n = n->next) {
+ pe_node_t *target = n->data;
+
+ rc = cli_resource_ban(out, rsc_id, target->details->uname, move_lifetime,
+ NULL, cib_conn, cib_options, promoted_role_only);
+ }
+ return rc;
+ }
+
+ later_s = parse_cli_lifetime(out, move_lifetime);
+ if(move_lifetime && later_s == NULL) {
+ return EINVAL;
+ }
+
+ fragment = create_xml_node(NULL, XML_CIB_TAG_CONSTRAINTS);
+
+ location = create_xml_node(fragment, XML_CONS_TAG_RSC_LOCATION);
+ crm_xml_set_id(location, "cli-ban-%s-on-%s", rsc_id, host);
+
+ out->info(out, "WARNING: Creating rsc_location constraint '%s' with a "
+ "score of -INFINITY for resource %s on %s.\n\tThis will "
+ "prevent %s from %s on %s until the constraint is removed "
+ "using the clear option or by editing the CIB with an "
+ "appropriate tool\n\tThis will be the case even if %s "
+ "is the last node in the cluster",
+ ID(location), rsc_id, host, rsc_id,
+ (promoted_role_only? "being promoted" : "running"),
+ host, host);
+
+ crm_xml_add(location, XML_LOC_ATTR_SOURCE, rsc_id);
+ if(promoted_role_only) {
+ crm_xml_add(location, XML_RULE_ATTR_ROLE, promoted_role_name());
+ } else {
+ crm_xml_add(location, XML_RULE_ATTR_ROLE, RSC_ROLE_STARTED_S);
+ }
+
+ if (later_s == NULL) {
+ /* Short form */
+ crm_xml_add(location, XML_CIB_TAG_NODE, host);
+ crm_xml_add(location, XML_RULE_ATTR_SCORE, CRM_MINUS_INFINITY_S);
+
+ } else {
+ xmlNode *rule = create_xml_node(location, XML_TAG_RULE);
+ xmlNode *expr = create_xml_node(rule, XML_TAG_EXPRESSION);
+
+ crm_xml_set_id(rule, "cli-ban-%s-on-%s-rule", rsc_id, host);
+ crm_xml_add(rule, XML_RULE_ATTR_SCORE, CRM_MINUS_INFINITY_S);
+ crm_xml_add(rule, XML_RULE_ATTR_BOOLEAN_OP, "and");
+
+ crm_xml_set_id(expr, "cli-ban-%s-on-%s-expr", rsc_id, host);
+ crm_xml_add(expr, XML_EXPR_ATTR_ATTRIBUTE, CRM_ATTR_UNAME);
+ crm_xml_add(expr, XML_EXPR_ATTR_OPERATION, "eq");
+ crm_xml_add(expr, XML_EXPR_ATTR_VALUE, host);
+ crm_xml_add(expr, XML_EXPR_ATTR_TYPE, "string");
+
+ expr = create_xml_node(rule, "date_expression");
+ crm_xml_set_id(expr, "cli-ban-%s-on-%s-lifetime", rsc_id, host);
+ crm_xml_add(expr, "operation", "lt");
+ crm_xml_add(expr, "end", later_s);
+ }
+
+ crm_log_xml_notice(fragment, "Modify");
+ rc = cib_conn->cmds->modify(cib_conn, XML_CIB_TAG_CONSTRAINTS, fragment,
+ cib_options);
+ rc = pcmk_legacy2rc(rc);
+
+ free_xml(fragment);
+ free(later_s);
+ return rc;
+}
+
+// \return Standard Pacemaker return code
+int
+cli_resource_prefer(pcmk__output_t *out,const char *rsc_id, const char *host,
+ const char *move_lifetime, cib_t * cib_conn, int cib_options,
+ gboolean promoted_role_only)
+{
+ char *later_s = parse_cli_lifetime(out, move_lifetime);
+ int rc = pcmk_rc_ok;
+ xmlNode *location = NULL;
+ xmlNode *fragment = NULL;
+
+ if(move_lifetime && later_s == NULL) {
+ return EINVAL;
+ }
+
+ if(cib_conn == NULL) {
+ free(later_s);
+ return ENOTCONN;
+ }
+
+ fragment = create_xml_node(NULL, XML_CIB_TAG_CONSTRAINTS);
+
+ location = create_xml_node(fragment, XML_CONS_TAG_RSC_LOCATION);
+ crm_xml_set_id(location, "cli-prefer-%s", rsc_id);
+
+ crm_xml_add(location, XML_LOC_ATTR_SOURCE, rsc_id);
+ if(promoted_role_only) {
+ crm_xml_add(location, XML_RULE_ATTR_ROLE, promoted_role_name());
+ } else {
+ crm_xml_add(location, XML_RULE_ATTR_ROLE, RSC_ROLE_STARTED_S);
+ }
+
+ if (later_s == NULL) {
+ /* Short form */
+ crm_xml_add(location, XML_CIB_TAG_NODE, host);
+ crm_xml_add(location, XML_RULE_ATTR_SCORE, CRM_INFINITY_S);
+
+ } else {
+ xmlNode *rule = create_xml_node(location, XML_TAG_RULE);
+ xmlNode *expr = create_xml_node(rule, XML_TAG_EXPRESSION);
+
+ crm_xml_set_id(rule, "cli-prefer-rule-%s", rsc_id);
+ crm_xml_add(rule, XML_RULE_ATTR_SCORE, CRM_INFINITY_S);
+ crm_xml_add(rule, XML_RULE_ATTR_BOOLEAN_OP, "and");
+
+ crm_xml_set_id(expr, "cli-prefer-expr-%s", rsc_id);
+ crm_xml_add(expr, XML_EXPR_ATTR_ATTRIBUTE, CRM_ATTR_UNAME);
+ crm_xml_add(expr, XML_EXPR_ATTR_OPERATION, "eq");
+ crm_xml_add(expr, XML_EXPR_ATTR_VALUE, host);
+ crm_xml_add(expr, XML_EXPR_ATTR_TYPE, "string");
+
+ expr = create_xml_node(rule, "date_expression");
+ crm_xml_set_id(expr, "cli-prefer-lifetime-end-%s", rsc_id);
+ crm_xml_add(expr, "operation", "lt");
+ crm_xml_add(expr, "end", later_s);
+ }
+
+ crm_log_xml_info(fragment, "Modify");
+ rc = cib_conn->cmds->modify(cib_conn, XML_CIB_TAG_CONSTRAINTS, fragment,
+ cib_options);
+ rc = pcmk_legacy2rc(rc);
+
+ free_xml(fragment);
+ free(later_s);
+ return rc;
+}
+
+/* Nodes can be specified two different ways in the CIB, so we have two different
+ * functions to try clearing out any constraints on them:
+ *
+ * (1) The node could be given by attribute=/value= in an expression XML node.
+ * That's what resource_clear_node_in_expr handles. That XML looks like this:
+ *
+ * <rsc_location id="cli-prefer-dummy" rsc="dummy" role="Started">
+ * <rule id="cli-prefer-rule-dummy" score="INFINITY" boolean-op="and">
+ * <expression id="cli-prefer-expr-dummy" attribute="#uname" operation="eq" value="test02" type="string"/>
+ * <date_expression id="cli-prefer-lifetime-end-dummy" operation="lt" end="2018-12-12 14:05:37 -05:00"/>
+ * </rule>
+ * </rsc_location>
+ *
+ * (2) The mode could be given by node= in an rsc_location XML node. That's
+ * what resource_clear_node_in_location handles. That XML looks like this:
+ *
+ * <rsc_location id="cli-prefer-dummy" rsc="dummy" role="Started" node="node1" score="INFINITY"/>
+ *
+ * \return Standard Pacemaker return code
+ */
+static int
+resource_clear_node_in_expr(const char *rsc_id, const char *host, cib_t * cib_conn,
+ int cib_options)
+{
+ int rc = pcmk_rc_ok;
+ char *xpath_string = NULL;
+
+#define XPATH_FMT \
+ "//" XML_CONS_TAG_RSC_LOCATION "[@" XML_ATTR_ID "='cli-prefer-%s']" \
+ "[" XML_TAG_RULE \
+ "[@" XML_ATTR_ID "='cli-prefer-rule-%s']" \
+ "/" XML_TAG_EXPRESSION \
+ "[@" XML_EXPR_ATTR_ATTRIBUTE "='#uname' " \
+ "and @" XML_EXPR_ATTR_VALUE "='%s']" \
+ "]"
+
+ xpath_string = crm_strdup_printf(XPATH_FMT, rsc_id, rsc_id, host);
+
+ rc = cib_conn->cmds->remove(cib_conn, xpath_string, NULL, cib_xpath | cib_options);
+ if (rc == -ENXIO) {
+ rc = pcmk_rc_ok;
+ } else {
+ rc = pcmk_legacy2rc(rc);
+ }
+
+ free(xpath_string);
+ return rc;
+}
+
+// \return Standard Pacemaker return code
+static int
+resource_clear_node_in_location(const char *rsc_id, const char *host, cib_t * cib_conn,
+ int cib_options, bool clear_ban_constraints, gboolean force)
+{
+ int rc = pcmk_rc_ok;
+ xmlNode *fragment = NULL;
+ xmlNode *location = NULL;
+
+ fragment = create_xml_node(NULL, XML_CIB_TAG_CONSTRAINTS);
+
+ if (clear_ban_constraints == TRUE) {
+ location = create_xml_node(fragment, XML_CONS_TAG_RSC_LOCATION);
+ crm_xml_set_id(location, "cli-ban-%s-on-%s", rsc_id, host);
+ }
+
+ location = create_xml_node(fragment, XML_CONS_TAG_RSC_LOCATION);
+ crm_xml_set_id(location, "cli-prefer-%s", rsc_id);
+ if (force == FALSE) {
+ crm_xml_add(location, XML_CIB_TAG_NODE, host);
+ }
+
+ crm_log_xml_info(fragment, "Delete");
+ rc = cib_conn->cmds->remove(cib_conn, XML_CIB_TAG_CONSTRAINTS, fragment, cib_options);
+ if (rc == -ENXIO) {
+ rc = pcmk_rc_ok;
+ } else {
+ rc = pcmk_legacy2rc(rc);
+ }
+
+ free_xml(fragment);
+ return rc;
+}
+
+// \return Standard Pacemaker return code
+int
+cli_resource_clear(const char *rsc_id, const char *host, GList *allnodes, cib_t * cib_conn,
+ int cib_options, bool clear_ban_constraints, gboolean force)
+{
+ int rc = pcmk_rc_ok;
+
+ if(cib_conn == NULL) {
+ return ENOTCONN;
+ }
+
+ if (host) {
+ rc = resource_clear_node_in_expr(rsc_id, host, cib_conn, cib_options);
+
+ /* rc does not tell us whether the previous operation did anything, only
+ * whether it failed or not. Thus, as long as it did not fail, we need
+ * to try the second clear method.
+ */
+ if (rc == pcmk_rc_ok) {
+ rc = resource_clear_node_in_location(rsc_id, host, cib_conn,
+ cib_options, clear_ban_constraints,
+ force);
+ }
+
+ } else {
+ GList *n = allnodes;
+
+ /* Iterate over all nodes, attempting to clear the constraint from each.
+ * On the first error, abort.
+ */
+ for(; n; n = n->next) {
+ pe_node_t *target = n->data;
+
+ rc = cli_resource_clear(rsc_id, target->details->uname, NULL,
+ cib_conn, cib_options, clear_ban_constraints,
+ force);
+ if (rc != pcmk_rc_ok) {
+ break;
+ }
+ }
+ }
+
+ return rc;
+}
+
+static void
+build_clear_xpath_string(GString *buf, const xmlNode *constraint_node,
+ const char *rsc, const char *node,
+ bool promoted_role_only)
+{
+ const char *cons_id = ID(constraint_node);
+ const char *cons_rsc = crm_element_value(constraint_node,
+ XML_LOC_ATTR_SOURCE);
+ GString *rsc_role_substr = NULL;
+
+ CRM_ASSERT(buf != NULL);
+ g_string_truncate(buf, 0);
+
+ if (!pcmk__starts_with(cons_id, "cli-ban-")
+ && !pcmk__starts_with(cons_id, "cli-prefer-")) {
+ return;
+ }
+
+ g_string_append(buf, "//" XML_CONS_TAG_RSC_LOCATION);
+
+ if ((node != NULL) || (rsc != NULL) || promoted_role_only) {
+ g_string_append_c(buf, '[');
+
+ if (node != NULL) {
+ pcmk__g_strcat(buf, "@" XML_CIB_TAG_NODE "='", node, "'", NULL);
+
+ if (promoted_role_only || (rsc != NULL)) {
+ g_string_append(buf, " and ");
+ }
+ }
+
+ if ((rsc != NULL) && promoted_role_only) {
+ rsc_role_substr = g_string_sized_new(64);
+ pcmk__g_strcat(rsc_role_substr,
+ "@" XML_LOC_ATTR_SOURCE "='", rsc, "' "
+ "and @" XML_RULE_ATTR_ROLE "='",
+ promoted_role_name(), "'", NULL);
+
+ } else if (rsc != NULL) {
+ rsc_role_substr = g_string_sized_new(64);
+ pcmk__g_strcat(rsc_role_substr,
+ "@" XML_LOC_ATTR_SOURCE "='", rsc, "'", NULL);
+
+ } else if (promoted_role_only) {
+ rsc_role_substr = g_string_sized_new(64);
+ pcmk__g_strcat(rsc_role_substr,
+ "@" XML_RULE_ATTR_ROLE "='", promoted_role_name(),
+ "'", NULL);
+ }
+
+ if (rsc_role_substr != NULL) {
+ g_string_append(buf, rsc_role_substr->str);
+ }
+ g_string_append_c(buf, ']');
+ }
+
+ if (node != NULL) {
+ g_string_append(buf, "|//" XML_CONS_TAG_RSC_LOCATION);
+
+ if (rsc_role_substr != NULL) {
+ pcmk__g_strcat(buf, "[", rsc_role_substr, "]", NULL);
+ }
+ pcmk__g_strcat(buf,
+ "/" XML_TAG_RULE "[" XML_TAG_EXPRESSION
+ "[@" XML_EXPR_ATTR_ATTRIBUTE "='" CRM_ATTR_UNAME "' "
+ "and @" XML_EXPR_ATTR_VALUE "='", node, "']]", NULL);
+ }
+
+ g_string_append(buf, "//" PCMK_XE_DATE_EXPRESSION "[@" XML_ATTR_ID "='");
+ if (pcmk__starts_with(cons_id, "cli-ban-")) {
+ pcmk__g_strcat(buf, cons_id, "-lifetime']", NULL);
+
+ } else { // starts with "cli-prefer-"
+ pcmk__g_strcat(buf,
+ "cli-prefer-lifetime-end-", cons_rsc, "']", NULL);
+ }
+
+ if (rsc_role_substr != NULL) {
+ g_string_free(rsc_role_substr, TRUE);
+ }
+}
+
+// \return Standard Pacemaker return code
+int
+cli_resource_clear_all_expired(xmlNode *root, cib_t *cib_conn, int cib_options,
+ const char *rsc, const char *node, gboolean promoted_role_only)
+{
+ GString *buf = NULL;
+ xmlXPathObject *xpathObj = NULL;
+ xmlNode *cib_constraints = NULL;
+ crm_time_t *now = crm_time_new(NULL);
+ int i;
+ int rc = pcmk_rc_ok;
+
+ cib_constraints = pcmk_find_cib_element(root, XML_CIB_TAG_CONSTRAINTS);
+ xpathObj = xpath_search(cib_constraints, "//" XML_CONS_TAG_RSC_LOCATION);
+
+ for (i = 0; i < numXpathResults(xpathObj); i++) {
+ xmlNode *constraint_node = getXpathResult(xpathObj, i);
+ xmlNode *date_expr_node = NULL;
+ crm_time_t *end = NULL;
+
+ if (buf == NULL) {
+ buf = g_string_sized_new(1024);
+ }
+
+ build_clear_xpath_string(buf, constraint_node, rsc, node,
+ promoted_role_only);
+ if (buf->len == 0) {
+ continue;
+ }
+
+ date_expr_node = get_xpath_object((const char *) buf->str,
+ constraint_node, LOG_DEBUG);
+ if (date_expr_node == NULL) {
+ continue;
+ }
+
+ /* And then finally, see if the date expression is expired. If so,
+ * clear the constraint.
+ */
+ end = crm_time_new(crm_element_value(date_expr_node, "end"));
+
+ if (crm_time_compare(now, end) == 1) {
+ xmlNode *fragment = NULL;
+ xmlNode *location = NULL;
+
+ fragment = create_xml_node(NULL, XML_CIB_TAG_CONSTRAINTS);
+ location = create_xml_node(fragment, XML_CONS_TAG_RSC_LOCATION);
+ crm_xml_set_id(location, "%s", ID(constraint_node));
+ crm_log_xml_info(fragment, "Delete");
+
+ rc = cib_conn->cmds->remove(cib_conn, XML_CIB_TAG_CONSTRAINTS,
+ fragment, cib_options);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ goto done;
+ }
+
+ free_xml(fragment);
+ }
+
+ crm_time_free(end);
+ }
+
+done:
+ if (buf != NULL) {
+ g_string_free(buf, TRUE);
+ }
+ freeXpathObject(xpathObj);
+ crm_time_free(now);
+ return rc;
+}
diff --git a/tools/crm_resource_print.c b/tools/crm_resource_print.c
new file mode 100644
index 0000000..c1be53c
--- /dev/null
+++ b/tools/crm_resource_print.c
@@ -0,0 +1,818 @@
+/*
+ * Copyright 2004-2023 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 <stdint.h>
+
+#include <crm_resource.h>
+#include <crm/common/lists_internal.h>
+#include <crm/common/output.h>
+#include <crm/common/results.h>
+
+#define cons_string(x) x?x:"NA"
+static int
+print_constraint(xmlNode *xml_obj, void *userdata)
+{
+ pe_working_set_t *data_set = (pe_working_set_t *) userdata;
+ pcmk__output_t *out = data_set->priv;
+ xmlNode *lifetime = NULL;
+ const char *id = crm_element_value(xml_obj, XML_ATTR_ID);
+
+ if (id == NULL) {
+ return pcmk_rc_ok;
+ }
+
+ // @COMPAT lifetime is deprecated
+ lifetime = first_named_child(xml_obj, "lifetime");
+ if (pe_evaluate_rules(lifetime, NULL, data_set->now, NULL) == FALSE) {
+ return pcmk_rc_ok;
+ }
+
+ if (!pcmk__str_eq(XML_CONS_TAG_RSC_DEPEND, crm_element_name(xml_obj), pcmk__str_casei)) {
+ return pcmk_rc_ok;
+ }
+
+ out->info(out, "Constraint %s %s %s %s %s %s %s",
+ crm_element_name(xml_obj),
+ cons_string(crm_element_value(xml_obj, XML_ATTR_ID)),
+ cons_string(crm_element_value(xml_obj, XML_COLOC_ATTR_SOURCE)),
+ cons_string(crm_element_value(xml_obj, XML_COLOC_ATTR_TARGET)),
+ cons_string(crm_element_value(xml_obj, XML_RULE_ATTR_SCORE)),
+ cons_string(crm_element_value(xml_obj, XML_COLOC_ATTR_SOURCE_ROLE)),
+ cons_string(crm_element_value(xml_obj, XML_COLOC_ATTR_TARGET_ROLE)));
+
+ return pcmk_rc_ok;
+}
+
+void
+cli_resource_print_cts_constraints(pe_working_set_t * data_set)
+{
+ pcmk__xe_foreach_child(pcmk_find_cib_element(data_set->input, XML_CIB_TAG_CONSTRAINTS),
+ NULL, print_constraint, data_set);
+}
+
+void
+cli_resource_print_cts(pe_resource_t * rsc, pcmk__output_t *out)
+{
+ const char *host = NULL;
+ bool needs_quorum = TRUE;
+ const char *rtype = crm_element_value(rsc->xml, XML_ATTR_TYPE);
+ const char *rprov = crm_element_value(rsc->xml, XML_AGENT_ATTR_PROVIDER);
+ const char *rclass = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS);
+ pe_node_t *node = pe__current_node(rsc);
+
+ if (pcmk__str_eq(rclass, PCMK_RESOURCE_CLASS_STONITH, pcmk__str_casei)) {
+ needs_quorum = FALSE;
+ } else {
+ // @TODO check requires in resource meta-data and rsc_defaults
+ }
+
+ if (node != NULL) {
+ host = node->details->uname;
+ }
+
+ out->info(out, "Resource: %s %s %s %s %s %s %s %s %d %lld %#.16llx",
+ crm_element_name(rsc->xml), rsc->id,
+ rsc->clone_name ? rsc->clone_name : rsc->id, rsc->parent ? rsc->parent->id : "NA",
+ rprov ? rprov : "NA", rclass, rtype, host ? host : "NA", needs_quorum, rsc->flags,
+ rsc->flags);
+
+ g_list_foreach(rsc->children, (GFunc) cli_resource_print_cts, out);
+}
+
+// \return Standard Pacemaker return code
+int
+cli_resource_print_operations(const char *rsc_id, const char *host_uname,
+ bool active, pe_working_set_t * data_set)
+{
+ pcmk__output_t *out = data_set->priv;
+ int rc = pcmk_rc_no_output;
+ GList *ops = find_operations(rsc_id, host_uname, active, data_set);
+
+ if (!ops) {
+ return rc;
+ }
+
+ out->begin_list(out, NULL, NULL, "Resource Operations");
+ rc = pcmk_rc_ok;
+
+ for (GList *lpc = ops; lpc != NULL; lpc = lpc->next) {
+ xmlNode *xml_op = (xmlNode *) lpc->data;
+ out->message(out, "node-and-op", data_set, xml_op);
+ }
+
+ out->end_list(out);
+ return rc;
+}
+
+// \return Standard Pacemaker return code
+int
+cli_resource_print(pe_resource_t *rsc, pe_working_set_t *data_set, bool expanded)
+{
+ pcmk__output_t *out = data_set->priv;
+ uint32_t show_opts = pcmk_show_pending;
+ GList *all = NULL;
+
+ all = g_list_prepend(all, (gpointer) "*");
+
+ out->begin_list(out, NULL, NULL, "Resource Config");
+ out->message(out, crm_map_element_name(rsc->xml), show_opts, rsc, all, all);
+ out->message(out, "resource-config", rsc, !expanded);
+ out->end_list(out);
+
+ g_list_free(all);
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("attribute-list", "pe_resource_t *", "const char *", "const char *")
+static int
+attribute_list_default(pcmk__output_t *out, va_list args) {
+ pe_resource_t *rsc = va_arg(args, pe_resource_t *);
+ const char *attr = va_arg(args, char *);
+ const char *value = va_arg(args, const char *);
+
+ if (value != NULL) {
+ out->begin_list(out, NULL, NULL, "Attributes");
+ out->list_item(out, attr, "%s", value);
+ out->end_list(out);
+ return pcmk_rc_ok;
+ } else {
+ out->err(out, "Attribute '%s' not found for '%s'", attr, rsc->id);
+ }
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("agent-status", "int", "const char *", "const char *", "const char *",
+ "const char *", "const char *", "crm_exit_t", "const char *")
+static int
+agent_status_default(pcmk__output_t *out, va_list args) {
+ int status = va_arg(args, int);
+ const char *action = va_arg(args, const char *);
+ const char *name = va_arg(args, const char *);
+ const char *class = va_arg(args, const char *);
+ const char *provider = va_arg(args, const char *);
+ const char *type = va_arg(args, const char *);
+ crm_exit_t rc = va_arg(args, crm_exit_t);
+ const char *exit_reason = va_arg(args, const char *);
+
+ if (status == PCMK_EXEC_DONE) {
+ /* Operation <action> [for <resource>] (<class>[:<provider>]:<agent>)
+ * returned <exit-code> (<exit-description>[: <exit-reason>])
+ */
+ out->info(out, "Operation %s%s%s (%s%s%s:%s) returned %d (%s%s%s)",
+ action,
+ ((name == NULL)? "" : " for "), ((name == NULL)? "" : name),
+ class,
+ ((provider == NULL)? "" : ":"),
+ ((provider == NULL)? "" : provider),
+ type, (int) rc, services_ocf_exitcode_str((int) rc),
+ ((exit_reason == NULL)? "" : ": "),
+ ((exit_reason == NULL)? "" : exit_reason));
+ } else {
+ /* Operation <action> [for <resource>] (<class>[:<provider>]:<agent>)
+ * could not be executed (<execution-status>[: <exit-reason>])
+ */
+ out->err(out,
+ "Operation %s%s%s (%s%s%s:%s) could not be executed (%s%s%s)",
+ action,
+ ((name == NULL)? "" : " for "), ((name == NULL)? "" : name),
+ class,
+ ((provider == NULL)? "" : ":"),
+ ((provider == NULL)? "" : provider),
+ type, pcmk_exec_status_str(status),
+ ((exit_reason == NULL)? "" : ": "),
+ ((exit_reason == NULL)? "" : exit_reason));
+ }
+
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("agent-status", "int", "const char *", "const char *", "const char *",
+ "const char *", "const char *", "crm_exit_t", "const char *")
+static int
+agent_status_xml(pcmk__output_t *out, va_list args) {
+ int status = va_arg(args, int);
+ const char *action G_GNUC_UNUSED = va_arg(args, const char *);
+ const char *name G_GNUC_UNUSED = va_arg(args, const char *);
+ const char *class G_GNUC_UNUSED = va_arg(args, const char *);
+ const char *provider G_GNUC_UNUSED = va_arg(args, const char *);
+ const char *type G_GNUC_UNUSED = va_arg(args, const char *);
+ crm_exit_t rc = va_arg(args, crm_exit_t);
+ const char *exit_reason = va_arg(args, const char *);
+
+ char *exit_str = pcmk__itoa(rc);
+ char *status_str = pcmk__itoa(status);
+
+ pcmk__output_create_xml_node(out, "agent-status",
+ "code", exit_str,
+ "message", services_ocf_exitcode_str((int) rc),
+ "execution_code", status_str,
+ "execution_message", pcmk_exec_status_str(status),
+ "reason", exit_reason,
+ NULL);
+
+ free(exit_str);
+ free(status_str);
+
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("attribute-list", "pe_resource_t *", "const char *", "const char *")
+static int
+attribute_list_text(pcmk__output_t *out, va_list args) {
+ pe_resource_t *rsc = va_arg(args, pe_resource_t *);
+ const char *attr = va_arg(args, char *);
+ const char *value = va_arg(args, const char *);
+
+ if (value != NULL) {
+ pcmk__formatted_printf(out, "%s\n", value);
+ return pcmk_rc_ok;
+ } else {
+ out->err(out, "Attribute '%s' not found for '%s'", attr, rsc->id);
+ }
+ return pcmk_rc_ok;
+}
+PCMK__OUTPUT_ARGS("override", "const char *", "const char *", "const char *")
+static int
+override_default(pcmk__output_t *out, va_list args) {
+ const char *rsc_name = va_arg(args, const char *);
+ const char *name = va_arg(args, const char *);
+ const char *value = va_arg(args, const char *);
+
+ if (rsc_name == NULL) {
+ out->list_item(out, NULL, "Overriding the cluster configuration with '%s' = '%s'",
+ name, value);
+ } else {
+ out->list_item(out, NULL, "Overriding the cluster configuration for '%s' with '%s' = '%s'",
+ rsc_name, name, value);
+ }
+
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("override", "const char *", "const char *", "const char *")
+static int
+override_xml(pcmk__output_t *out, va_list args) {
+ const char *rsc_name = va_arg(args, const char *);
+ const char *name = va_arg(args, const char *);
+ const char *value = va_arg(args, const char *);
+
+ xmlNodePtr node = pcmk__output_create_xml_node(out, "override",
+ "name", name,
+ "value", value,
+ NULL);
+
+ if (rsc_name != NULL) {
+ crm_xml_add(node, "rsc", rsc_name);
+ }
+
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("property-list", "pe_resource_t *", "const char *")
+static int
+property_list_default(pcmk__output_t *out, va_list args) {
+ pe_resource_t *rsc = va_arg(args, pe_resource_t *);
+ const char *attr = va_arg(args, char *);
+
+ const char *value = crm_element_value(rsc->xml, attr);
+
+ if (value != NULL) {
+ out->begin_list(out, NULL, NULL, "Properties");
+ out->list_item(out, attr, "%s", value);
+ out->end_list(out);
+ }
+
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("property-list", "pe_resource_t *", "const char *")
+static int
+property_list_text(pcmk__output_t *out, va_list args) {
+ pe_resource_t *rsc = va_arg(args, pe_resource_t *);
+ const char *attr = va_arg(args, const char *);
+
+ const char *value = crm_element_value(rsc->xml, attr);
+
+ if (value != NULL) {
+ pcmk__formatted_printf(out, "%s\n", value);
+ }
+
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("resource-agent-action", "int", "const char *", "const char *",
+ "const char *", "const char *", "const char *", "GHashTable *",
+ "crm_exit_t", "int", "const char *", "const char *", "const char *")
+static int
+resource_agent_action_default(pcmk__output_t *out, va_list args) {
+ int verbose = va_arg(args, int);
+
+ const char *class = va_arg(args, const char *);
+ const char *provider = va_arg(args, const char *);
+ const char *type = va_arg(args, const char *);
+ const char *rsc_name = va_arg(args, const char *);
+ const char *action = va_arg(args, const char *);
+ GHashTable *overrides = va_arg(args, GHashTable *);
+ crm_exit_t rc = va_arg(args, crm_exit_t);
+ int status = va_arg(args, int);
+ const char *exit_reason = va_arg(args, const char *);
+ const char *stdout_data = va_arg(args, const char *);
+ const char *stderr_data = va_arg(args, const char *);
+
+ if (overrides) {
+ GHashTableIter iter;
+ const char *name = NULL;
+ const char *value = NULL;
+
+ out->begin_list(out, NULL, NULL, "overrides");
+
+ g_hash_table_iter_init(&iter, overrides);
+ while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &value)) {
+ out->message(out, "override", rsc_name, name, value);
+ }
+
+ out->end_list(out);
+ }
+
+ out->message(out, "agent-status", status, action, rsc_name, class, provider,
+ type, rc, exit_reason);
+
+ /* hide output for validate-all if not in verbose */
+ if (verbose == 0 && pcmk__str_eq(action, "validate-all", pcmk__str_casei)) {
+ return pcmk_rc_ok;
+ }
+
+ if (stdout_data || stderr_data) {
+ xmlNodePtr doc = NULL;
+
+ if (stdout_data != NULL) {
+ doc = string2xml(stdout_data);
+ }
+ if (doc != NULL) {
+ out->output_xml(out, "command", stdout_data);
+ xmlFreeNode(doc);
+ } else {
+ out->subprocess_output(out, rc, stdout_data, stderr_data);
+ }
+ }
+
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("resource-agent-action", "int", "const char *", "const char *",
+ "const char *", "const char *", "const char *", "GHashTable *",
+ "crm_exit_t", "int", "const char *", "const char *", "const char *")
+static int
+resource_agent_action_xml(pcmk__output_t *out, va_list args) {
+ int verbose G_GNUC_UNUSED = va_arg(args, int);
+
+ const char *class = va_arg(args, const char *);
+ const char *provider = va_arg(args, const char *);
+ const char *type = va_arg(args, const char *);
+ const char *rsc_name = va_arg(args, const char *);
+ const char *action = va_arg(args, const char *);
+ GHashTable *overrides = va_arg(args, GHashTable *);
+ crm_exit_t rc = va_arg(args, crm_exit_t);
+ int status = va_arg(args, int);
+ const char *exit_reason = va_arg(args, const char *);
+ const char *stdout_data = va_arg(args, const char *);
+ const char *stderr_data = va_arg(args, const char *);
+
+ xmlNodePtr node = pcmk__output_xml_create_parent(out, "resource-agent-action",
+ "action", action,
+ "class", class,
+ "type", type,
+ NULL);
+
+ if (rsc_name) {
+ crm_xml_add(node, "rsc", rsc_name);
+ }
+
+ if (provider) {
+ crm_xml_add(node, "provider", provider);
+ }
+
+ if (overrides) {
+ GHashTableIter iter;
+ const char *name = NULL;
+ const char *value = NULL;
+
+ out->begin_list(out, NULL, NULL, "overrides");
+
+ g_hash_table_iter_init(&iter, overrides);
+ while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &value)) {
+ out->message(out, "override", rsc_name, name, value);
+ }
+
+ out->end_list(out);
+ }
+
+ out->message(out, "agent-status", status, action, rsc_name, class, provider,
+ type, rc, exit_reason);
+
+ if (stdout_data || stderr_data) {
+ xmlNodePtr doc = NULL;
+
+ if (stdout_data != NULL) {
+ doc = string2xml(stdout_data);
+ }
+ if (doc != NULL) {
+ out->output_xml(out, "command", stdout_data);
+ xmlFreeNode(doc);
+ } else {
+ out->subprocess_output(out, rc, stdout_data, stderr_data);
+ }
+ }
+
+ pcmk__output_xml_pop_parent(out);
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("resource-check-list", "resource_checks_t *")
+static int
+resource_check_list_default(pcmk__output_t *out, va_list args) {
+ resource_checks_t *checks = va_arg(args, resource_checks_t *);
+
+ const pe_resource_t *parent = pe__const_top_resource(checks->rsc, false);
+
+ if (checks->flags == 0) {
+ return pcmk_rc_no_output;
+ }
+
+ out->begin_list(out, NULL, NULL, "Resource Checks");
+
+ if (pcmk_is_set(checks->flags, rsc_remain_stopped)) {
+ out->list_item(out, "check", "Configuration specifies '%s' should remain stopped",
+ parent->id);
+ }
+
+ if (pcmk_is_set(checks->flags, rsc_unpromotable)) {
+ out->list_item(out, "check", "Configuration specifies '%s' should not be promoted",
+ parent->id);
+ }
+
+ if (pcmk_is_set(checks->flags, rsc_unmanaged)) {
+ out->list_item(out, "check", "Configuration prevents cluster from stopping or starting unmanaged '%s'",
+ parent->id);
+ }
+
+ if (pcmk_is_set(checks->flags, rsc_locked)) {
+ out->list_item(out, "check", "'%s' is locked to node %s due to shutdown",
+ parent->id, checks->lock_node);
+ }
+
+ if (pcmk_is_set(checks->flags, rsc_node_health)) {
+ out->list_item(out, "check",
+ "'%s' cannot run on unhealthy nodes due to "
+ PCMK__OPT_NODE_HEALTH_STRATEGY "='%s'",
+ parent->id,
+ pe_pref(checks->rsc->cluster->config_hash,
+ PCMK__OPT_NODE_HEALTH_STRATEGY));
+ }
+
+ out->end_list(out);
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("resource-check-list", "resource_checks_t *")
+static int
+resource_check_list_xml(pcmk__output_t *out, va_list args) {
+ resource_checks_t *checks = va_arg(args, resource_checks_t *);
+
+ const pe_resource_t *parent = pe__const_top_resource(checks->rsc, false);
+
+ xmlNodePtr node = pcmk__output_create_xml_node(out, "check",
+ "id", parent->id,
+ NULL);
+
+ if (pcmk_is_set(checks->flags, rsc_remain_stopped)) {
+ pcmk__xe_set_bool_attr(node, "remain_stopped", true);
+ }
+
+ if (pcmk_is_set(checks->flags, rsc_unpromotable)) {
+ pcmk__xe_set_bool_attr(node, "promotable", false);
+ }
+
+ if (pcmk_is_set(checks->flags, rsc_unmanaged)) {
+ pcmk__xe_set_bool_attr(node, "unmanaged", true);
+ }
+
+ if (pcmk_is_set(checks->flags, rsc_locked)) {
+ crm_xml_add(node, "locked-to", checks->lock_node);
+ }
+
+ if (pcmk_is_set(checks->flags, rsc_node_health)) {
+ pcmk__xe_set_bool_attr(node, "unhealthy", true);
+ }
+
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("resource-search-list", "GList *", "const char *")
+static int
+resource_search_list_default(pcmk__output_t *out, va_list args)
+{
+ GList *nodes = va_arg(args, GList *);
+ const char *requested_name = va_arg(args, const char *);
+
+ bool printed = false;
+ int rc = pcmk_rc_no_output;
+
+ if (!out->is_quiet(out) && nodes == NULL) {
+ out->err(out, "resource %s is NOT running", requested_name);
+ return rc;
+ }
+
+ for (GList *lpc = nodes; lpc != NULL; lpc = lpc->next) {
+ node_info_t *ni = (node_info_t *) lpc->data;
+
+ if (!printed) {
+ out->begin_list(out, NULL, NULL, "Nodes");
+ printed = true;
+ rc = pcmk_rc_ok;
+ }
+
+ if (out->is_quiet(out)) {
+ out->list_item(out, "node", "%s", ni->node_name);
+ } else {
+ const char *role_text = "";
+
+ if (ni->promoted) {
+#ifdef PCMK__COMPAT_2_0
+ role_text = " " RSC_ROLE_PROMOTED_LEGACY_S;
+#else
+ role_text = " " RSC_ROLE_PROMOTED_S;
+#endif
+ }
+ out->list_item(out, "node", "resource %s is running on: %s%s",
+ requested_name, ni->node_name, role_text);
+ }
+ }
+
+ if (printed) {
+ out->end_list(out);
+ }
+
+ return rc;
+}
+
+PCMK__OUTPUT_ARGS("resource-search-list", "GList *", "const char *")
+static int
+resource_search_list_xml(pcmk__output_t *out, va_list args)
+{
+ GList *nodes = va_arg(args, GList *);
+ const char *requested_name = va_arg(args, const char *);
+
+ pcmk__output_xml_create_parent(out, "nodes",
+ "resource", requested_name,
+ NULL);
+
+ for (GList *lpc = nodes; lpc != NULL; lpc = lpc->next) {
+ node_info_t *ni = (node_info_t *) lpc->data;
+ xmlNodePtr sub_node = pcmk__output_create_xml_text_node(out, "node", ni->node_name);
+
+ if (ni->promoted) {
+ crm_xml_add(sub_node, "state", "promoted");
+ }
+ }
+
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("resource-reasons-list", "GList *", "pe_resource_t *",
+ "pe_node_t *")
+static int
+resource_reasons_list_default(pcmk__output_t *out, va_list args)
+{
+ GList *resources = va_arg(args, GList *);
+ pe_resource_t *rsc = va_arg(args, pe_resource_t *);
+ pe_node_t *node = va_arg(args, pe_node_t *);
+
+ const char *host_uname = (node == NULL)? NULL : node->details->uname;
+
+ out->begin_list(out, NULL, NULL, "Resource Reasons");
+
+ if ((rsc == NULL) && (host_uname == NULL)) {
+ GList *lpc = NULL;
+ GList *hosts = NULL;
+
+ for (lpc = resources; lpc != NULL; lpc = lpc->next) {
+ pe_resource_t *rsc = (pe_resource_t *) lpc->data;
+ rsc->fns->location(rsc, &hosts, TRUE);
+
+ if (hosts == NULL) {
+ out->list_item(out, "reason", "Resource %s is not running", rsc->id);
+ } else {
+ out->list_item(out, "reason", "Resource %s is running", rsc->id);
+ }
+
+ cli_resource_check(out, rsc, NULL);
+ g_list_free(hosts);
+ hosts = NULL;
+ }
+
+ } else if ((rsc != NULL) && (host_uname != NULL)) {
+ if (resource_is_running_on(rsc, host_uname)) {
+ out->list_item(out, "reason", "Resource %s is running on host %s",
+ rsc->id, host_uname);
+ } else {
+ out->list_item(out, "reason", "Resource %s is not running on host %s",
+ rsc->id, host_uname);
+ }
+
+ cli_resource_check(out, rsc, node);
+
+ } else if ((rsc == NULL) && (host_uname != NULL)) {
+ const char* host_uname = node->details->uname;
+ GList *allResources = node->details->allocated_rsc;
+ GList *activeResources = node->details->running_rsc;
+ GList *unactiveResources = pcmk__subtract_lists(allResources, activeResources, (GCompareFunc) strcmp);
+ GList *lpc = NULL;
+
+ for (lpc = activeResources; lpc != NULL; lpc = lpc->next) {
+ pe_resource_t *rsc = (pe_resource_t *) lpc->data;
+ out->list_item(out, "reason", "Resource %s is running on host %s",
+ rsc->id, host_uname);
+ cli_resource_check(out, rsc, node);
+ }
+
+ for(lpc = unactiveResources; lpc != NULL; lpc = lpc->next) {
+ pe_resource_t *rsc = (pe_resource_t *) lpc->data;
+ out->list_item(out, "reason", "Resource %s is assigned to host %s but not running",
+ rsc->id, host_uname);
+ cli_resource_check(out, rsc, node);
+ }
+
+ g_list_free(allResources);
+ g_list_free(activeResources);
+ g_list_free(unactiveResources);
+
+ } else if ((rsc != NULL) && (host_uname == NULL)) {
+ GList *hosts = NULL;
+
+ rsc->fns->location(rsc, &hosts, TRUE);
+ out->list_item(out, "reason", "Resource %s is %srunning",
+ rsc->id, (hosts? "" : "not "));
+ cli_resource_check(out, rsc, NULL);
+ g_list_free(hosts);
+ }
+
+ out->end_list(out);
+ return pcmk_rc_ok;
+}
+
+PCMK__OUTPUT_ARGS("resource-reasons-list", "GList *", "pe_resource_t *",
+ "pe_node_t *")
+static int
+resource_reasons_list_xml(pcmk__output_t *out, va_list args)
+{
+ GList *resources = va_arg(args, GList *);
+ pe_resource_t *rsc = va_arg(args, pe_resource_t *);
+ pe_node_t *node = va_arg(args, pe_node_t *);
+
+ const char *host_uname = (node == NULL)? NULL : node->details->uname;
+
+ xmlNodePtr xml_node = pcmk__output_xml_create_parent(out, "reason", NULL);
+
+ if ((rsc == NULL) && (host_uname == NULL)) {
+ GList *lpc = NULL;
+ GList *hosts = NULL;
+
+ pcmk__output_xml_create_parent(out, "resources", NULL);
+
+ for (lpc = resources; lpc != NULL; lpc = lpc->next) {
+ pe_resource_t *rsc = (pe_resource_t *) lpc->data;
+
+ rsc->fns->location(rsc, &hosts, TRUE);
+
+ pcmk__output_xml_create_parent(out, "resource",
+ "id", rsc->id,
+ "running", pcmk__btoa(hosts != NULL),
+ NULL);
+
+ cli_resource_check(out, rsc, NULL);
+ pcmk__output_xml_pop_parent(out);
+ g_list_free(hosts);
+ hosts = NULL;
+ }
+
+ pcmk__output_xml_pop_parent(out);
+
+ } else if ((rsc != NULL) && (host_uname != NULL)) {
+ if (resource_is_running_on(rsc, host_uname)) {
+ crm_xml_add(xml_node, "running_on", host_uname);
+ }
+
+ cli_resource_check(out, rsc, node);
+
+ } else if ((rsc == NULL) && (host_uname != NULL)) {
+ const char* host_uname = node->details->uname;
+ GList *allResources = node->details->allocated_rsc;
+ GList *activeResources = node->details->running_rsc;
+ GList *unactiveResources = pcmk__subtract_lists(allResources, activeResources, (GCompareFunc) strcmp);
+ GList *lpc = NULL;
+
+ pcmk__output_xml_create_parent(out, "resources", NULL);
+
+ for (lpc = activeResources; lpc != NULL; lpc = lpc->next) {
+ pe_resource_t *rsc = (pe_resource_t *) lpc->data;
+
+ pcmk__output_xml_create_parent(out, "resource",
+ "id", rsc->id,
+ "running", "true",
+ "host", host_uname,
+ NULL);
+
+ cli_resource_check(out, rsc, node);
+ pcmk__output_xml_pop_parent(out);
+ }
+
+ for(lpc = unactiveResources; lpc != NULL; lpc = lpc->next) {
+ pe_resource_t *rsc = (pe_resource_t *) lpc->data;
+
+ pcmk__output_xml_create_parent(out, "resource",
+ "id", rsc->id,
+ "running", "false",
+ "host", host_uname,
+ NULL);
+
+ cli_resource_check(out, rsc, node);
+ pcmk__output_xml_pop_parent(out);
+ }
+
+ pcmk__output_xml_pop_parent(out);
+ g_list_free(allResources);
+ g_list_free(activeResources);
+ g_list_free(unactiveResources);
+
+ } else if ((rsc != NULL) && (host_uname == NULL)) {
+ GList *hosts = NULL;
+
+ rsc->fns->location(rsc, &hosts, TRUE);
+ crm_xml_add(xml_node, "running", pcmk__btoa(hosts != NULL));
+ cli_resource_check(out, rsc, NULL);
+ g_list_free(hosts);
+ }
+
+ return pcmk_rc_ok;
+}
+
+static void
+add_resource_name(pe_resource_t *rsc, pcmk__output_t *out) {
+ if (rsc->children == NULL) {
+ out->list_item(out, "resource", "%s", rsc->id);
+ } else {
+ g_list_foreach(rsc->children, (GFunc) add_resource_name, out);
+ }
+}
+
+PCMK__OUTPUT_ARGS("resource-names-list", "GList *")
+static int
+resource_names(pcmk__output_t *out, va_list args) {
+ GList *resources = va_arg(args, GList *);
+
+ if (resources == NULL) {
+ out->err(out, "NO resources configured\n");
+ return pcmk_rc_no_output;
+ }
+
+ out->begin_list(out, NULL, NULL, "Resource Names");
+ g_list_foreach(resources, (GFunc) add_resource_name, out);
+ out->end_list(out);
+ return pcmk_rc_ok;
+}
+
+static pcmk__message_entry_t fmt_functions[] = {
+ { "agent-status", "default", agent_status_default },
+ { "agent-status", "xml", agent_status_xml },
+ { "attribute-list", "default", attribute_list_default },
+ { "attribute-list", "text", attribute_list_text },
+ { "override", "default", override_default },
+ { "override", "xml", override_xml },
+ { "property-list", "default", property_list_default },
+ { "property-list", "text", property_list_text },
+ { "resource-agent-action", "default", resource_agent_action_default },
+ { "resource-agent-action", "xml", resource_agent_action_xml },
+ { "resource-check-list", "default", resource_check_list_default },
+ { "resource-check-list", "xml", resource_check_list_xml },
+ { "resource-search-list", "default", resource_search_list_default },
+ { "resource-search-list", "xml", resource_search_list_xml },
+ { "resource-reasons-list", "default", resource_reasons_list_default },
+ { "resource-reasons-list", "xml", resource_reasons_list_xml },
+ { "resource-names-list", "default", resource_names },
+
+ { NULL, NULL, NULL }
+};
+
+void
+crm_resource_register_messages(pcmk__output_t *out) {
+ pcmk__register_messages(out, fmt_functions);
+}
diff --git a/tools/crm_resource_runtime.c b/tools/crm_resource_runtime.c
new file mode 100644
index 0000000..f25dbbc
--- /dev/null
+++ b/tools/crm_resource_runtime.c
@@ -0,0 +1,2178 @@
+/*
+ * Copyright 2004-2023 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 <crm_resource.h>
+#include <crm/common/ipc_attrd_internal.h>
+#include <crm/common/ipc_controld.h>
+#include <crm/common/lists_internal.h>
+#include <crm/services_internal.h>
+
+static GList *
+build_node_info_list(const pe_resource_t *rsc)
+{
+ GList *retval = NULL;
+
+ for (const GList *iter = rsc->children; iter != NULL; iter = iter->next) {
+ const pe_resource_t *child = (const pe_resource_t *) iter->data;
+
+ for (const GList *iter2 = child->running_on;
+ iter2 != NULL; iter2 = iter2->next) {
+
+ const pe_node_t *node = (const pe_node_t *) iter2->data;
+ node_info_t *ni = calloc(1, sizeof(node_info_t));
+
+ ni->node_name = node->details->uname;
+ ni->promoted = pcmk_is_set(rsc->flags, pe_rsc_promotable) &&
+ child->fns->state(child, TRUE) == RSC_ROLE_PROMOTED;
+
+ retval = g_list_prepend(retval, ni);
+ }
+ }
+
+ return retval;
+}
+
+GList *
+cli_resource_search(pe_resource_t *rsc, const char *requested_name,
+ pe_working_set_t *data_set)
+{
+ GList *retval = NULL;
+ const pe_resource_t *parent = pe__const_top_resource(rsc, false);
+
+ if (pe_rsc_is_clone(rsc)) {
+ retval = build_node_info_list(rsc);
+
+ /* The anonymous clone children's common ID is supplied */
+ } else if (pe_rsc_is_clone(parent)
+ && !pcmk_is_set(rsc->flags, pe_rsc_unique)
+ && rsc->clone_name
+ && pcmk__str_eq(requested_name, rsc->clone_name, pcmk__str_casei)
+ && !pcmk__str_eq(requested_name, rsc->id, pcmk__str_casei)) {
+
+ retval = build_node_info_list(parent);
+
+ } else if (rsc->running_on != NULL) {
+ for (GList *iter = rsc->running_on; iter != NULL; iter = iter->next) {
+ pe_node_t *node = (pe_node_t *) iter->data;
+ node_info_t *ni = calloc(1, sizeof(node_info_t));
+ ni->node_name = node->details->uname;
+ ni->promoted = (rsc->fns->state(rsc, TRUE) == RSC_ROLE_PROMOTED);
+
+ retval = g_list_prepend(retval, ni);
+ }
+ }
+
+ return retval;
+}
+
+// \return Standard Pacemaker return code
+static int
+find_resource_attr(pcmk__output_t *out, cib_t * the_cib, const char *attr,
+ const char *rsc, const char *attr_set_type, const char *set_name,
+ const char *attr_id, const char *attr_name, char **value)
+{
+ int rc = pcmk_rc_ok;
+ xmlNode *xml_search = NULL;
+ GString *xpath = NULL;
+ const char *xpath_base = NULL;
+
+ if(value) {
+ *value = NULL;
+ }
+
+ if(the_cib == NULL) {
+ return ENOTCONN;
+ }
+
+ xpath_base = pcmk_cib_xpath_for(XML_CIB_TAG_RESOURCES);
+ if (xpath_base == NULL) {
+ crm_err(XML_CIB_TAG_RESOURCES " CIB element not known (bug?)");
+ return ENOMSG;
+ }
+
+ xpath = g_string_sized_new(1024);
+ pcmk__g_strcat(xpath,
+ xpath_base, "//*[@" XML_ATTR_ID "=\"", rsc, "\"]", NULL);
+
+ if (attr_set_type != NULL) {
+ pcmk__g_strcat(xpath, "/", attr_set_type, NULL);
+ if (set_name != NULL) {
+ pcmk__g_strcat(xpath, "[@" XML_ATTR_ID "=\"", set_name, "\"]",
+ NULL);
+ }
+ }
+
+ g_string_append(xpath, "//" XML_CIB_TAG_NVPAIR "[");
+ if (attr_id != NULL) {
+ pcmk__g_strcat(xpath, "@" XML_ATTR_ID "=\"", attr_id, "\"", NULL);
+ }
+
+ if (attr_name != NULL) {
+ if (attr_id != NULL) {
+ g_string_append(xpath, " and ");
+ }
+ pcmk__g_strcat(xpath, "@" XML_NVPAIR_ATTR_NAME "=\"", attr_name, "\"",
+ NULL);
+ }
+ g_string_append_c(xpath, ']');
+
+ rc = the_cib->cmds->query(the_cib, (const char *) xpath->str, &xml_search,
+ cib_sync_call | cib_scope_local | cib_xpath);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ goto done;
+ }
+
+ crm_log_xml_debug(xml_search, "Match");
+ if (xml_has_children(xml_search)) {
+ xmlNode *child = NULL;
+
+ rc = ENOTUNIQ;
+ out->info(out, "Multiple attributes match name=%s", attr_name);
+
+ for (child = pcmk__xml_first_child(xml_search); child != NULL;
+ child = pcmk__xml_next(child)) {
+ out->info(out, " Value: %s \t(id=%s)",
+ crm_element_value(child, XML_NVPAIR_ATTR_VALUE), ID(child));
+ }
+
+ out->spacer(out);
+
+ } else if(value) {
+ pcmk__str_update(value, crm_element_value(xml_search, attr));
+ }
+
+ done:
+ g_string_free(xpath, TRUE);
+ free_xml(xml_search);
+ return rc;
+}
+
+/* PRIVATE. Use the find_matching_attr_resources instead. */
+static void
+find_matching_attr_resources_recursive(pcmk__output_t *out, GList/* <pe_resource_t*> */ ** result,
+ pe_resource_t * rsc, const char * rsc_id,
+ const char * attr_set, const char * attr_set_type,
+ const char * attr_id, const char * attr_name,
+ cib_t * cib, const char * cmd, int depth)
+{
+ int rc = pcmk_rc_ok;
+ char *lookup_id = clone_strip(rsc->id);
+ char *local_attr_id = NULL;
+
+ /* visit the children */
+ for(GList *gIter = rsc->children; gIter; gIter = gIter->next) {
+ find_matching_attr_resources_recursive(out, result, (pe_resource_t*)gIter->data,
+ rsc_id, attr_set, attr_set_type,
+ attr_id, attr_name, cib, cmd, depth+1);
+ /* do it only once for clones */
+ if(pe_clone == rsc->variant) {
+ break;
+ }
+ }
+
+ rc = find_resource_attr(out, cib, XML_ATTR_ID, lookup_id, attr_set_type,
+ attr_set, attr_id, attr_name, &local_attr_id);
+ /* Post-order traversal.
+ * The root is always on the list and it is the last item. */
+ if((0 == depth) || (pcmk_rc_ok == rc)) {
+ /* push the head */
+ *result = g_list_append(*result, rsc);
+ }
+
+ free(local_attr_id);
+ free(lookup_id);
+}
+
+
+/* The result is a linearized pre-ordered tree of resources. */
+static GList/*<pe_resource_t*>*/ *
+find_matching_attr_resources(pcmk__output_t *out, pe_resource_t * rsc,
+ const char * rsc_id, const char * attr_set,
+ const char * attr_set_type, const char * attr_id,
+ const char * attr_name, cib_t * cib, const char * cmd,
+ gboolean force)
+{
+ int rc = pcmk_rc_ok;
+ char *lookup_id = NULL;
+ char *local_attr_id = NULL;
+ GList * result = NULL;
+ /* If --force is used, update only the requested resource (clone or primitive).
+ * Otherwise, if the primitive has the attribute, use that.
+ * Otherwise use the clone. */
+ if(force == TRUE) {
+ return g_list_append(result, rsc);
+ }
+ if(rsc->parent && pe_clone == rsc->parent->variant) {
+ int rc = pcmk_rc_ok;
+ char *local_attr_id = NULL;
+ rc = find_resource_attr(out, cib, XML_ATTR_ID, rsc_id, attr_set_type,
+ attr_set, attr_id, attr_name, &local_attr_id);
+ free(local_attr_id);
+
+ if(rc != pcmk_rc_ok) {
+ rsc = rsc->parent;
+ out->info(out, "Performing %s of '%s' on '%s', the parent of '%s'",
+ cmd, attr_name, rsc->id, rsc_id);
+ }
+ return g_list_append(result, rsc);
+ } else if(rsc->parent == NULL && rsc->children && pe_clone == rsc->variant) {
+ pe_resource_t *child = rsc->children->data;
+
+ if(child->variant == pe_native) {
+ lookup_id = clone_strip(child->id); /* Could be a cloned group! */
+ rc = find_resource_attr(out, cib, XML_ATTR_ID, lookup_id, attr_set_type,
+ attr_set, attr_id, attr_name, &local_attr_id);
+
+ if(rc == pcmk_rc_ok) {
+ rsc = child;
+ out->info(out, "A value for '%s' already exists in child '%s', performing %s on that instead of '%s'",
+ attr_name, lookup_id, cmd, rsc_id);
+ }
+
+ free(local_attr_id);
+ free(lookup_id);
+ }
+ return g_list_append(result, rsc);
+ }
+ /* If the resource is a group ==> children inherit the attribute if defined. */
+ find_matching_attr_resources_recursive(out, &result, rsc, rsc_id, attr_set,
+ attr_set_type, attr_id, attr_name,
+ cib, cmd, 0);
+ return result;
+}
+
+// \return Standard Pacemaker return code
+int
+cli_resource_update_attribute(pe_resource_t *rsc, const char *requested_name,
+ const char *attr_set, const char *attr_set_type,
+ const char *attr_id, const char *attr_name,
+ const char *attr_value, gboolean recursive,
+ cib_t *cib, int cib_options, gboolean force)
+{
+ pcmk__output_t *out = rsc->cluster->priv;
+ int rc = pcmk_rc_ok;
+
+ char *found_attr_id = NULL;
+
+ GList/*<pe_resource_t*>*/ *resources = NULL;
+ const char *top_id = pe__const_top_resource(rsc, false)->id;
+
+ if ((attr_id == NULL) && !force) {
+ find_resource_attr(out, cib, XML_ATTR_ID, top_id, NULL, NULL, NULL,
+ attr_name, NULL);
+ }
+
+ if (pcmk__str_eq(attr_set_type, XML_TAG_ATTR_SETS, pcmk__str_casei)) {
+ if (!force) {
+ rc = find_resource_attr(out, cib, XML_ATTR_ID, top_id,
+ XML_TAG_META_SETS, attr_set, attr_id,
+ attr_name, &found_attr_id);
+ if ((rc == pcmk_rc_ok) && !out->is_quiet(out)) {
+ out->err(out,
+ "WARNING: There is already a meta attribute "
+ "for '%s' called '%s' (id=%s)",
+ top_id, attr_name, found_attr_id);
+ out->err(out,
+ " Delete '%s' first or use the force option "
+ "to override", found_attr_id);
+ }
+ free(found_attr_id);
+ if (rc == pcmk_rc_ok) {
+ return ENOTUNIQ;
+ }
+ }
+ resources = g_list_append(resources, rsc);
+
+ } else if (pcmk__str_eq(attr_set_type, ATTR_SET_ELEMENT, pcmk__str_none)) {
+ crm_xml_add(rsc->xml, attr_name, attr_value);
+ CRM_ASSERT(cib != NULL);
+ rc = cib->cmds->replace(cib, XML_CIB_TAG_RESOURCES, rsc->xml,
+ cib_options);
+ rc = pcmk_legacy2rc(rc);
+ if (rc == pcmk_rc_ok) {
+ out->info(out, "Set attribute: name=%s value=%s",
+ attr_name, attr_value);
+ }
+ return rc;
+
+ } else {
+ resources = find_matching_attr_resources(out, rsc, requested_name,
+ attr_set, attr_set_type,
+ attr_id, attr_name, cib,
+ "update", force);
+ }
+
+ /* If the user specified attr_set or attr_id, the intent is to modify a
+ * single resource, which will be the last item in the list.
+ */
+ if ((attr_set != NULL) || (attr_id != NULL)) {
+ GList *last = g_list_last(resources);
+
+ resources = g_list_remove_link(resources, last);
+ g_list_free(resources);
+ resources = last;
+ }
+
+ for (GList *iter = resources; iter != NULL; iter = iter->next) {
+ char *lookup_id = NULL;
+ char *local_attr_set = NULL;
+ const char *rsc_attr_id = attr_id;
+ const char *rsc_attr_set = attr_set;
+
+ xmlNode *xml_top = NULL;
+ xmlNode *xml_obj = NULL;
+ found_attr_id = NULL;
+
+ rsc = (pe_resource_t *) iter->data;
+
+ lookup_id = clone_strip(rsc->id); /* Could be a cloned group! */
+ rc = find_resource_attr(out, cib, XML_ATTR_ID, lookup_id, attr_set_type,
+ attr_set, attr_id, attr_name, &found_attr_id);
+
+ switch (rc) {
+ case pcmk_rc_ok:
+ crm_debug("Found a match for name=%s: id=%s",
+ attr_name, found_attr_id);
+ rsc_attr_id = found_attr_id;
+ break;
+
+ case ENXIO:
+ if (rsc_attr_set == NULL) {
+ local_attr_set = crm_strdup_printf("%s-%s", lookup_id,
+ attr_set_type);
+ rsc_attr_set = local_attr_set;
+ }
+ if (rsc_attr_id == NULL) {
+ found_attr_id = crm_strdup_printf("%s-%s",
+ rsc_attr_set, attr_name);
+ rsc_attr_id = found_attr_id;
+ }
+
+ xml_top = create_xml_node(NULL, crm_element_name(rsc->xml));
+ crm_xml_add(xml_top, XML_ATTR_ID, lookup_id);
+
+ xml_obj = create_xml_node(xml_top, attr_set_type);
+ crm_xml_add(xml_obj, XML_ATTR_ID, rsc_attr_set);
+ break;
+
+ default:
+ free(lookup_id);
+ free(found_attr_id);
+ g_list_free(resources);
+ return rc;
+ }
+
+ xml_obj = crm_create_nvpair_xml(xml_obj, rsc_attr_id, attr_name,
+ attr_value);
+ if (xml_top == NULL) {
+ xml_top = xml_obj;
+ }
+
+ crm_log_xml_debug(xml_top, "Update");
+
+ rc = cib->cmds->modify(cib, XML_CIB_TAG_RESOURCES, xml_top,
+ cib_options);
+ rc = pcmk_legacy2rc(rc);
+ if (rc == pcmk_rc_ok) {
+ out->info(out, "Set '%s' option: id=%s%s%s%s%s value=%s",
+ lookup_id, found_attr_id,
+ ((rsc_attr_set == NULL)? "" : " set="),
+ pcmk__s(rsc_attr_set, ""),
+ ((attr_name == NULL)? "" : " name="),
+ pcmk__s(attr_name, ""), attr_value);
+ }
+
+ free_xml(xml_top);
+
+ free(lookup_id);
+ free(found_attr_id);
+ free(local_attr_set);
+
+ if (recursive
+ && pcmk__str_eq(attr_set_type, XML_TAG_META_SETS,
+ pcmk__str_casei)) {
+ GList *lpc = NULL;
+ static bool need_init = true;
+
+ if (need_init) {
+ need_init = false;
+ pcmk__unpack_constraints(rsc->cluster);
+ pe__clear_resource_flags_on_all(rsc->cluster,
+ pe_rsc_detect_loop);
+ }
+
+ /* We want to set the attribute only on resources explicitly
+ * colocated with this one, so we use rsc->rsc_cons_lhs directly
+ * rather than the with_this_colocations() method.
+ */
+ pe__set_resource_flags(rsc, pe_rsc_detect_loop);
+ for (lpc = rsc->rsc_cons_lhs; lpc != NULL; lpc = lpc->next) {
+ pcmk__colocation_t *cons = (pcmk__colocation_t *) lpc->data;
+
+ crm_debug("Checking %s %d", cons->id, cons->score);
+ if (!pcmk_is_set(cons->dependent->flags, pe_rsc_detect_loop)
+ && (cons->score > 0)) {
+ crm_debug("Setting %s=%s for dependent resource %s",
+ attr_name, attr_value, cons->dependent->id);
+ cli_resource_update_attribute(cons->dependent,
+ cons->dependent->id, NULL,
+ attr_set_type, NULL,
+ attr_name, attr_value,
+ recursive, cib, cib_options,
+ force);
+ }
+ }
+ }
+ }
+ g_list_free(resources);
+ return rc;
+}
+
+// \return Standard Pacemaker return code
+int
+cli_resource_delete_attribute(pe_resource_t *rsc, const char *requested_name,
+ const char *attr_set, const char *attr_set_type,
+ const char *attr_id, const char *attr_name,
+ cib_t *cib, int cib_options, gboolean force)
+{
+ pcmk__output_t *out = rsc->cluster->priv;
+ int rc = pcmk_rc_ok;
+ GList/*<pe_resource_t*>*/ *resources = NULL;
+
+ if ((attr_id == NULL) && !force) {
+ find_resource_attr(out, cib, XML_ATTR_ID,
+ pe__const_top_resource(rsc, false)->id, NULL,
+ NULL, NULL, attr_name, NULL);
+ }
+
+ if (pcmk__str_eq(attr_set_type, XML_TAG_META_SETS, pcmk__str_casei)) {
+ resources = find_matching_attr_resources(out, rsc, requested_name,
+ attr_set, attr_set_type,
+ attr_id, attr_name, cib,
+ "delete", force);
+
+ } else if (pcmk__str_eq(attr_set_type, ATTR_SET_ELEMENT, pcmk__str_none)) {
+ xml_remove_prop(rsc->xml, attr_name);
+ CRM_ASSERT(cib != NULL);
+ rc = cib->cmds->replace(cib, XML_CIB_TAG_RESOURCES, rsc->xml,
+ cib_options);
+ rc = pcmk_legacy2rc(rc);
+ if (rc == pcmk_rc_ok) {
+ out->info(out, "Deleted attribute: %s", attr_name);
+ }
+ return rc;
+
+ } else {
+ resources = g_list_append(resources, rsc);
+ }
+
+ for (GList *iter = resources; iter != NULL; iter = iter->next) {
+ char *lookup_id = NULL;
+ xmlNode *xml_obj = NULL;
+ char *found_attr_id = NULL;
+ const char *rsc_attr_id = attr_id;
+
+ rsc = (pe_resource_t *) iter->data;
+
+ lookup_id = clone_strip(rsc->id);
+ rc = find_resource_attr(out, cib, XML_ATTR_ID, lookup_id, attr_set_type,
+ attr_set, attr_id, attr_name, &found_attr_id);
+ switch (rc) {
+ case pcmk_rc_ok:
+ break;
+
+ case ENXIO:
+ free(lookup_id);
+ rc = pcmk_rc_ok;
+ continue;
+
+ default:
+ free(lookup_id);
+ g_list_free(resources);
+ return rc;
+ }
+
+ if (rsc_attr_id == NULL) {
+ rsc_attr_id = found_attr_id;
+ }
+
+ xml_obj = crm_create_nvpair_xml(NULL, rsc_attr_id, attr_name, NULL);
+ crm_log_xml_debug(xml_obj, "Delete");
+
+ CRM_ASSERT(cib);
+ rc = cib->cmds->remove(cib, XML_CIB_TAG_RESOURCES, xml_obj,
+ cib_options);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc == pcmk_rc_ok) {
+ out->info(out, "Deleted '%s' option: id=%s%s%s%s%s",
+ lookup_id, found_attr_id,
+ ((attr_set == NULL)? "" : " set="),
+ pcmk__s(attr_set, ""),
+ ((attr_name == NULL)? "" : " name="),
+ pcmk__s(attr_name, ""));
+ }
+
+ free(lookup_id);
+ free_xml(xml_obj);
+ free(found_attr_id);
+ }
+ g_list_free(resources);
+ return rc;
+}
+
+// \return Standard Pacemaker return code
+static int
+send_lrm_rsc_op(pcmk_ipc_api_t *controld_api, bool do_fail_resource,
+ const char *host_uname, const char *rsc_id, pe_working_set_t *data_set)
+{
+ pcmk__output_t *out = data_set->priv;
+ const char *router_node = host_uname;
+ const char *rsc_api_id = NULL;
+ const char *rsc_long_id = NULL;
+ const char *rsc_class = NULL;
+ const char *rsc_provider = NULL;
+ const char *rsc_type = NULL;
+ bool cib_only = false;
+ pe_resource_t *rsc = pe_find_resource(data_set->resources, rsc_id);
+
+ if (rsc == NULL) {
+ out->err(out, "Resource %s not found", rsc_id);
+ return ENXIO;
+
+ } else if (rsc->variant != pe_native) {
+ out->err(out, "We can only process primitive resources, not %s", rsc_id);
+ return EINVAL;
+ }
+
+ rsc_class = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS);
+ rsc_provider = crm_element_value(rsc->xml, XML_AGENT_ATTR_PROVIDER),
+ rsc_type = crm_element_value(rsc->xml, XML_ATTR_TYPE);
+ if ((rsc_class == NULL) || (rsc_type == NULL)) {
+ out->err(out, "Resource %s does not have a class and type", rsc_id);
+ return EINVAL;
+ }
+
+ {
+ pe_node_t *node = pe_find_node(data_set->nodes, host_uname);
+
+ if (node == NULL) {
+ out->err(out, "Node %s not found", host_uname);
+ return pcmk_rc_node_unknown;
+ }
+
+ if (!(node->details->online)) {
+ if (do_fail_resource) {
+ out->err(out, "Node %s is not online", host_uname);
+ return ENOTCONN;
+ } else {
+ cib_only = true;
+ }
+ }
+ if (!cib_only && pe__is_guest_or_remote_node(node)) {
+ node = pe__current_node(node->details->remote_rsc);
+ if (node == NULL) {
+ out->err(out, "No cluster connection to Pacemaker Remote node %s detected",
+ host_uname);
+ return ENOTCONN;
+ }
+ router_node = node->details->uname;
+ }
+ }
+
+ if (rsc->clone_name) {
+ rsc_api_id = rsc->clone_name;
+ rsc_long_id = rsc->id;
+ } else {
+ rsc_api_id = rsc->id;
+ }
+ if (do_fail_resource) {
+ return pcmk_controld_api_fail(controld_api, host_uname, router_node,
+ rsc_api_id, rsc_long_id,
+ rsc_class, rsc_provider, rsc_type);
+ } else {
+ return pcmk_controld_api_refresh(controld_api, host_uname, router_node,
+ rsc_api_id, rsc_long_id, rsc_class,
+ rsc_provider, rsc_type, cib_only);
+ }
+}
+
+/*!
+ * \internal
+ * \brief Get resource name as used in failure-related node attributes
+ *
+ * \param[in] rsc Resource to check
+ *
+ * \return Newly allocated string containing resource's fail name
+ * \note The caller is responsible for freeing the result.
+ */
+static inline char *
+rsc_fail_name(const pe_resource_t *rsc)
+{
+ const char *name = (rsc->clone_name? rsc->clone_name : rsc->id);
+
+ return pcmk_is_set(rsc->flags, pe_rsc_unique)? strdup(name) : clone_strip(name);
+}
+
+// \return Standard Pacemaker return code
+static int
+clear_rsc_history(pcmk_ipc_api_t *controld_api, const char *host_uname,
+ const char *rsc_id, pe_working_set_t *data_set)
+{
+ int rc = pcmk_rc_ok;
+
+ /* Erase the resource's entire LRM history in the CIB, even if we're only
+ * clearing a single operation's fail count. If we erased only entries for a
+ * single operation, we might wind up with a wrong idea of the current
+ * resource state, and we might not re-probe the resource.
+ */
+ rc = send_lrm_rsc_op(controld_api, false, host_uname, rsc_id, data_set);
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
+
+ crm_trace("Processing %d mainloop inputs",
+ pcmk_controld_api_replies_expected(controld_api));
+ while (g_main_context_iteration(NULL, FALSE)) {
+ crm_trace("Processed mainloop input, %d still remaining",
+ pcmk_controld_api_replies_expected(controld_api));
+ }
+ return rc;
+}
+
+// \return Standard Pacemaker return code
+static int
+clear_rsc_failures(pcmk__output_t *out, pcmk_ipc_api_t *controld_api,
+ const char *node_name, const char *rsc_id, const char *operation,
+ const char *interval_spec, pe_working_set_t *data_set)
+{
+ int rc = pcmk_rc_ok;
+ const char *failed_value = NULL;
+ const char *failed_id = NULL;
+ const char *interval_ms_s = NULL;
+ GHashTable *rscs = NULL;
+ GHashTableIter iter;
+
+ /* Create a hash table to use as a set of resources to clean. This lets us
+ * clean each resource only once (per node) regardless of how many failed
+ * operations it has.
+ */
+ rscs = pcmk__strkey_table(NULL, NULL);
+
+ // Normalize interval to milliseconds for comparison to history entry
+ if (operation) {
+ interval_ms_s = crm_strdup_printf("%u",
+ crm_parse_interval_spec(interval_spec));
+ }
+
+ for (xmlNode *xml_op = pcmk__xml_first_child(data_set->failed);
+ xml_op != NULL;
+ xml_op = pcmk__xml_next(xml_op)) {
+
+ failed_id = crm_element_value(xml_op, XML_LRM_ATTR_RSCID);
+ if (failed_id == NULL) {
+ // Malformed history entry, should never happen
+ continue;
+ }
+
+ // No resource specified means all resources match
+ if (rsc_id) {
+ pe_resource_t *fail_rsc = pe_find_resource_with_flags(data_set->resources,
+ failed_id,
+ pe_find_renamed|pe_find_anon);
+
+ if (!fail_rsc || !pcmk__str_eq(rsc_id, fail_rsc->id, pcmk__str_casei)) {
+ continue;
+ }
+ }
+
+ // Host name should always have been provided by this point
+ failed_value = crm_element_value(xml_op, XML_ATTR_UNAME);
+ if (!pcmk__str_eq(node_name, failed_value, pcmk__str_casei)) {
+ continue;
+ }
+
+ // No operation specified means all operations match
+ if (operation) {
+ failed_value = crm_element_value(xml_op, XML_LRM_ATTR_TASK);
+ if (!pcmk__str_eq(operation, failed_value, pcmk__str_casei)) {
+ continue;
+ }
+
+ // Interval (if operation was specified) defaults to 0 (not all)
+ failed_value = crm_element_value(xml_op, XML_LRM_ATTR_INTERVAL_MS);
+ if (!pcmk__str_eq(interval_ms_s, failed_value, pcmk__str_casei)) {
+ continue;
+ }
+ }
+
+ g_hash_table_add(rscs, (gpointer) failed_id);
+ }
+
+ g_hash_table_iter_init(&iter, rscs);
+ while (g_hash_table_iter_next(&iter, (gpointer *) &failed_id, NULL)) {
+ crm_debug("Erasing failures of %s on %s", failed_id, node_name);
+ rc = clear_rsc_history(controld_api, node_name, failed_id, data_set);
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
+ }
+ g_hash_table_destroy(rscs);
+ return rc;
+}
+
+// \return Standard Pacemaker return code
+static int
+clear_rsc_fail_attrs(const pe_resource_t *rsc, const char *operation,
+ const char *interval_spec, const pe_node_t *node)
+{
+ int rc = pcmk_rc_ok;
+ int attr_options = pcmk__node_attr_none;
+ char *rsc_name = rsc_fail_name(rsc);
+
+ if (pe__is_guest_or_remote_node(node)) {
+ attr_options |= pcmk__node_attr_remote;
+ }
+
+ rc = pcmk__attrd_api_clear_failures(NULL, node->details->uname, rsc_name,
+ operation, interval_spec, NULL,
+ attr_options);
+ free(rsc_name);
+ return rc;
+}
+
+// \return Standard Pacemaker return code
+int
+cli_resource_delete(pcmk_ipc_api_t *controld_api, const char *host_uname,
+ const pe_resource_t *rsc, const char *operation,
+ const char *interval_spec, bool just_failures,
+ pe_working_set_t *data_set, gboolean force)
+{
+ pcmk__output_t *out = data_set->priv;
+ int rc = pcmk_rc_ok;
+ pe_node_t *node = NULL;
+
+ if (rsc == NULL) {
+ return ENXIO;
+
+ } else if (rsc->children) {
+
+ for (const GList *lpc = rsc->children; lpc != NULL; lpc = lpc->next) {
+ const pe_resource_t *child = (const pe_resource_t *) lpc->data;
+
+ rc = cli_resource_delete(controld_api, host_uname, child, operation,
+ interval_spec, just_failures, data_set, force);
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
+ }
+ return pcmk_rc_ok;
+
+ } else if (host_uname == NULL) {
+ GList *lpc = NULL;
+ GList *nodes = g_hash_table_get_values(rsc->known_on);
+
+ if(nodes == NULL && force) {
+ nodes = pcmk__copy_node_list(data_set->nodes, false);
+
+ } else if(nodes == NULL && rsc->exclusive_discover) {
+ GHashTableIter iter;
+ pe_node_t *node = NULL;
+
+ g_hash_table_iter_init(&iter, rsc->allowed_nodes);
+ while (g_hash_table_iter_next(&iter, NULL, (void**)&node)) {
+ if(node->weight >= 0) {
+ nodes = g_list_prepend(nodes, node);
+ }
+ }
+
+ } else if(nodes == NULL) {
+ nodes = g_hash_table_get_values(rsc->allowed_nodes);
+ }
+
+ for (lpc = nodes; lpc != NULL; lpc = lpc->next) {
+ node = (pe_node_t *) lpc->data;
+
+ if (node->details->online) {
+ rc = cli_resource_delete(controld_api, node->details->uname, rsc,
+ operation, interval_spec, just_failures,
+ data_set, force);
+ }
+ if (rc != pcmk_rc_ok) {
+ g_list_free(nodes);
+ return rc;
+ }
+ }
+
+ g_list_free(nodes);
+ return pcmk_rc_ok;
+ }
+
+ node = pe_find_node(data_set->nodes, host_uname);
+
+ if (node == NULL) {
+ out->err(out, "Unable to clean up %s because node %s not found",
+ rsc->id, host_uname);
+ return ENODEV;
+ }
+
+ if (!node->details->rsc_discovery_enabled) {
+ out->err(out, "Unable to clean up %s because resource discovery disabled on %s",
+ rsc->id, host_uname);
+ return EOPNOTSUPP;
+ }
+
+ if (controld_api == NULL) {
+ out->err(out, "Dry run: skipping clean-up of %s on %s due to CIB_file",
+ rsc->id, host_uname);
+ return pcmk_rc_ok;
+ }
+
+ rc = clear_rsc_fail_attrs(rsc, operation, interval_spec, node);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Unable to clean up %s failures on %s: %s",
+ rsc->id, host_uname, pcmk_rc_str(rc));
+ return rc;
+ }
+
+ if (just_failures) {
+ rc = clear_rsc_failures(out, controld_api, host_uname, rsc->id, operation,
+ interval_spec, data_set);
+ } else {
+ rc = clear_rsc_history(controld_api, host_uname, rsc->id, data_set);
+ }
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Cleaned %s failures on %s, but unable to clean history: %s",
+ rsc->id, host_uname, pcmk_strerror(rc));
+ } else {
+ out->info(out, "Cleaned up %s on %s", rsc->id, host_uname);
+ }
+ return rc;
+}
+
+// \return Standard Pacemaker return code
+int
+cli_cleanup_all(pcmk_ipc_api_t *controld_api, const char *node_name,
+ const char *operation, const char *interval_spec,
+ pe_working_set_t *data_set)
+{
+ pcmk__output_t *out = data_set->priv;
+ int rc = pcmk_rc_ok;
+ int attr_options = pcmk__node_attr_none;
+ const char *display_name = node_name? node_name : "all nodes";
+
+ if (controld_api == NULL) {
+ out->info(out, "Dry run: skipping clean-up of %s due to CIB_file",
+ display_name);
+ return rc;
+ }
+
+ if (node_name) {
+ pe_node_t *node = pe_find_node(data_set->nodes, node_name);
+
+ if (node == NULL) {
+ out->err(out, "Unknown node: %s", node_name);
+ return ENXIO;
+ }
+ if (pe__is_guest_or_remote_node(node)) {
+ attr_options |= pcmk__node_attr_remote;
+ }
+ }
+
+ rc = pcmk__attrd_api_clear_failures(NULL, node_name, NULL, operation,
+ interval_spec, NULL, attr_options);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Unable to clean up all failures on %s: %s",
+ display_name, pcmk_rc_str(rc));
+ return rc;
+ }
+
+ if (node_name) {
+ rc = clear_rsc_failures(out, controld_api, node_name, NULL,
+ operation, interval_spec, data_set);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Cleaned all resource failures on %s, but unable to clean history: %s",
+ node_name, pcmk_strerror(rc));
+ return rc;
+ }
+ } else {
+ for (GList *iter = data_set->nodes; iter; iter = iter->next) {
+ pe_node_t *node = (pe_node_t *) iter->data;
+
+ rc = clear_rsc_failures(out, controld_api, node->details->uname, NULL,
+ operation, interval_spec, data_set);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Cleaned all resource failures on all nodes, but unable to clean history: %s",
+ pcmk_strerror(rc));
+ return rc;
+ }
+ }
+ }
+
+ out->info(out, "Cleaned up all resources on %s", display_name);
+ return rc;
+}
+
+static void
+check_role(resource_checks_t *checks)
+{
+ const char *role_s = g_hash_table_lookup(checks->rsc->meta,
+ XML_RSC_ATTR_TARGET_ROLE);
+
+ if (role_s == NULL) {
+ return;
+ }
+ switch (text2role(role_s)) {
+ case RSC_ROLE_STOPPED:
+ checks->flags |= rsc_remain_stopped;
+ break;
+
+ case RSC_ROLE_UNPROMOTED:
+ if (pcmk_is_set(pe__const_top_resource(checks->rsc, false)->flags,
+ pe_rsc_promotable)) {
+ checks->flags |= rsc_unpromotable;
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+static void
+check_managed(resource_checks_t *checks)
+{
+ const char *managed_s = g_hash_table_lookup(checks->rsc->meta,
+ XML_RSC_ATTR_MANAGED);
+
+ if ((managed_s != NULL) && !crm_is_true(managed_s)) {
+ checks->flags |= rsc_unmanaged;
+ }
+}
+
+static void
+check_locked(resource_checks_t *checks)
+{
+ if (checks->rsc->lock_node != NULL) {
+ checks->flags |= rsc_locked;
+ checks->lock_node = checks->rsc->lock_node->details->uname;
+ }
+}
+
+static bool
+node_is_unhealthy(pe_node_t *node)
+{
+ switch (pe__health_strategy(node->details->data_set)) {
+ case pcmk__health_strategy_none:
+ break;
+
+ case pcmk__health_strategy_no_red:
+ if (pe__node_health(node) < 0) {
+ return true;
+ }
+ break;
+
+ case pcmk__health_strategy_only_green:
+ if (pe__node_health(node) <= 0) {
+ return true;
+ }
+ break;
+
+ case pcmk__health_strategy_progressive:
+ case pcmk__health_strategy_custom:
+ /* @TODO These are finite scores, possibly with rules, and possibly
+ * combining with other scores, so attributing these as a cause is
+ * nontrivial.
+ */
+ break;
+ }
+ return false;
+}
+
+static void
+check_node_health(resource_checks_t *checks, pe_node_t *node)
+{
+ if (node == NULL) {
+ GHashTableIter iter;
+ bool allowed = false;
+ bool all_nodes_unhealthy = true;
+
+ g_hash_table_iter_init(&iter, checks->rsc->allowed_nodes);
+ while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) {
+ allowed = true;
+ if (!node_is_unhealthy(node)) {
+ all_nodes_unhealthy = false;
+ break;
+ }
+ }
+ if (allowed && all_nodes_unhealthy) {
+ checks->flags |= rsc_node_health;
+ }
+
+ } else if (node_is_unhealthy(node)) {
+ checks->flags |= rsc_node_health;
+ }
+}
+
+int
+cli_resource_check(pcmk__output_t *out, pe_resource_t *rsc, pe_node_t *node)
+{
+ resource_checks_t checks = { .rsc = rsc };
+
+ check_role(&checks);
+ check_managed(&checks);
+ check_locked(&checks);
+ check_node_health(&checks, node);
+
+ return out->message(out, "resource-check-list", &checks);
+}
+
+// \return Standard Pacemaker return code
+int
+cli_resource_fail(pcmk_ipc_api_t *controld_api, const char *host_uname,
+ const char *rsc_id, pe_working_set_t *data_set)
+{
+ crm_notice("Failing %s on %s", rsc_id, host_uname);
+ return send_lrm_rsc_op(controld_api, true, host_uname, rsc_id, data_set);
+}
+
+static GHashTable *
+generate_resource_params(pe_resource_t *rsc, pe_node_t *node,
+ pe_working_set_t *data_set)
+{
+ GHashTable *params = NULL;
+ GHashTable *meta = NULL;
+ GHashTable *combined = NULL;
+ GHashTableIter iter;
+ char *key = NULL;
+ char *value = NULL;
+
+ combined = pcmk__strkey_table(free, free);
+
+ params = pe_rsc_params(rsc, node, data_set);
+ if (params != NULL) {
+ g_hash_table_iter_init(&iter, params);
+ while (g_hash_table_iter_next(&iter, (gpointer *) & key, (gpointer *) & value)) {
+ g_hash_table_insert(combined, strdup(key), strdup(value));
+ }
+ }
+
+ meta = pcmk__strkey_table(free, free);
+ get_meta_attributes(meta, rsc, node, data_set);
+ if (meta != NULL) {
+ g_hash_table_iter_init(&iter, meta);
+ while (g_hash_table_iter_next(&iter, (gpointer *) & key, (gpointer *) & value)) {
+ char *crm_name = crm_meta_name(key);
+
+ g_hash_table_insert(combined, crm_name, strdup(value));
+ }
+ g_hash_table_destroy(meta);
+ }
+
+ return combined;
+}
+
+bool resource_is_running_on(pe_resource_t *rsc, const char *host)
+{
+ bool found = true;
+ GList *hIter = NULL;
+ GList *hosts = NULL;
+
+ if (rsc == NULL) {
+ return false;
+ }
+
+ rsc->fns->location(rsc, &hosts, TRUE);
+ for (hIter = hosts; host != NULL && hIter != NULL; hIter = hIter->next) {
+ pe_node_t *node = (pe_node_t *) hIter->data;
+
+ if (pcmk__strcase_any_of(host, node->details->uname, node->details->id, NULL)) {
+ crm_trace("Resource %s is running on %s\n", rsc->id, host);
+ goto done;
+ }
+ }
+
+ if (host != NULL) {
+ crm_trace("Resource %s is not running on: %s\n", rsc->id, host);
+ found = false;
+
+ } else if(host == NULL && hosts == NULL) {
+ crm_trace("Resource %s is not running\n", rsc->id);
+ found = false;
+ }
+
+ done:
+ g_list_free(hosts);
+ return found;
+}
+
+/*!
+ * \internal
+ * \brief Create a list of all resources active on host from a given list
+ *
+ * \param[in] host Name of host to check whether resources are active
+ * \param[in] rsc_list List of resources to check
+ *
+ * \return New list of resources from list that are active on host
+ */
+static GList *
+get_active_resources(const char *host, GList *rsc_list)
+{
+ GList *rIter = NULL;
+ GList *active = NULL;
+
+ for (rIter = rsc_list; rIter != NULL; rIter = rIter->next) {
+ pe_resource_t *rsc = (pe_resource_t *) rIter->data;
+
+ /* Expand groups to their members, because if we're restarting a member
+ * other than the first, we can't otherwise tell which resources are
+ * stopping and starting.
+ */
+ if (rsc->variant == pe_group) {
+ active = g_list_concat(active,
+ get_active_resources(host, rsc->children));
+ } else if (resource_is_running_on(rsc, host)) {
+ active = g_list_append(active, strdup(rsc->id));
+ }
+ }
+ return active;
+}
+
+static void dump_list(GList *items, const char *tag)
+{
+ int lpc = 0;
+ GList *item = NULL;
+
+ for (item = items; item != NULL; item = item->next) {
+ crm_trace("%s[%d]: %s", tag, lpc, (char*)item->data);
+ lpc++;
+ }
+}
+
+static void display_list(pcmk__output_t *out, GList *items, const char *tag)
+{
+ GList *item = NULL;
+
+ for (item = items; item != NULL; item = item->next) {
+ out->info(out, "%s%s", tag, (const char *)item->data);
+ }
+}
+
+/*!
+ * \internal
+ * \brief Upgrade XML to latest schema version and use it as working set input
+ *
+ * This also updates the working set timestamp to the current time.
+ *
+ * \param[in,out] data_set Working set instance to update
+ * \param[in,out] xml XML to use as input
+ *
+ * \return Standard Pacemaker return code
+ * \note On success, caller is responsible for freeing memory allocated for
+ * data_set->now.
+ * \todo This follows the example of other callers of cli_config_update()
+ * and returns ENOKEY ("Required key not available") if that fails,
+ * but perhaps pcmk_rc_schema_validation would be better in that case.
+ */
+int
+update_working_set_xml(pe_working_set_t *data_set, xmlNode **xml)
+{
+ if (cli_config_update(xml, NULL, FALSE) == FALSE) {
+ return ENOKEY;
+ }
+ data_set->input = *xml;
+ data_set->now = crm_time_new(NULL);
+ return pcmk_rc_ok;
+}
+
+/*!
+ * \internal
+ * \brief Update a working set's XML input based on a CIB query
+ *
+ * \param[in] data_set Data set instance to initialize
+ * \param[in] cib Connection to the CIB manager
+ *
+ * \return Standard Pacemaker return code
+ * \note On success, caller is responsible for freeing memory allocated for
+ * data_set->input and data_set->now.
+ */
+static int
+update_working_set_from_cib(pcmk__output_t *out, pe_working_set_t * data_set,
+ cib_t *cib)
+{
+ xmlNode *cib_xml_copy = NULL;
+ int rc = pcmk_rc_ok;
+
+ rc = cib->cmds->query(cib, NULL, &cib_xml_copy, cib_scope_local | cib_sync_call);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Could not obtain the current CIB: %s (%d)", pcmk_strerror(rc), rc);
+ return rc;
+ }
+ rc = update_working_set_xml(data_set, &cib_xml_copy);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Could not upgrade the current CIB XML");
+ free_xml(cib_xml_copy);
+ return rc;
+ }
+
+ return rc;
+}
+
+// \return Standard Pacemaker return code
+static int
+update_dataset(cib_t *cib, pe_working_set_t * data_set, bool simulate)
+{
+ char *pid = NULL;
+ char *shadow_file = NULL;
+ cib_t *shadow_cib = NULL;
+ int rc = pcmk_rc_ok;
+
+ pcmk__output_t *out = data_set->priv;
+
+ pe_reset_working_set(data_set);
+ pe__set_working_set_flags(data_set, pe_flag_no_counts|pe_flag_no_compat);
+ rc = update_working_set_from_cib(out, data_set, cib);
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
+
+ if(simulate) {
+ bool prev_quiet = false;
+
+ pid = pcmk__getpid_s();
+ shadow_cib = cib_shadow_new(pid);
+ shadow_file = get_shadow_file(pid);
+
+ if (shadow_cib == NULL) {
+ out->err(out, "Could not create shadow cib: '%s'", pid);
+ rc = ENXIO;
+ goto done;
+ }
+
+ rc = write_xml_file(data_set->input, shadow_file, FALSE);
+
+ if (rc < 0) {
+ out->err(out, "Could not populate shadow cib: %s (%d)", pcmk_strerror(rc), rc);
+ goto done;
+ }
+
+ rc = shadow_cib->cmds->signon(shadow_cib, crm_system_name, cib_command);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Could not connect to shadow cib: %s (%d)", pcmk_strerror(rc), rc);
+ goto done;
+ }
+
+ pcmk__schedule_actions(data_set->input,
+ pe_flag_no_counts|pe_flag_no_compat, data_set);
+
+ prev_quiet = out->is_quiet(out);
+ out->quiet = true;
+ pcmk__simulate_transition(data_set, shadow_cib, NULL);
+ out->quiet = prev_quiet;
+
+ rc = update_dataset(shadow_cib, data_set, false);
+
+ } else {
+ cluster_status(data_set);
+ }
+
+ done:
+ /* Do not free data_set->input here, we need rsc->xml to be valid later on */
+ cib_delete(shadow_cib);
+ free(pid);
+
+ if(shadow_file) {
+ unlink(shadow_file);
+ free(shadow_file);
+ }
+
+ return rc;
+}
+
+static int
+max_delay_for_resource(pe_working_set_t * data_set, pe_resource_t *rsc)
+{
+ int delay = 0;
+ int max_delay = 0;
+
+ if(rsc && rsc->children) {
+ GList *iter = NULL;
+
+ for(iter = rsc->children; iter; iter = iter->next) {
+ pe_resource_t *child = (pe_resource_t *)iter->data;
+
+ delay = max_delay_for_resource(data_set, child);
+ if(delay > max_delay) {
+ double seconds = delay / 1000.0;
+ crm_trace("Calculated new delay of %.1fs due to %s", seconds, child->id);
+ max_delay = delay;
+ }
+ }
+
+ } else if(rsc) {
+ char *key = crm_strdup_printf("%s_%s_0", rsc->id, RSC_STOP);
+ pe_action_t *stop = custom_action(rsc, key, RSC_STOP, NULL, TRUE, FALSE, data_set);
+ const char *value = g_hash_table_lookup(stop->meta, XML_ATTR_TIMEOUT);
+ long long result_ll;
+
+ if ((pcmk__scan_ll(value, &result_ll, -1LL) == pcmk_rc_ok)
+ && (result_ll >= 0) && (result_ll <= INT_MAX)) {
+ max_delay = (int) result_ll;
+ } else {
+ max_delay = -1;
+ }
+ pe_free_action(stop);
+ }
+
+ return max_delay;
+}
+
+static int
+max_delay_in(pe_working_set_t * data_set, GList *resources)
+{
+ int max_delay = 0;
+ GList *item = NULL;
+
+ for (item = resources; item != NULL; item = item->next) {
+ int delay = 0;
+ pe_resource_t *rsc = pe_find_resource(data_set->resources, (const char *)item->data);
+
+ delay = max_delay_for_resource(data_set, rsc);
+
+ if(delay > max_delay) {
+ double seconds = delay / 1000.0;
+ crm_trace("Calculated new delay of %.1fs due to %s", seconds, rsc->id);
+ max_delay = delay;
+ }
+ }
+
+ return 5 + (max_delay / 1000);
+}
+
+#define waiting_for_starts(d, r, h) ((d != NULL) || \
+ (!resource_is_running_on((r), (h))))
+
+/*!
+ * \internal
+ * \brief Restart a resource (on a particular host if requested).
+ *
+ * \param[in,out] out Output object
+ * \param[in,out] rsc The resource to restart
+ * \param[in] node Node to restart resource on (NULL for all)
+ * \param[in] move_lifetime If not NULL, how long constraint should
+ * remain in effect (as ISO 8601 string)
+ * \param[in] timeout_ms Consider failed if actions do not complete
+ * in this time (specified in milliseconds,
+ * but a two-second granularity is actually
+ * used; if 0, it will be calculated based on
+ * the resource timeout)
+ * \param[in,out] cib Connection to the CIB manager
+ * \param[in] cib_options Group of enum cib_call_options flags to
+ * use with CIB calls
+ * \param[in] promoted_role_only If true, limit to promoted instances
+ * \param[in] force If true, apply only to requested instance
+ * if part of a collective resource
+ *
+ * \return Standard Pacemaker return code (exits on certain failures)
+ */
+int
+cli_resource_restart(pcmk__output_t *out, pe_resource_t *rsc,
+ const pe_node_t *node, const char *move_lifetime,
+ int timeout_ms, cib_t *cib, int cib_options,
+ gboolean promoted_role_only, gboolean force)
+{
+ int rc = pcmk_rc_ok;
+ int lpc = 0;
+ int before = 0;
+ int step_timeout_s = 0;
+ int sleep_interval = 2;
+ int timeout = timeout_ms / 1000;
+
+ bool stop_via_ban = false;
+ char *rsc_id = NULL;
+ char *lookup_id = NULL;
+ char *orig_target_role = NULL;
+
+ GList *list_delta = NULL;
+ GList *target_active = NULL;
+ GList *current_active = NULL;
+ GList *restart_target_active = NULL;
+
+ pe_working_set_t *data_set = NULL;
+ pe_resource_t *parent = uber_parent(rsc);
+
+ bool running = false;
+ const char *id = rsc->clone_name ? rsc->clone_name : rsc->id;
+ const char *host = node ? node->details->uname : NULL;
+
+ /* If the implicit resource or primitive resource of a bundle is given, operate on the
+ * bundle itself instead.
+ */
+ if (pe_rsc_is_bundled(rsc)) {
+ rsc = parent->parent;
+ }
+
+ running = resource_is_running_on(rsc, host);
+
+ if (pe_rsc_is_clone(parent) && !running) {
+ if (pe_rsc_is_unique_clone(parent)) {
+ lookup_id = strdup(rsc->id);
+ } else {
+ lookup_id = clone_strip(rsc->id);
+ }
+
+ rsc = parent->fns->find_rsc(parent, lookup_id, node, pe_find_any|pe_find_current);
+ free(lookup_id);
+ running = resource_is_running_on(rsc, host);
+ }
+
+ if (!running) {
+ if (host) {
+ out->err(out, "%s is not running on %s and so cannot be restarted", id, host);
+ } else {
+ out->err(out, "%s is not running anywhere and so cannot be restarted", id);
+ }
+ return ENXIO;
+ }
+
+ rsc_id = strdup(rsc->id);
+
+ if (pe_rsc_is_unique_clone(parent)) {
+ lookup_id = strdup(rsc->id);
+ } else {
+ lookup_id = clone_strip(rsc->id);
+ }
+
+ if (host) {
+ if (pe_rsc_is_clone(rsc) || pe_bundle_replicas(rsc)) {
+ stop_via_ban = true;
+ } else if (pe_rsc_is_clone(parent)) {
+ stop_via_ban = true;
+ free(lookup_id);
+ lookup_id = strdup(parent->id);
+ }
+ }
+
+ /*
+ grab full cib
+ determine originally active resources
+ disable or ban
+ poll cib and watch for affected resources to get stopped
+ without --timeout, calculate the stop timeout for each step and wait for that
+ if we hit --timeout or the service timeout, re-enable or un-ban, report failure and indicate which resources we couldn't take down
+ if everything stopped, re-enable or un-ban
+ poll cib and watch for affected resources to get started
+ without --timeout, calculate the start timeout for each step and wait for that
+ if we hit --timeout or the service timeout, report (different) failure and indicate which resources we couldn't bring back up
+ report success
+
+ Optimizations:
+ - use constraints to determine ordered list of affected resources
+ - Allow a --no-deps option (aka. --force-restart)
+ */
+
+ data_set = pe_new_working_set();
+ if (data_set == NULL) {
+ crm_perror(LOG_ERR, "Could not allocate working set");
+ rc = ENOMEM;
+ goto done;
+ }
+
+ data_set->priv = out;
+ rc = update_dataset(cib, data_set, false);
+
+ if(rc != pcmk_rc_ok) {
+ out->err(out, "Could not get new resource list: %s (%d)", pcmk_strerror(rc), rc);
+ goto done;
+ }
+
+ restart_target_active = get_active_resources(host, data_set->resources);
+ current_active = get_active_resources(host, data_set->resources);
+
+ dump_list(current_active, "Origin");
+
+ if (stop_via_ban) {
+ /* Stop the clone or bundle instance by banning it from the host */
+ out->quiet = true;
+ rc = cli_resource_ban(out, lookup_id, host, move_lifetime, NULL, cib,
+ cib_options, promoted_role_only);
+
+ } else {
+ /* Stop the resource by setting target-role to Stopped.
+ * Remember any existing target-role so we can restore it later
+ * (though it only makes any difference if it's Unpromoted).
+ */
+
+ find_resource_attr(out, cib, XML_NVPAIR_ATTR_VALUE, lookup_id, NULL, NULL,
+ NULL, XML_RSC_ATTR_TARGET_ROLE, &orig_target_role);
+ rc = cli_resource_update_attribute(rsc, rsc_id, NULL, XML_TAG_META_SETS,
+ NULL, XML_RSC_ATTR_TARGET_ROLE,
+ RSC_STOPPED, FALSE, cib, cib_options,
+ force);
+ }
+ if(rc != pcmk_rc_ok) {
+ out->err(out, "Could not set target-role for %s: %s (%d)", rsc_id, pcmk_strerror(rc), rc);
+ if (current_active != NULL) {
+ g_list_free_full(current_active, free);
+ current_active = NULL;
+ }
+ if (restart_target_active != NULL) {
+ g_list_free_full(restart_target_active, free);
+ restart_target_active = NULL;
+ }
+ goto done;
+ }
+
+ rc = update_dataset(cib, data_set, true);
+ if(rc != pcmk_rc_ok) {
+ out->err(out, "Could not determine which resources would be stopped");
+ goto failure;
+ }
+
+ target_active = get_active_resources(host, data_set->resources);
+ dump_list(target_active, "Target");
+
+ list_delta = pcmk__subtract_lists(current_active, target_active, (GCompareFunc) strcmp);
+ out->info(out, "Waiting for %d resources to stop:", g_list_length(list_delta));
+ display_list(out, list_delta, " * ");
+
+ step_timeout_s = timeout / sleep_interval;
+ while (list_delta != NULL) {
+ before = g_list_length(list_delta);
+ if(timeout_ms == 0) {
+ step_timeout_s = max_delay_in(data_set, list_delta) / sleep_interval;
+ }
+
+ /* We probably don't need the entire step timeout */
+ for(lpc = 0; (lpc < step_timeout_s) && (list_delta != NULL); lpc++) {
+ sleep(sleep_interval);
+ if(timeout) {
+ timeout -= sleep_interval;
+ crm_trace("%ds remaining", timeout);
+ }
+ rc = update_dataset(cib, data_set, FALSE);
+ if(rc != pcmk_rc_ok) {
+ out->err(out, "Could not determine which resources were stopped");
+ goto failure;
+ }
+
+ if (current_active != NULL) {
+ g_list_free_full(current_active, free);
+ current_active = NULL;
+ }
+ current_active = get_active_resources(host, data_set->resources);
+ g_list_free(list_delta);
+ list_delta = NULL;
+ list_delta = pcmk__subtract_lists(current_active, target_active, (GCompareFunc) strcmp);
+ dump_list(current_active, "Current");
+ dump_list(list_delta, "Delta");
+ }
+
+ crm_trace("%d (was %d) resources remaining", g_list_length(list_delta), before);
+ if(before == g_list_length(list_delta)) {
+ /* aborted during stop phase, print the contents of list_delta */
+ out->err(out, "Could not complete shutdown of %s, %d resources remaining", rsc_id, g_list_length(list_delta));
+ display_list(out, list_delta, " * ");
+ rc = ETIME;
+ goto failure;
+ }
+
+ }
+
+ if (stop_via_ban) {
+ rc = cli_resource_clear(lookup_id, host, NULL, cib, cib_options, true, force);
+
+ } else if (orig_target_role) {
+ rc = cli_resource_update_attribute(rsc, rsc_id, NULL, XML_TAG_META_SETS,
+ NULL, XML_RSC_ATTR_TARGET_ROLE,
+ orig_target_role, FALSE, cib,
+ cib_options, force);
+ free(orig_target_role);
+ orig_target_role = NULL;
+ } else {
+ rc = cli_resource_delete_attribute(rsc, rsc_id, NULL, XML_TAG_META_SETS,
+ NULL, XML_RSC_ATTR_TARGET_ROLE, cib,
+ cib_options, force);
+ }
+
+ if(rc != pcmk_rc_ok) {
+ out->err(out, "Could not unset target-role for %s: %s (%d)", rsc_id, pcmk_strerror(rc), rc);
+ goto done;
+ }
+
+ if (target_active != NULL) {
+ g_list_free_full(target_active, free);
+ target_active = NULL;
+ }
+ target_active = restart_target_active;
+ list_delta = pcmk__subtract_lists(target_active, current_active, (GCompareFunc) strcmp);
+ out->info(out, "Waiting for %d resources to start again:", g_list_length(list_delta));
+ display_list(out, list_delta, " * ");
+
+ step_timeout_s = timeout / sleep_interval;
+ while (waiting_for_starts(list_delta, rsc, host)) {
+ before = g_list_length(list_delta);
+ if(timeout_ms == 0) {
+ step_timeout_s = max_delay_in(data_set, list_delta) / sleep_interval;
+ }
+
+ /* We probably don't need the entire step timeout */
+ for (lpc = 0; (lpc < step_timeout_s) && waiting_for_starts(list_delta, rsc, host); lpc++) {
+
+ sleep(sleep_interval);
+ if(timeout) {
+ timeout -= sleep_interval;
+ crm_trace("%ds remaining", timeout);
+ }
+
+ rc = update_dataset(cib, data_set, false);
+ if(rc != pcmk_rc_ok) {
+ out->err(out, "Could not determine which resources were started");
+ goto failure;
+ }
+
+ if (current_active != NULL) {
+ g_list_free_full(current_active, free);
+ current_active = NULL;
+ }
+
+ /* It's OK if dependent resources moved to a different node,
+ * so we check active resources on all nodes.
+ */
+ current_active = get_active_resources(NULL, data_set->resources);
+ g_list_free(list_delta);
+ list_delta = pcmk__subtract_lists(target_active, current_active, (GCompareFunc) strcmp);
+ dump_list(current_active, "Current");
+ dump_list(list_delta, "Delta");
+ }
+
+ if(before == g_list_length(list_delta)) {
+ /* aborted during start phase, print the contents of list_delta */
+ out->err(out, "Could not complete restart of %s, %d resources remaining", rsc_id, g_list_length(list_delta));
+ display_list(out, list_delta, " * ");
+ rc = ETIME;
+ goto failure;
+ }
+
+ }
+
+ rc = pcmk_rc_ok;
+ goto done;
+
+ failure:
+ if (stop_via_ban) {
+ cli_resource_clear(lookup_id, host, NULL, cib, cib_options, true, force);
+ } else if (orig_target_role) {
+ cli_resource_update_attribute(rsc, rsc_id, NULL, XML_TAG_META_SETS, NULL,
+ XML_RSC_ATTR_TARGET_ROLE, orig_target_role,
+ FALSE, cib, cib_options, force);
+ free(orig_target_role);
+ } else {
+ cli_resource_delete_attribute(rsc, rsc_id, NULL, XML_TAG_META_SETS,
+ NULL, XML_RSC_ATTR_TARGET_ROLE, cib,
+ cib_options, force);
+ }
+
+done:
+ if (list_delta != NULL) {
+ g_list_free(list_delta);
+ }
+ if (current_active != NULL) {
+ g_list_free_full(current_active, free);
+ }
+ if (target_active != NULL && (target_active != restart_target_active)) {
+ g_list_free_full(target_active, free);
+ }
+ if (restart_target_active != NULL) {
+ g_list_free_full(restart_target_active, free);
+ }
+ free(rsc_id);
+ free(lookup_id);
+ pe_free_working_set(data_set);
+ return rc;
+}
+
+static inline bool
+action_is_pending(const pe_action_t *action)
+{
+ if (pcmk_any_flags_set(action->flags, pe_action_optional|pe_action_pseudo)
+ || !pcmk_is_set(action->flags, pe_action_runnable)
+ || pcmk__str_eq("notify", action->task, pcmk__str_casei)) {
+ return false;
+ }
+ return true;
+}
+
+/*!
+ * \internal
+ * \brief Check whether any actions in a list are pending
+ *
+ * \param[in] actions List of actions to check
+ *
+ * \return true if any actions in the list are pending, otherwise false
+ */
+static bool
+actions_are_pending(const GList *actions)
+{
+ for (const GList *action = actions; action != NULL; action = action->next) {
+ const pe_action_t *a = (const pe_action_t *) action->data;
+
+ if (action_is_pending(a)) {
+ crm_notice("Waiting for %s (flags=%#.8x)", a->uuid, a->flags);
+ return true;
+ }
+ }
+ return false;
+}
+
+static void
+print_pending_actions(pcmk__output_t *out, GList *actions)
+{
+ GList *action;
+
+ out->info(out, "Pending actions:");
+ for (action = actions; action != NULL; action = action->next) {
+ pe_action_t *a = (pe_action_t *) action->data;
+
+ if (!action_is_pending(a)) {
+ continue;
+ }
+
+ if (a->node) {
+ out->info(out, "\tAction %d: %s\ton %s",
+ a->id, a->uuid, pe__node_name(a->node));
+ } else {
+ out->info(out, "\tAction %d: %s", a->id, a->uuid);
+ }
+ }
+}
+
+/* For --wait, timeout (in seconds) to use if caller doesn't specify one */
+#define WAIT_DEFAULT_TIMEOUT_S (60 * 60)
+
+/* For --wait, how long to sleep between cluster state checks */
+#define WAIT_SLEEP_S (2)
+
+/*!
+ * \internal
+ * \brief Wait until all pending cluster actions are complete
+ *
+ * This waits until either the CIB's transition graph is idle or a timeout is
+ * reached.
+ *
+ * \param[in,out] out Output object
+ * \param[in] timeout_ms Consider failed if actions do not complete in
+ * this time (specified in milliseconds, but
+ * one-second granularity is actually used; if 0, a
+ * default will be used)
+ * \param[in,out] cib Connection to the CIB manager
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+wait_till_stable(pcmk__output_t *out, int timeout_ms, cib_t * cib)
+{
+ pe_working_set_t *data_set = NULL;
+ int rc = pcmk_rc_ok;
+ int timeout_s = timeout_ms? ((timeout_ms + 999) / 1000) : WAIT_DEFAULT_TIMEOUT_S;
+ time_t expire_time = time(NULL) + timeout_s;
+ time_t time_diff;
+ bool printed_version_warning = out->is_quiet(out); // i.e. don't print if quiet
+
+ data_set = pe_new_working_set();
+ if (data_set == NULL) {
+ return ENOMEM;
+ }
+
+ do {
+
+ /* Abort if timeout is reached */
+ time_diff = expire_time - time(NULL);
+ if (time_diff > 0) {
+ crm_info("Waiting up to %lld seconds for cluster actions to complete", (long long) time_diff);
+ } else {
+ print_pending_actions(out, data_set->actions);
+ pe_free_working_set(data_set);
+ return ETIME;
+ }
+ if (rc == pcmk_rc_ok) { /* this avoids sleep on first loop iteration */
+ sleep(WAIT_SLEEP_S);
+ }
+
+ /* Get latest transition graph */
+ pe_reset_working_set(data_set);
+ rc = update_working_set_from_cib(out, data_set, cib);
+ if (rc != pcmk_rc_ok) {
+ pe_free_working_set(data_set);
+ return rc;
+ }
+ pcmk__schedule_actions(data_set->input,
+ pe_flag_no_counts|pe_flag_no_compat, data_set);
+
+ if (!printed_version_warning) {
+ /* If the DC has a different version than the local node, the two
+ * could come to different conclusions about what actions need to be
+ * done. Warn the user in this case.
+ *
+ * @TODO A possible long-term solution would be to reimplement the
+ * wait as a new controller operation that would be forwarded to the
+ * DC. However, that would have potential problems of its own.
+ */
+ const char *dc_version = g_hash_table_lookup(data_set->config_hash,
+ "dc-version");
+
+ if (!pcmk__str_eq(dc_version, PACEMAKER_VERSION "-" BUILD_VERSION, pcmk__str_casei)) {
+ out->info(out, "warning: wait option may not work properly in "
+ "mixed-version cluster");
+ printed_version_warning = true;
+ }
+ }
+
+ } while (actions_are_pending(data_set->actions));
+
+ pe_free_working_set(data_set);
+ return rc;
+}
+
+static const char *
+get_action(const char *rsc_action) {
+ const char *action = NULL;
+
+ if (pcmk__str_eq(rsc_action, "validate", pcmk__str_casei)) {
+ action = "validate-all";
+
+ } else if (pcmk__str_eq(rsc_action, "force-check", pcmk__str_casei)) {
+ action = "monitor";
+
+ } else if (pcmk__strcase_any_of(rsc_action, "force-start", "force-stop",
+ "force-demote", "force-promote", NULL)) {
+ action = rsc_action+6;
+ } else {
+ action = rsc_action;
+ }
+
+ return action;
+}
+
+/*!
+ * \brief Set up environment variables as expected by resource agents
+ *
+ * When the cluster executes resource agents, it adds certain environment
+ * variables (directly or via resource meta-attributes) expected by some
+ * resource agents. Add the essential ones that many resource agents expect, so
+ * the behavior is the same for command-line execution.
+ *
+ * \param[in,out] params Resource parameters that will be passed to agent
+ * \param[in] timeout_ms Action timeout (in milliseconds)
+ * \param[in] check_level OCF check level
+ * \param[in] verbosity Verbosity level
+ */
+static void
+set_agent_environment(GHashTable *params, int timeout_ms, int check_level,
+ int verbosity)
+{
+ g_hash_table_insert(params, strdup("CRM_meta_timeout"),
+ crm_strdup_printf("%d", timeout_ms));
+
+ g_hash_table_insert(params, strdup(XML_ATTR_CRM_VERSION),
+ strdup(CRM_FEATURE_SET));
+
+ if (check_level >= 0) {
+ char *level = crm_strdup_printf("%d", check_level);
+
+ setenv("OCF_CHECK_LEVEL", level, 1);
+ free(level);
+ }
+
+ setenv("HA_debug", (verbosity > 0)? "1" : "0", 1);
+ if (verbosity > 1) {
+ setenv("OCF_TRACE_RA", "1", 1);
+ }
+
+ /* A resource agent using the standard ocf-shellfuncs library will not print
+ * messages to stderr if it doesn't have a controlling terminal (e.g. if
+ * crm_resource is called via script or ssh). This forces it to do so.
+ */
+ setenv("OCF_TRACE_FILE", "/dev/stderr", 0);
+}
+
+/*!
+ * \internal
+ * \brief Apply command-line overrides to resource parameters
+ *
+ * \param[in,out] params Parameters to be passed to agent
+ * \param[in] overrides Parameters to override (or NULL if none)
+ */
+static void
+apply_overrides(GHashTable *params, GHashTable *overrides)
+{
+ if (overrides != NULL) {
+ GHashTableIter iter;
+ char *name = NULL;
+ char *value = NULL;
+
+ g_hash_table_iter_init(&iter, overrides);
+ while (g_hash_table_iter_next(&iter, (gpointer *) &name,
+ (gpointer *) &value)) {
+ g_hash_table_replace(params, strdup(name), strdup(value));
+ }
+ }
+}
+
+crm_exit_t
+cli_resource_execute_from_params(pcmk__output_t *out, const char *rsc_name,
+ const char *rsc_class, const char *rsc_prov,
+ const char *rsc_type, const char *rsc_action,
+ GHashTable *params, GHashTable *override_hash,
+ int timeout_ms, int resource_verbose, gboolean force,
+ int check_level)
+{
+ const char *class = rsc_class;
+ const char *action = get_action(rsc_action);
+ crm_exit_t exit_code = CRM_EX_OK;
+ svc_action_t *op = NULL;
+
+ // If no timeout was provided, use the same default as the cluster
+ if (timeout_ms == 0) {
+ timeout_ms = crm_get_msec(CRM_DEFAULT_OP_TIMEOUT_S);
+ }
+
+ set_agent_environment(params, timeout_ms, check_level, resource_verbose);
+ apply_overrides(params, override_hash);
+
+ op = services__create_resource_action(rsc_name? rsc_name : "test",
+ rsc_class, rsc_prov, rsc_type, action,
+ 0, timeout_ms, params, 0);
+ if (op == NULL) {
+ out->err(out, "Could not execute %s using %s%s%s:%s: %s",
+ action, rsc_class, (rsc_prov? ":" : ""),
+ (rsc_prov? rsc_prov : ""), rsc_type, strerror(ENOMEM));
+ g_hash_table_destroy(params);
+ return CRM_EX_OSERR;
+ }
+
+ if (pcmk__str_eq(rsc_class, PCMK_RESOURCE_CLASS_SERVICE, pcmk__str_casei)) {
+ class = resources_find_service_class(rsc_type);
+ }
+ if (!pcmk__strcase_any_of(class, PCMK_RESOURCE_CLASS_OCF,
+ PCMK_RESOURCE_CLASS_LSB, NULL)) {
+ services__format_result(op, CRM_EX_UNIMPLEMENT_FEATURE, PCMK_EXEC_ERROR,
+ "Manual execution of the %s standard is "
+ "unsupported", pcmk__s(class, "unspecified"));
+ }
+
+ if (op->rc != PCMK_OCF_UNKNOWN) {
+ exit_code = op->rc;
+ goto done;
+ }
+
+ services_action_sync(op);
+
+ // Map results to OCF codes for consistent reporting to user
+ {
+ enum ocf_exitcode ocf_code = services_result2ocf(class, action, op->rc);
+
+ // Cast variable instead of function return to keep compilers happy
+ exit_code = (crm_exit_t) ocf_code;
+ }
+
+done:
+ out->message(out, "resource-agent-action", resource_verbose, rsc_class,
+ rsc_prov, rsc_type, rsc_name, rsc_action, override_hash,
+ exit_code, op->status, services__exit_reason(op),
+ op->stdout_data, op->stderr_data);
+ services_action_free(op);
+ return exit_code;
+}
+
+crm_exit_t
+cli_resource_execute(pe_resource_t *rsc, const char *requested_name,
+ const char *rsc_action, GHashTable *override_hash,
+ int timeout_ms, cib_t * cib, pe_working_set_t *data_set,
+ int resource_verbose, gboolean force, int check_level)
+{
+ pcmk__output_t *out = data_set->priv;
+ crm_exit_t exit_code = CRM_EX_OK;
+ const char *rid = NULL;
+ const char *rtype = NULL;
+ const char *rprov = NULL;
+ const char *rclass = NULL;
+ GHashTable *params = NULL;
+
+ if (pcmk__strcase_any_of(rsc_action, "force-start", "force-demote",
+ "force-promote", NULL)) {
+ if(pe_rsc_is_clone(rsc)) {
+ GList *nodes = cli_resource_search(rsc, requested_name, data_set);
+ if(nodes != NULL && force == FALSE) {
+ out->err(out, "It is not safe to %s %s here: the cluster claims it is already active",
+ rsc_action, rsc->id);
+ out->err(out, "Try setting target-role=Stopped first or specifying "
+ "the force option");
+ return CRM_EX_UNSAFE;
+ }
+
+ g_list_free_full(nodes, free);
+ }
+ }
+
+ if(pe_rsc_is_clone(rsc)) {
+ /* Grab the first child resource in the hope it's not a group */
+ rsc = rsc->children->data;
+ }
+
+ if(rsc->variant == pe_group) {
+ out->err(out, "Sorry, the %s option doesn't support group resources", rsc_action);
+ return CRM_EX_UNIMPLEMENT_FEATURE;
+ } else if (rsc->variant == pe_container || pe_rsc_is_bundled(rsc)) {
+ out->err(out, "Sorry, the %s option doesn't support bundled resources", rsc_action);
+ return CRM_EX_UNIMPLEMENT_FEATURE;
+ }
+
+ rclass = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS);
+ rprov = crm_element_value(rsc->xml, XML_AGENT_ATTR_PROVIDER);
+ rtype = crm_element_value(rsc->xml, XML_ATTR_TYPE);
+
+ params = generate_resource_params(rsc, NULL /* @TODO use local node */,
+ data_set);
+
+ if (timeout_ms == 0) {
+ timeout_ms = pe_get_configured_timeout(rsc, get_action(rsc_action), data_set);
+ }
+
+ rid = pe_rsc_is_anon_clone(rsc->parent)? requested_name : rsc->id;
+
+ exit_code = cli_resource_execute_from_params(out, rid, rclass, rprov, rtype, rsc_action,
+ params, override_hash, timeout_ms,
+ resource_verbose, force, check_level);
+ return exit_code;
+}
+
+// \return Standard Pacemaker return code
+int
+cli_resource_move(const pe_resource_t *rsc, const char *rsc_id,
+ const char *host_name, const char *move_lifetime, cib_t *cib,
+ int cib_options, pe_working_set_t *data_set,
+ gboolean promoted_role_only, gboolean force)
+{
+ pcmk__output_t *out = data_set->priv;
+ int rc = pcmk_rc_ok;
+ unsigned int count = 0;
+ pe_node_t *current = NULL;
+ pe_node_t *dest = pe_find_node(data_set->nodes, host_name);
+ bool cur_is_dest = false;
+
+ if (dest == NULL) {
+ return pcmk_rc_node_unknown;
+ }
+
+ if (promoted_role_only && !pcmk_is_set(rsc->flags, pe_rsc_promotable)) {
+ const pe_resource_t *p = pe__const_top_resource(rsc, false);
+
+ if (pcmk_is_set(p->flags, pe_rsc_promotable)) {
+ out->info(out, "Using parent '%s' for move instead of '%s'.", rsc->id, rsc_id);
+ rsc_id = p->id;
+ rsc = p;
+
+ } else {
+ out->info(out, "Ignoring --promoted option: %s is not promotable",
+ rsc_id);
+ promoted_role_only = FALSE;
+ }
+ }
+
+ current = pe__find_active_requires(rsc, &count);
+
+ if (pcmk_is_set(rsc->flags, pe_rsc_promotable)) {
+ unsigned int promoted_count = 0;
+ pe_node_t *promoted_node = NULL;
+
+ for (const GList *iter = rsc->children; iter; iter = iter->next) {
+ const pe_resource_t *child = (const pe_resource_t *) iter->data;
+ enum rsc_role_e child_role = child->fns->state(child, TRUE);
+
+ if (child_role == RSC_ROLE_PROMOTED) {
+ rsc = child;
+ promoted_node = pe__current_node(child);
+ promoted_count++;
+ }
+ }
+ if (promoted_role_only || (promoted_count != 0)) {
+ count = promoted_count;
+ current = promoted_node;
+ }
+
+ }
+
+ if (count > 1) {
+ if (pe_rsc_is_clone(rsc)) {
+ current = NULL;
+ } else {
+ return pcmk_rc_multiple;
+ }
+ }
+
+ if (current && (current->details == dest->details)) {
+ cur_is_dest = true;
+ if (force) {
+ crm_info("%s is already %s on %s, reinforcing placement with location constraint.",
+ rsc_id, promoted_role_only?"promoted":"active",
+ pe__node_name(dest));
+ } else {
+ return pcmk_rc_already;
+ }
+ }
+
+ /* Clear any previous prefer constraints across all nodes. */
+ cli_resource_clear(rsc_id, NULL, data_set->nodes, cib, cib_options, false, force);
+
+ /* Clear any previous ban constraints on 'dest'. */
+ cli_resource_clear(rsc_id, dest->details->uname, data_set->nodes, cib,
+ cib_options, TRUE, force);
+
+ /* Record an explicit preference for 'dest' */
+ rc = cli_resource_prefer(out, rsc_id, dest->details->uname, move_lifetime,
+ cib, cib_options, promoted_role_only);
+
+ crm_trace("%s%s now prefers %s%s",
+ rsc->id, (promoted_role_only? " (promoted)" : ""),
+ pe__node_name(dest), force?"(forced)":"");
+
+ /* only ban the previous location if current location != destination location.
+ * it is possible to use -M to enforce a location without regard of where the
+ * resource is currently located */
+ if (force && !cur_is_dest) {
+ /* Ban the original location if possible */
+ if(current) {
+ (void)cli_resource_ban(out, rsc_id, current->details->uname, move_lifetime,
+ NULL, cib, cib_options, promoted_role_only);
+
+ } else if(count > 1) {
+ out->info(out, "Resource '%s' is currently %s in %d locations. "
+ "One may now move to %s",
+ rsc_id, (promoted_role_only? "promoted" : "active"),
+ count, pe__node_name(dest));
+ out->info(out, "To prevent '%s' from being %s at a specific location, "
+ "specify a node.",
+ rsc_id, (promoted_role_only? "promoted" : "active"));
+
+ } else {
+ crm_trace("Not banning %s from its current location: not active", rsc_id);
+ }
+ }
+
+ return rc;
+}
diff --git a/tools/crm_rule.8.inc b/tools/crm_rule.8.inc
new file mode 100644
index 0000000..2b6ac89
--- /dev/null
+++ b/tools/crm_rule.8.inc
@@ -0,0 +1,8 @@
+[synopsis]
+crm_rule mode [options]
+
+/state of rules/
+.SH OPTIONS
+
+/multiple times/
+.SH NOTICE
diff --git a/tools/crm_rule.c b/tools/crm_rule.c
new file mode 100644
index 0000000..5cdc118
--- /dev/null
+++ b/tools/crm_rule.c
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2019-2023 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 <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
+#include <crm/common/iso8601.h>
+#include <crm/msg_xml.h>
+#include <crm/pengine/rules_internal.h>
+#include <crm/pengine/status.h>
+#include <pacemaker-internal.h>
+
+#include <sys/stat.h>
+
+#define SUMMARY "evaluate rules from the Pacemaker configuration"
+
+static pcmk__supported_format_t formats[] = {
+ PCMK__SUPPORTED_FORMAT_NONE,
+ PCMK__SUPPORTED_FORMAT_TEXT,
+ PCMK__SUPPORTED_FORMAT_XML,
+ { NULL, NULL, NULL }
+};
+
+enum crm_rule_mode {
+ crm_rule_mode_none,
+ crm_rule_mode_check
+};
+
+struct {
+ char *date;
+ char *input_xml;
+ enum crm_rule_mode mode;
+ gchar **rules;
+} options = {
+ .mode = crm_rule_mode_none
+};
+
+static gboolean mode_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+
+static GOptionEntry mode_entries[] = {
+ { "check", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, mode_cb,
+ "Check whether a rule is in effect",
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry data_entries[] = {
+ { "xml-text", 'X', 0, G_OPTION_ARG_STRING, &options.input_xml,
+ "Use argument for XML (or stdin if '-')",
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry addl_entries[] = {
+ { "date", 'd', 0, G_OPTION_ARG_STRING, &options.date,
+ "Whether the rule is in effect on a given date",
+ NULL },
+ { "rule", 'r', 0, G_OPTION_ARG_STRING_ARRAY, &options.rules,
+ "The ID of the rule to check (may be specified multiple times)",
+ NULL },
+
+ { NULL }
+};
+
+static gboolean
+mode_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (strcmp(option_name, "c")) {
+ options.mode = crm_rule_mode_check;
+ }
+
+ return TRUE;
+}
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
+ GOptionContext *context = NULL;
+
+ context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
+
+ pcmk__add_arg_group(context, "modes", "Modes (mutually exclusive):",
+ "Show modes of operation", mode_entries);
+ pcmk__add_arg_group(context, "data", "Data:",
+ "Show data options", data_entries);
+ pcmk__add_arg_group(context, "additional", "Additional Options:",
+ "Show additional options", addl_entries);
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ crm_time_t *rule_date = NULL;
+ xmlNode *input = NULL;
+
+ int rc = pcmk_rc_ok;
+ crm_exit_t exit_code = CRM_EX_OK;
+
+ GError *error = NULL;
+
+ pcmk__output_t *out = NULL;
+
+ GOptionGroup *output_group = NULL;
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ GOptionContext *context = build_arg_context(args, &output_group);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, "drX");
+
+ pcmk__register_formats(output_group, formats);
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("crm_rule", args->verbosity);
+
+ rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
+ if (rc != pcmk_rc_ok) {
+ exit_code = CRM_EX_ERROR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s",
+ args->output_ty, pcmk_rc_str(rc));
+ goto done;
+ }
+
+ pcmk__register_lib_messages(out);
+
+ if (args->version) {
+ out->version(out, false);
+ goto done;
+ }
+
+ /* Check command line arguments before opening a connection to
+ * the CIB manager or doing anything else important.
+ */
+ switch(options.mode) {
+ case crm_rule_mode_check:
+ if (options.rules == NULL) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "--check requires use of --rule=");
+ goto done;
+ }
+
+ break;
+
+ default:
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "No mode operation given");
+ goto done;
+ break;
+ }
+
+ /* Set up some defaults. */
+ rule_date = crm_time_new(options.date);
+ if (rule_date == NULL) {
+ if (options.date != NULL) {
+ exit_code = CRM_EX_DATAERR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Invalid date specified: '%s'", options.date);
+
+ } else {
+ // Should never happen
+ exit_code = CRM_EX_OSERR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "No --date given and can't determine current date");
+ }
+ goto done;
+ }
+
+ // Parse the input XML specified by the command-line options, if any
+ if (pcmk__str_eq(options.input_xml, "-", pcmk__str_casei)) {
+ input = stdin2xml();
+
+ if (input == NULL) {
+ exit_code = CRM_EX_DATAERR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Couldn't parse input from STDIN\n");
+ goto done;
+ }
+ } else if (options.input_xml != NULL) {
+ input = string2xml(options.input_xml);
+
+ if (input == NULL) {
+ exit_code = CRM_EX_DATAERR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Couldn't parse input string: %s\n", options.input_xml);
+ goto done;
+ }
+ }
+
+ /* Now do whichever operation mode was asked for. There's only one at the
+ * moment so this looks a little silly, but I expect there will be more
+ * modes in the future.
+ */
+ switch(options.mode) {
+ case crm_rule_mode_check:
+ rc = pcmk__check_rules(out, input, rule_date,
+ (const char **) options.rules);
+ exit_code = pcmk_rc2exitc(rc);
+ break;
+
+ default:
+ break;
+ }
+
+done:
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+
+ crm_time_free(rule_date);
+ free_xml(input);
+
+ pcmk__output_and_clear_error(&error, out);
+
+ if (out != NULL) {
+ out->finish(out, exit_code, true, NULL);
+ pcmk__output_free(out);
+ }
+
+ pcmk__unregister_formats();
+ return crm_exit(exit_code);
+}
diff --git a/tools/crm_shadow.8.inc b/tools/crm_shadow.8.inc
new file mode 100644
index 0000000..4c15a95
--- /dev/null
+++ b/tools/crm_shadow.8.inc
@@ -0,0 +1,5 @@
+[=synopsis]
+crm_shadow <query>|<command> [options]
+
+/for side effects./
+.SH OPTIONS
diff --git a/tools/crm_shadow.c b/tools/crm_shadow.c
new file mode 100644
index 0000000..ef69502
--- /dev/null
+++ b/tools/crm_shadow.c
@@ -0,0 +1,1322 @@
+/*
+ * Copyright 2004-2023 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 <stdio.h>
+#include <unistd.h>
+
+#include <sys/param.h>
+#include <crm/crm.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <crm/msg_xml.h>
+
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/ipc.h>
+#include <crm/common/output_internal.h>
+#include <crm/common/xml.h>
+
+#include <crm/cib.h>
+#include <crm/cib/internal.h>
+
+#define SUMMARY "perform Pacemaker configuration changes in a sandbox\n\n" \
+ "This command sets up an environment in which " \
+ "configuration tools (cibadmin,\n" \
+ "crm_resource, etc.) work offline instead of against a " \
+ "live cluster, allowing\n" \
+ "changes to be previewed and tested for side effects."
+
+#define INDENT " "
+
+enum shadow_command {
+ shadow_cmd_none = 0,
+ shadow_cmd_which,
+ shadow_cmd_display,
+ shadow_cmd_diff,
+ shadow_cmd_file,
+ shadow_cmd_create,
+ shadow_cmd_create_empty,
+ shadow_cmd_commit,
+ shadow_cmd_delete,
+ shadow_cmd_edit,
+ shadow_cmd_reset,
+ shadow_cmd_switch,
+};
+
+/*!
+ * \internal
+ * \enum shadow_disp_flags
+ * \brief Bit flags to control which fields of shadow CIB info are displayed
+ *
+ * \note Ignored for XML output.
+ */
+enum shadow_disp_flags {
+ shadow_disp_instance = (1 << 0),
+ shadow_disp_file = (1 << 1),
+ shadow_disp_content = (1 << 2),
+ shadow_disp_diff = (1 << 3),
+};
+
+static crm_exit_t exit_code = CRM_EX_OK;
+
+static struct {
+ enum shadow_command cmd;
+ int cmd_options;
+ char *instance;
+ gboolean force;
+ gboolean batch;
+ gboolean full_upload;
+ gchar *validate_with;
+} options = {
+ .cmd_options = cib_sync_call,
+};
+
+/*!
+ * \internal
+ * \brief Display an instruction to the user
+ *
+ * \param[in,out] out Output object
+ * \param[in] ... Message arguments
+ *
+ * \return Standard Pacemaker return code
+ *
+ * \note The variadic message arguments are of the following format:
+ * -# Instructional message
+ */
+PCMK__OUTPUT_ARGS("instruction", "const char *")
+static int
+instruction_default(pcmk__output_t *out, va_list args)
+{
+ const char *msg = va_arg(args, const char *);
+
+ if (msg == NULL) {
+ return pcmk_rc_no_output;
+ }
+ return out->info(out, "%s", msg);
+}
+
+/*!
+ * \internal
+ * \brief Display an instruction to the user
+ *
+ * \param[in,out] out Output object
+ * \param[in] ... Message arguments
+ *
+ * \return Standard Pacemaker return code
+ *
+ * \note The variadic message arguments are of the following format:
+ * -# Instructional message
+ */
+PCMK__OUTPUT_ARGS("instruction", "const char *")
+static int
+instruction_xml(pcmk__output_t *out, va_list args)
+{
+ const char *msg = va_arg(args, const char *);
+
+ if (msg == NULL) {
+ return pcmk_rc_no_output;
+ }
+ pcmk__output_create_xml_text_node(out, "instruction", msg);
+ return pcmk_rc_ok;
+}
+
+/*!
+ * \internal
+ * \brief Display information about a shadow CIB instance
+ *
+ * \param[in,out] out Output object
+ * \param[in] ... Message arguments
+ *
+ * \return Standard Pacemaker return code
+ *
+ * \note The variadic message arguments are of the following format:
+ * -# Instance name (can be \p NULL)
+ * -# Shadow file name (can be \p NULL)
+ * -# Shadow file content (can be \p NULL)
+ * -# Patchset containing the changes in the shadow CIB (can be \p NULL)
+ * -# Group of \p shadow_disp_flags indicating which fields to display
+ */
+PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "xmlNodePtr",
+ "xmlNodePtr", "enum shadow_disp_flags")
+static int
+shadow_default(pcmk__output_t *out, va_list args)
+{
+ const char *instance = va_arg(args, const char *);
+ const char *filename = va_arg(args, const char *);
+ xmlNodePtr content = va_arg(args, xmlNodePtr);
+ xmlNodePtr diff = va_arg(args, xmlNodePtr);
+ enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int);
+
+ int rc = pcmk_rc_no_output;
+
+ if (pcmk_is_set(flags, shadow_disp_instance)) {
+ rc = out->info(out, "Instance: %s", pcmk__s(instance, "<unknown>"));
+ }
+ if (pcmk_is_set(flags, shadow_disp_file)) {
+ rc = out->info(out, "File name: %s", pcmk__s(filename, "<unknown>"));
+ }
+ if (pcmk_is_set(flags, shadow_disp_content)) {
+ rc = out->info(out, "Content:");
+
+ if (content != NULL) {
+ char *buf = pcmk__trim(dump_xml_formatted_with_text(content));
+
+ if (!pcmk__str_empty(buf)) {
+ out->info(out, "%s", buf);
+ }
+ free(buf);
+
+ } else {
+ out->info(out, "<unknown>");
+ }
+ }
+ if (pcmk_is_set(flags, shadow_disp_diff)) {
+ rc = out->info(out, "Diff:");
+
+ if (diff != NULL) {
+ out->message(out, "xml-patchset", diff);
+ } else {
+ out->info(out, "<empty>");
+ }
+ }
+
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Display information about a shadow CIB instance
+ *
+ * \param[in,out] out Output object
+ * \param[in] ... Message arguments
+ *
+ * \return Standard Pacemaker return code
+ *
+ * \note The variadic message arguments are of the following format:
+ * -# Instance name (can be \p NULL)
+ * -# Shadow file name (can be \p NULL)
+ * -# Shadow file content (can be \p NULL)
+ * -# Patchset containing the changes in the shadow CIB (can be \p NULL)
+ * -# Group of \p shadow_disp_flags indicating which fields to display
+ */
+PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "xmlNodePtr",
+ "xmlNodePtr", "enum shadow_disp_flags")
+static int
+shadow_text(pcmk__output_t *out, va_list args)
+{
+ if (!out->is_quiet(out)) {
+ return shadow_default(out, args);
+
+ } else {
+ const char *instance = va_arg(args, const char *);
+ const char *filename = va_arg(args, const char *);
+ xmlNodePtr content = va_arg(args, xmlNodePtr);
+ xmlNodePtr diff = va_arg(args, xmlNodePtr);
+ enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int);
+
+ int rc = pcmk_rc_no_output;
+ bool quiet_orig = out->quiet;
+
+ /* We have to disable quiet mode for the "xml-patchset" message if we
+ * call it, so we might as well do so for this whole section.
+ */
+ out->quiet = false;
+
+ if (pcmk_is_set(flags, shadow_disp_instance) && (instance != NULL)) {
+ rc = out->info(out, "%s", instance);
+ }
+ if (pcmk_is_set(flags, shadow_disp_file) && (filename != NULL)) {
+ rc = out->info(out, "%s", filename);
+ }
+ if (pcmk_is_set(flags, shadow_disp_content) && (content != NULL)) {
+ char *buf = pcmk__trim(dump_xml_formatted_with_text(content));
+
+ rc = out->info(out, "%s", pcmk__trim(buf));
+ free(buf);
+ }
+ if (pcmk_is_set(flags, shadow_disp_diff) && (diff != NULL)) {
+ rc = out->message(out, "xml-patchset", diff);
+ }
+
+ out->quiet = quiet_orig;
+ return rc;
+ }
+}
+
+/*!
+ * \internal
+ * \brief Display information about a shadow CIB instance
+ *
+ * \param[in,out] out Output object
+ * \param[in] ... Message arguments
+ *
+ * \return Standard Pacemaker return code
+ *
+ * \note The variadic message arguments are of the following format:
+ * -# Instance name (can be \p NULL)
+ * -# Shadow file name (can be \p NULL)
+ * -# Shadow file content (can be \p NULL)
+ * -# Patchset containing the changes in the shadow CIB (can be \p NULL)
+ * -# Group of \p shadow_disp_flags indicating which fields to display
+ * (ignored)
+ */
+PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "xmlNodePtr",
+ "xmlNodePtr", "enum shadow_disp_flags")
+static int
+shadow_xml(pcmk__output_t *out, va_list args)
+{
+ const char *instance = va_arg(args, const char *);
+ const char *filename = va_arg(args, const char *);
+ xmlNodePtr content = va_arg(args, xmlNodePtr);
+ xmlNodePtr diff = va_arg(args, xmlNodePtr);
+ enum shadow_disp_flags flags G_GNUC_UNUSED =
+ (enum shadow_disp_flags) va_arg(args, int);
+
+ pcmk__output_xml_create_parent(out, "shadow",
+ "instance", instance,
+ "file", filename,
+ NULL);
+
+ if (content != NULL) {
+ char *buf = dump_xml_formatted_with_text(content);
+
+ out->output_xml(out, "content", buf);
+ free(buf);
+ }
+
+ if (diff != NULL) {
+ out->message(out, "xml-patchset", diff);
+ }
+
+ pcmk__output_xml_pop_parent(out);
+ return pcmk_rc_ok;
+}
+
+static const pcmk__supported_format_t formats[] = {
+ PCMK__SUPPORTED_FORMAT_NONE,
+ PCMK__SUPPORTED_FORMAT_TEXT,
+ PCMK__SUPPORTED_FORMAT_XML,
+ { NULL, NULL, NULL }
+};
+
+static const pcmk__message_entry_t fmt_functions[] = {
+ { "instruction", "default", instruction_default },
+ { "instruction", "xml", instruction_xml },
+ { "shadow", "default", shadow_default },
+ { "shadow", "text", shadow_text },
+ { "shadow", "xml", shadow_xml },
+
+ { NULL, NULL, NULL }
+};
+
+/*!
+ * \internal
+ * \brief Set the error when \p --force is not passed with a dangerous command
+ *
+ * \param[in] reason Why command is dangerous
+ * \param[in] for_shadow If true, command is dangerous to the shadow file.
+ * Otherwise, command is dangerous to the active
+ * cluster.
+ * \param[in] show_mismatch If true and the supplied shadow instance is not
+ * the same as the active shadow instance, report
+ * this
+ * \param[out] error Where to store error
+ */
+static void
+set_danger_error(const char *reason, bool for_shadow, bool show_mismatch,
+ GError **error)
+{
+ const char *active = getenv("CIB_shadow");
+ char *full = NULL;
+
+ if (show_mismatch
+ && !pcmk__str_eq(active, options.instance, pcmk__str_null_matches)) {
+
+ full = crm_strdup_printf("%s.\nAdditionally, the supplied shadow "
+ "instance (%s) is not the same as the active "
+ "one (%s)",
+ reason, options.instance, active);
+ reason = full;
+ }
+
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "%s%sTo prevent accidental destruction of the %s, the --force "
+ "flag is required in order to proceed.",
+ pcmk__s(reason, ""), ((reason != NULL)? ".\n" : ""),
+ (for_shadow? "shadow file" : "cluster"));
+ free(full);
+}
+
+/*!
+ * \internal
+ * \brief Get the active shadow instance from the environment
+ *
+ * This sets \p options.instance to the value of the \p CIB_shadow env variable.
+ *
+ * \param[out] error Where to store error
+ */
+static int
+get_instance_from_env(GError **error)
+{
+ int rc = pcmk_rc_ok;
+
+ pcmk__str_update(&options.instance, getenv("CIB_shadow"));
+ if (options.instance == NULL) {
+ rc = ENXIO;
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "No active shadow configuration defined");
+ }
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Validate that the shadow file does or does not exist, as appropriate
+ *
+ * \param[in] filename Absolute path of shadow file
+ * \param[in] should_exist Whether the shadow file is expected to exist
+ * \param[out] error Where to store error
+ *
+ * \return Standard Pacemaker return code
+ */
+static int
+check_file_exists(const char *filename, bool should_exist, GError **error)
+{
+ struct stat buf;
+
+ if (!should_exist && (stat(filename, &buf) == 0)) {
+ char *reason = crm_strdup_printf("A shadow instance '%s' already "
+ "exists", options.instance);
+
+ exit_code = CRM_EX_CANTCREAT;
+ set_danger_error(reason, true, false, error);
+ free(reason);
+ return EEXIST;
+ }
+
+ if (should_exist && (stat(filename, &buf) < 0)) {
+ // @COMPAT: Use pcmk_rc2exitc(errno)?
+ exit_code = CRM_EX_NOSUCH;
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "Could not access shadow instance '%s': %s",
+ options.instance, strerror(errno));
+ return errno;
+ }
+
+ return pcmk_rc_ok;
+}
+
+/*!
+ * \internal
+ * \brief Connect to the "real" (non-shadow) CIB
+ *
+ * \param[out] real_cib Where to store CIB connection
+ * \param[out] error Where to store error
+ *
+ * \return Standard Pacemaker return code
+ */
+static int
+connect_real_cib(cib_t **real_cib, GError **error)
+{
+ int rc = pcmk_rc_ok;
+
+ *real_cib = cib_new_no_shadow();
+ if (*real_cib == NULL) {
+ rc = ENOMEM;
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "Could not create a CIB connection object");
+ return rc;
+ }
+
+ rc = (*real_cib)->cmds->signon(*real_cib, crm_system_name, cib_command);
+ rc = pcmk_legacy2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "Could not connect to CIB: %s", pcmk_rc_str(rc));
+ }
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Query the "real" (non-shadow) CIB and store the result
+ *
+ * \param[out] output Where to store query output
+ * \param[out] error Where to store error
+ *
+ * \return Standard Pacemaker return code
+ */
+static int
+query_real_cib(xmlNode **output, GError **error)
+{
+ cib_t *real_cib = NULL;
+ int rc = connect_real_cib(&real_cib, error);
+
+ if (rc != pcmk_rc_ok) {
+ goto done;
+ }
+
+ rc = real_cib->cmds->query(real_cib, NULL, output, options.cmd_options);
+ rc = pcmk_legacy2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "Could not query the non-shadow CIB: %s", pcmk_rc_str(rc));
+ }
+
+done:
+ cib_delete(real_cib);
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Read XML from the given file
+ *
+ * \param[in] filename Path of input file
+ * \param[out] output Where to store XML read from \p filename
+ * \param[out] error Where to store error
+ *
+ * \return Standard Pacemaker return code
+ */
+static int
+read_xml(const char *filename, xmlNode **output, GError **error)
+{
+ int rc = pcmk_rc_ok;
+
+ *output = filename2xml(filename);
+ if (*output == NULL) {
+ rc = pcmk_rc_no_input;
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "Could not parse XML from input file '%s'", filename);
+ }
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Write the shadow XML to a file
+ *
+ * \param[in,out] xml Shadow XML
+ * \param[in] filename Name of destination file
+ * \param[in] reset Whether the write is a reset (for logging only)
+ * \param[out] error Where to store error
+ */
+static int
+write_shadow_file(xmlNode *xml, const char *filename, bool reset,
+ GError **error)
+{
+ int rc = write_xml_file(xml, filename, FALSE);
+
+ if (rc < 0) {
+ rc = pcmk_legacy2rc(rc);
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "Could not %s the shadow instance '%s': %s",
+ reset? "reset" : "create", options.instance,
+ pcmk_rc_str(rc));
+ return rc;
+ }
+ return pcmk_rc_ok;
+}
+
+/*!
+ * \internal
+ * \brief Create a shell prompt based on the given shadow instance name
+ *
+ * \return Newly created prompt
+ *
+ * \note The caller is responsible for freeing the return value using \p free().
+ */
+static inline char *
+get_shadow_prompt(void)
+{
+ return crm_strdup_printf("shadow[%.40s] # ", options.instance);
+}
+
+/*!
+ * \internal
+ * \brief Set up environment variables for a shadow instance
+ *
+ * \param[in,out] out Output object
+ * \param[in] do_switch If true, switch to an existing instance (logging
+ * only)
+ * \param[out] error Where to store error
+ */
+static void
+shadow_setup(pcmk__output_t *out, bool do_switch, GError **error)
+{
+ const char *active = getenv("CIB_shadow");
+ const char *prompt = getenv("PS1");
+ const char *shell = getenv("SHELL");
+ char *new_prompt = get_shadow_prompt();
+
+ if (pcmk__str_eq(active, options.instance, pcmk__str_none)
+ && pcmk__str_eq(new_prompt, prompt, pcmk__str_none)) {
+ // CIB_shadow and prompt environment variables are already set up
+ goto done;
+ }
+
+ if (!options.batch && (shell != NULL)) {
+ out->info(out, "Setting up shadow instance");
+ setenv("PS1", new_prompt, 1);
+ setenv("CIB_shadow", options.instance, 1);
+
+ out->message(out, "instruction",
+ "Press Ctrl+D to exit the crm_shadow shell");
+
+ if (pcmk__str_eq(shell, "(^|/)bash$", pcmk__str_regex)) {
+ execl(shell, shell, "--norc", "--noprofile", NULL);
+ } else {
+ execl(shell, shell, NULL);
+ }
+
+ exit_code = pcmk_rc2exitc(errno);
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "Failed to launch shell '%s': %s",
+ shell, pcmk_rc_str(errno));
+
+ } else {
+ char *msg = NULL;
+ const char *prefix = "A new shadow instance was created. To begin "
+ "using it";
+
+ if (do_switch) {
+ prefix = "To switch to the named shadow instance";
+ }
+
+ msg = crm_strdup_printf("%s, enter the following into your shell:\n"
+ "\texport CIB_shadow=%s",
+ prefix, options.instance);
+ out->message(out, "instruction", msg);
+ free(msg);
+ }
+
+done:
+ free(new_prompt);
+}
+
+/*!
+ * \internal
+ * \brief Remind the user to clean up the shadow environment
+ *
+ * \param[in,out] out Output object
+ */
+static void
+shadow_teardown(pcmk__output_t *out)
+{
+ const char *active = getenv("CIB_shadow");
+ const char *prompt = getenv("PS1");
+
+ if (pcmk__str_eq(active, options.instance, pcmk__str_none)) {
+ char *our_prompt = get_shadow_prompt();
+
+ if (pcmk__str_eq(prompt, our_prompt, pcmk__str_none)) {
+ out->message(out, "instruction",
+ "Press Ctrl+D to exit the crm_shadow shell");
+
+ } else {
+ out->message(out, "instruction",
+ "Remember to unset the CIB_shadow variable by "
+ "entering the following into your shell:\n"
+ "\tunset CIB_shadow");
+ }
+ free(our_prompt);
+ }
+}
+
+/*!
+ * \internal
+ * \brief Commit the shadow file contents to the active cluster
+ *
+ * \param[out] error Where to store error
+ */
+static void
+commit_shadow_file(GError **error)
+{
+ char *filename = NULL;
+ cib_t *real_cib = NULL;
+
+ xmlNodePtr input = NULL;
+ xmlNodePtr section_xml = NULL;
+ const char *section = NULL;
+
+ int rc = pcmk_rc_ok;
+
+ if (!options.force) {
+ const char *reason = "The commit command overwrites the active cluster "
+ "configuration";
+
+ exit_code = CRM_EX_USAGE;
+ set_danger_error(reason, false, true, error);
+ return;
+ }
+
+ filename = get_shadow_file(options.instance);
+ if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
+ goto done;
+ }
+
+ if (connect_real_cib(&real_cib, error) != pcmk_rc_ok) {
+ goto done;
+ }
+
+ if (read_xml(filename, &input, error) != pcmk_rc_ok) {
+ goto done;
+ }
+
+ section_xml = input;
+
+ if (!options.full_upload) {
+ section = XML_CIB_TAG_CONFIGURATION;
+ section_xml = first_named_child(input, section);
+ }
+
+ rc = real_cib->cmds->replace(real_cib, section, section_xml,
+ options.cmd_options);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "Could not commit shadow instance '%s' to the CIB: %s",
+ options.instance, pcmk_rc_str(rc));
+ }
+
+done:
+ free(filename);
+ cib_delete(real_cib);
+ free_xml(input);
+}
+
+/*!
+ * \internal
+ * \brief Create a new empty shadow instance
+ *
+ * \param[in,out] out Output object
+ * \param[out] error Where to store error
+ *
+ * \note If \p --force is given, we try to write the file regardless of whether
+ * it already exists.
+ */
+static void
+create_shadow_empty(pcmk__output_t *out, GError **error)
+{
+ char *filename = get_shadow_file(options.instance);
+ xmlNode *output = NULL;
+
+ if (!options.force
+ && (check_file_exists(filename, false, error) != pcmk_rc_ok)) {
+ goto done;
+ }
+
+ output = createEmptyCib(0);
+ crm_xml_add(output, XML_ATTR_VALIDATION, options.validate_with);
+ out->info(out, "Created new %s configuration",
+ crm_element_value(output, XML_ATTR_VALIDATION));
+
+ if (write_shadow_file(output, filename, false, error) != pcmk_rc_ok) {
+ goto done;
+ }
+ shadow_setup(out, false, error);
+
+done:
+ free(filename);
+ free_xml(output);
+}
+
+/*!
+ * \internal
+ * \brief Create a shadow instance based on the active CIB
+ *
+ * \param[in,out] out Output object
+ * \param[in] reset If true, overwrite the given existing shadow instance.
+ * Otherwise, create a new shadow instance with the given
+ * name.
+ * \param[out] error Where to store error
+ *
+ * \note If \p --force is given, we try to write the file regardless of whether
+ * it already exists.
+ */
+static void
+create_shadow_from_cib(pcmk__output_t *out, bool reset, GError **error)
+{
+ char *filename = get_shadow_file(options.instance);
+ xmlNode *output = NULL;
+
+ if (!options.force) {
+ if (reset) {
+ /* @COMPAT: Reset is dangerous to the shadow file, but to preserve
+ * compatibility we can't require --force unless there's a mismatch.
+ * At a compatibility break, call set_danger_error() with for_shadow
+ * and show_mismatch set to true.
+ */
+ const char *local = getenv("CIB_shadow");
+
+ if (!pcmk__str_eq(local, options.instance, pcmk__str_null_matches)) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "The supplied shadow instance (%s) is not the same "
+ "as the active one (%s).\n"
+ "To prevent accidental destruction of the shadow "
+ "file, the --force flag is required in order to "
+ "proceed.",
+ options.instance, local);
+ goto done;
+ }
+ }
+
+ if (check_file_exists(filename, reset, error) != pcmk_rc_ok) {
+ goto done;
+ }
+ }
+
+ if (query_real_cib(&output, error) != pcmk_rc_ok) {
+ goto done;
+ }
+
+ if (write_shadow_file(output, filename, reset, error) != pcmk_rc_ok) {
+ goto done;
+ }
+ shadow_setup(out, false, error);
+
+done:
+ free(filename);
+ free_xml(output);
+}
+
+/*!
+ * \internal
+ * \brief Delete the shadow file
+ *
+ * \param[in,out] out Output object
+ * \param[out] error Where to store error
+ */
+static void
+delete_shadow_file(pcmk__output_t *out, GError **error)
+{
+ char *filename = NULL;
+
+ if (!options.force) {
+ const char *reason = "The delete command removes the specified shadow "
+ "file";
+
+ exit_code = CRM_EX_USAGE;
+ set_danger_error(reason, true, true, error);
+ return;
+ }
+
+ filename = get_shadow_file(options.instance);
+
+ if ((unlink(filename) < 0) && (errno != ENOENT)) {
+ exit_code = pcmk_rc2exitc(errno);
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "Could not remove shadow instance '%s': %s",
+ options.instance, strerror(errno));
+ } else {
+ shadow_teardown(out);
+ }
+ free(filename);
+}
+
+/*!
+ * \internal
+ * \brief Open the shadow file in a text editor
+ *
+ * \param[out] error Where to store error
+ *
+ * \note The \p EDITOR environment variable must be set.
+ */
+static void
+edit_shadow_file(GError **error)
+{
+ char *filename = NULL;
+ const char *editor = NULL;
+
+ if (get_instance_from_env(error) != pcmk_rc_ok) {
+ return;
+ }
+
+ filename = get_shadow_file(options.instance);
+ if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
+ goto done;
+ }
+
+ editor = getenv("EDITOR");
+ if (editor == NULL) {
+ exit_code = CRM_EX_NOT_CONFIGURED;
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "No value for EDITOR defined");
+ goto done;
+ }
+
+ execlp(editor, "--", filename, NULL);
+ exit_code = CRM_EX_OSFILE;
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "Could not invoke EDITOR (%s %s): %s",
+ editor, filename, strerror(errno));
+
+done:
+ free(filename);
+}
+
+/*!
+ * \internal
+ * \brief Show the contents of the active shadow instance
+ *
+ * \param[in,out] out Output object
+ * \param[out] error Where to store error
+ */
+static void
+show_shadow_contents(pcmk__output_t *out, GError **error)
+{
+ char *filename = NULL;
+
+ if (get_instance_from_env(error) != pcmk_rc_ok) {
+ return;
+ }
+
+ filename = get_shadow_file(options.instance);
+
+ if (check_file_exists(filename, true, error) == pcmk_rc_ok) {
+ xmlNode *output = NULL;
+ bool quiet_orig = out->quiet;
+
+ if (read_xml(filename, &output, error) != pcmk_rc_ok) {
+ goto done;
+ }
+
+ out->quiet = true;
+ out->message(out, "shadow",
+ options.instance, NULL, output, NULL, shadow_disp_content);
+ out->quiet = quiet_orig;
+
+ free_xml(output);
+ }
+
+done:
+ free(filename);
+}
+
+/*!
+ * \internal
+ * \brief Show the changes in the active shadow instance
+ *
+ * \param[in,out] out Output object
+ * \param[out] error Where to store error
+ */
+static void
+show_shadow_diff(pcmk__output_t *out, GError **error)
+{
+ char *filename = NULL;
+ xmlNodePtr old_config = NULL;
+ xmlNodePtr new_config = NULL;
+ xmlNodePtr diff = NULL;
+ pcmk__output_t *logger_out = NULL;
+ bool quiet_orig = out->quiet;
+ int rc = pcmk_rc_ok;
+
+ if (get_instance_from_env(error) != pcmk_rc_ok) {
+ return;
+ }
+
+ filename = get_shadow_file(options.instance);
+ if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
+ goto done;
+ }
+
+ if (query_real_cib(&old_config, error) != pcmk_rc_ok) {
+ goto done;
+ }
+
+ if (read_xml(filename, &new_config, error) != pcmk_rc_ok) {
+ goto done;
+ }
+ xml_track_changes(new_config, NULL, new_config, false);
+ xml_calculate_changes(old_config, new_config);
+ diff = xml_create_patchset(0, old_config, new_config, NULL, false);
+
+ rc = pcmk__log_output_new(&logger_out);
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "Could not create logger object: %s", pcmk_rc_str(rc));
+ goto done;
+ }
+ pcmk__output_set_log_level(logger_out, LOG_INFO);
+ rc = pcmk__xml_show_changes(logger_out, new_config);
+ logger_out->finish(logger_out, pcmk_rc2exitc(rc), true, NULL);
+ pcmk__output_free(logger_out);
+
+ xml_accept_changes(new_config);
+
+ out->quiet = true;
+ out->message(out, "shadow",
+ options.instance, NULL, NULL, diff, shadow_disp_diff);
+ out->quiet = quiet_orig;
+
+ if (diff != NULL) {
+ /* @COMPAT: Exit with CRM_EX_DIGEST? This is not really an error; we
+ * just want to indicate that there are differences (as the diff command
+ * does).
+ */
+ exit_code = CRM_EX_ERROR;
+ }
+
+done:
+ free(filename);
+ free_xml(old_config);
+ free_xml(new_config);
+ free_xml(diff);
+}
+
+/*!
+ * \internal
+ * \brief Show the absolute path of the active shadow instance
+ *
+ * \param[in,out] out Output object
+ * \param[out] error Where to store error
+ */
+static void
+show_shadow_filename(pcmk__output_t *out, GError **error)
+{
+ if (get_instance_from_env(error) == pcmk_rc_ok) {
+ char *filename = get_shadow_file(options.instance);
+ bool quiet_orig = out->quiet;
+
+ out->quiet = true;
+ out->message(out, "shadow",
+ options.instance, filename, NULL, NULL, shadow_disp_file);
+ out->quiet = quiet_orig;
+
+ free(filename);
+ }
+}
+
+/*!
+ * \internal
+ * \brief Show the active shadow instance
+ *
+ * \param[in,out] out Output object
+ * \param[out] error Where to store error
+ */
+static void
+show_shadow_instance(pcmk__output_t *out, GError **error)
+{
+ if (get_instance_from_env(error) == pcmk_rc_ok) {
+ bool quiet_orig = out->quiet;
+
+ out->quiet = true;
+ out->message(out, "shadow",
+ options.instance, NULL, NULL, NULL, shadow_disp_instance);
+ out->quiet = quiet_orig;
+ }
+}
+
+/*!
+ * \internal
+ * \brief Switch to the given shadow instance
+ *
+ * \param[in,out] out Output object
+ * \param[out] error Where to store error
+ */
+static void
+switch_shadow_instance(pcmk__output_t *out, GError **error)
+{
+ char *filename = NULL;
+
+ filename = get_shadow_file(options.instance);
+ if (check_file_exists(filename, true, error) == pcmk_rc_ok) {
+ shadow_setup(out, true, error);
+ }
+ free(filename);
+}
+
+static gboolean
+command_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **error)
+{
+ if (pcmk__str_any_of(option_name, "-w", "--which", NULL)) {
+ options.cmd = shadow_cmd_which;
+
+ } else if (pcmk__str_any_of(option_name, "-p", "--display", NULL)) {
+ options.cmd = shadow_cmd_display;
+
+ } else if (pcmk__str_any_of(option_name, "-d", "--diff", NULL)) {
+ options.cmd = shadow_cmd_diff;
+
+ } else if (pcmk__str_any_of(option_name, "-F", "--file", NULL)) {
+ options.cmd = shadow_cmd_file;
+
+ } else if (pcmk__str_any_of(option_name, "-c", "--create", NULL)) {
+ options.cmd = shadow_cmd_create;
+
+ } else if (pcmk__str_any_of(option_name, "-e", "--create-empty", NULL)) {
+ options.cmd = shadow_cmd_create_empty;
+
+ } else if (pcmk__str_any_of(option_name, "-C", "--commit", NULL)) {
+ options.cmd = shadow_cmd_commit;
+
+ } else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) {
+ options.cmd = shadow_cmd_delete;
+
+ } else if (pcmk__str_any_of(option_name, "-E", "--edit", NULL)) {
+ options.cmd = shadow_cmd_edit;
+
+ } else if (pcmk__str_any_of(option_name, "-r", "--reset", NULL)) {
+ options.cmd = shadow_cmd_reset;
+
+ } else if (pcmk__str_any_of(option_name, "-s", "--switch", NULL)) {
+ options.cmd = shadow_cmd_switch;
+
+ } else {
+ // Should be impossible
+ return FALSE;
+ }
+
+ // optarg may be NULL and that's okay
+ pcmk__str_update(&options.instance, optarg);
+ return TRUE;
+}
+
+static GOptionEntry query_entries[] = {
+ { "which", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Indicate the active shadow copy", NULL },
+
+ { "display", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the contents of the active shadow copy", NULL },
+
+ { "diff", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the changes in the active shadow copy", NULL },
+
+ { "file", 'F', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the location of the active shadow copy file", NULL },
+
+ { NULL }
+};
+
+static GOptionEntry command_entries[] = {
+ { "create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
+ "Create the named shadow copy of the active cluster configuration",
+ "name" },
+
+ { "create-empty", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
+ command_cb,
+ "Create the named shadow copy with an empty cluster configuration.\n"
+ INDENT "Optional: --validate-with", "name" },
+
+ { "commit", 'C', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
+ "Upload the contents of the named shadow copy to the cluster", "name" },
+
+ { "delete", 'D', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
+ "Delete the contents of the named shadow copy", "name" },
+
+ { "edit", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Edit the contents of the active shadow copy with your favorite $EDITOR",
+ NULL },
+
+ { "reset", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
+ "Recreate named shadow copy from the active cluster configuration",
+ "name" },
+
+ { "switch", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
+ "(Advanced) Switch to the named shadow copy", "name" },
+
+ { NULL }
+};
+
+static GOptionEntry addl_entries[] = {
+ { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force,
+ "(Advanced) Force the action to be performed", NULL },
+
+ { "batch", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.batch,
+ "(Advanced) Don't spawn a new shell", NULL },
+
+ { "all", 'a', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.full_upload,
+ "(Advanced) Upload entire CIB, including status, with --commit", NULL },
+
+ { "validate-with", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
+ &options.validate_with,
+ "(Advanced) Create an older configuration version", NULL },
+
+ { NULL }
+};
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args, GOptionGroup **group)
+{
+ const char *desc = NULL;
+ GOptionContext *context = NULL;
+
+ desc = "Examples:\n\n"
+ "Create a blank shadow configuration:\n\n"
+ "\t# crm_shadow --create-empty myShadow\n\n"
+ "Create a shadow configuration from the running cluster\n\n"
+ "\t# crm_shadow --create myShadow\n\n"
+ "Display the current shadow configuration:\n\n"
+ "\t# crm_shadow --display\n\n"
+ "Discard the current shadow configuration (named myShadow):\n\n"
+ "\t# crm_shadow --delete myShadow --force\n\n"
+ "Upload current shadow configuration (named myShadow) to running "
+ "cluster:\n\n"
+ "\t# crm_shadow --commit myShadow\n\n";
+
+ context = pcmk__build_arg_context(args, "text (default), xml", group,
+ "<query>|<command>");
+ g_option_context_set_description(context, desc);
+
+ pcmk__add_arg_group(context, "queries", "Queries:",
+ "Show query help", query_entries);
+ pcmk__add_arg_group(context, "commands", "Commands:",
+ "Show command help", command_entries);
+ pcmk__add_arg_group(context, "additional", "Additional Options:",
+ "Show additional options", addl_entries);
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ int rc = pcmk_rc_ok;
+ pcmk__output_t *out = NULL;
+
+ GError *error = NULL;
+
+ GOptionGroup *output_group = NULL;
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, "CDcersv");
+ GOptionContext *context = build_arg_context(args, &output_group);
+
+ crm_log_preinit(NULL, argc, argv);
+
+ pcmk__register_formats(output_group, formats);
+
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
+ if (rc != pcmk_rc_ok) {
+ exit_code = CRM_EX_ERROR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Error creating output format %s: %s", args->output_ty,
+ pcmk_rc_str(rc));
+ goto done;
+ }
+
+ if (g_strv_length(processed_args) > 1) {
+ gchar *help = g_option_context_get_help(context, TRUE, NULL);
+ GString *extra = g_string_sized_new(128);
+
+ for (int lpc = 1; processed_args[lpc] != NULL; lpc++) {
+ if (extra->len > 0) {
+ g_string_append_c(extra, ' ');
+ }
+ g_string_append(extra, processed_args[lpc]);
+ }
+
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "non-option ARGV-elements: %s\n\n%s", extra->str, help);
+ g_free(help);
+ g_string_free(extra, TRUE);
+ goto done;
+ }
+
+ if (args->version) {
+ out->version(out, false);
+ goto done;
+ }
+
+ pcmk__register_messages(out, fmt_functions);
+
+ if (options.cmd == shadow_cmd_none) {
+ // @COMPAT: Create a default command if other tools have one
+ gchar *help = g_option_context_get_help(context, TRUE, NULL);
+
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Must specify a query or command option\n\n%s", help);
+ g_free(help);
+ goto done;
+ }
+
+ pcmk__cli_init_logging("crm_shadow", args->verbosity);
+
+ if (args->verbosity > 0) {
+ cib__set_call_options(options.cmd_options, crm_system_name,
+ cib_verbose);
+ }
+
+ // Run the command
+ switch (options.cmd) {
+ case shadow_cmd_commit:
+ commit_shadow_file(&error);
+ break;
+ case shadow_cmd_create:
+ create_shadow_from_cib(out, false, &error);
+ break;
+ case shadow_cmd_create_empty:
+ create_shadow_empty(out, &error);
+ break;
+ case shadow_cmd_reset:
+ create_shadow_from_cib(out, true, &error);
+ break;
+ case shadow_cmd_delete:
+ delete_shadow_file(out, &error);
+ break;
+ case shadow_cmd_diff:
+ show_shadow_diff(out, &error);
+ break;
+ case shadow_cmd_display:
+ show_shadow_contents(out, &error);
+ break;
+ case shadow_cmd_edit:
+ edit_shadow_file(&error);
+ break;
+ case shadow_cmd_file:
+ show_shadow_filename(out, &error);
+ break;
+ case shadow_cmd_switch:
+ switch_shadow_instance(out, &error);
+ break;
+ case shadow_cmd_which:
+ show_shadow_instance(out, &error);
+ break;
+ default:
+ // Should never reach this point
+ break;
+ }
+
+done:
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+
+ pcmk__output_and_clear_error(&error, out);
+
+ free(options.instance);
+ g_free(options.validate_with);
+
+ if (out != NULL) {
+ out->finish(out, exit_code, true, NULL);
+ pcmk__output_free(out);
+ }
+
+ crm_exit(exit_code);
+}
diff --git a/tools/crm_simulate.8.inc b/tools/crm_simulate.8.inc
new file mode 100644
index 0000000..8f37f5b
--- /dev/null
+++ b/tools/crm_simulate.8.inc
@@ -0,0 +1,8 @@
+[synopsis]
+crm_simulate <data source> <operation> [options]
+
+/response to events/
+.SH OPTIONS
+
+/only essential output/
+.SH OPERATION SPECIFICATION
diff --git a/tools/crm_simulate.c b/tools/crm_simulate.c
new file mode 100644
index 0000000..932c5bd
--- /dev/null
+++ b/tools/crm_simulate.c
@@ -0,0 +1,587 @@
+/*
+ * Copyright 2009-2023 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 <stdint.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include <sys/stat.h>
+#include <sys/param.h>
+#include <sys/types.h>
+#include <dirent.h>
+
+#include <crm/crm.h>
+#include <crm/cib.h>
+#include <crm/cib/internal.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
+#include <crm/common/output.h>
+#include <crm/common/util.h>
+#include <crm/common/iso8601.h>
+#include <crm/pengine/status.h>
+#include <pacemaker-internal.h>
+#include <pacemaker.h>
+
+#define SUMMARY "crm_simulate - simulate a Pacemaker cluster's response to events"
+
+struct {
+ char *dot_file;
+ char *graph_file;
+ gchar *input_file;
+ pcmk_injections_t *injections;
+ unsigned int flags;
+ gchar *output_file;
+ long long repeat;
+ gboolean store;
+ gchar *test_dir;
+ char *use_date;
+ char *xml_file;
+} options = {
+ .flags = pcmk_sim_show_pending | pcmk_sim_sanitized,
+ .repeat = 1
+};
+
+uint32_t section_opts = 0;
+char *temp_shadow = NULL;
+crm_exit_t exit_code = CRM_EX_OK;
+
+#define INDENT " "
+
+static pcmk__supported_format_t formats[] = {
+ PCMK__SUPPORTED_FORMAT_NONE,
+ PCMK__SUPPORTED_FORMAT_TEXT,
+ PCMK__SUPPORTED_FORMAT_XML,
+ { NULL, NULL, NULL }
+};
+
+static gboolean
+all_actions_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.flags |= pcmk_sim_all_actions;
+ return TRUE;
+}
+
+static gboolean
+attrs_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ section_opts |= pcmk_section_attributes;
+ return TRUE;
+}
+
+static gboolean
+failcounts_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ section_opts |= pcmk_section_failcounts | pcmk_section_failures;
+ return TRUE;
+}
+
+static gboolean
+in_place_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.store = TRUE;
+ options.flags |= pcmk_sim_process | pcmk_sim_simulate;
+ return TRUE;
+}
+
+static gboolean
+live_check_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (options.xml_file) {
+ free(options.xml_file);
+ }
+
+ options.xml_file = NULL;
+ options.flags &= ~pcmk_sim_sanitized;
+ return TRUE;
+}
+
+static gboolean
+node_down_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.injections->node_down = g_list_append(options.injections->node_down, g_strdup(optarg));
+ return TRUE;
+}
+
+static gboolean
+node_fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.injections->node_fail = g_list_append(options.injections->node_fail, g_strdup(optarg));
+ return TRUE;
+}
+
+static gboolean
+node_up_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ pcmk__simulate_node_config = true;
+ options.injections->node_up = g_list_append(options.injections->node_up, g_strdup(optarg));
+ return TRUE;
+}
+
+static gboolean
+op_fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.flags |= pcmk_sim_process | pcmk_sim_simulate;
+ options.injections->op_fail = g_list_append(options.injections->op_fail, g_strdup(optarg));
+ return TRUE;
+}
+
+static gboolean
+op_inject_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.injections->op_inject = g_list_append(options.injections->op_inject, g_strdup(optarg));
+ return TRUE;
+}
+
+static gboolean
+pending_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.flags |= pcmk_sim_show_pending;
+ return TRUE;
+}
+
+static gboolean
+process_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.flags |= pcmk_sim_process;
+ return TRUE;
+}
+
+static gboolean
+quorum_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ pcmk__str_update(&options.injections->quorum, optarg);
+ return TRUE;
+}
+
+static gboolean
+save_dotfile_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.flags |= pcmk_sim_process;
+ pcmk__str_update(&options.dot_file, optarg);
+ return TRUE;
+}
+
+static gboolean
+save_graph_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.flags |= pcmk_sim_process;
+ pcmk__str_update(&options.graph_file, optarg);
+ return TRUE;
+}
+
+static gboolean
+show_scores_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.flags |= pcmk_sim_process | pcmk_sim_show_scores;
+ return TRUE;
+}
+
+static gboolean
+simulate_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.flags |= pcmk_sim_process | pcmk_sim_simulate;
+ return TRUE;
+}
+
+static gboolean
+ticket_activate_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.injections->ticket_activate = g_list_append(options.injections->ticket_activate, g_strdup(optarg));
+ return TRUE;
+}
+
+static gboolean
+ticket_grant_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.injections->ticket_grant = g_list_append(options.injections->ticket_grant, g_strdup(optarg));
+ return TRUE;
+}
+
+static gboolean
+ticket_revoke_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.injections->ticket_revoke = g_list_append(options.injections->ticket_revoke, g_strdup(optarg));
+ return TRUE;
+}
+
+static gboolean
+ticket_standby_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.injections->ticket_standby = g_list_append(options.injections->ticket_standby, g_strdup(optarg));
+ return TRUE;
+}
+
+static gboolean
+utilization_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.flags |= pcmk_sim_process | pcmk_sim_show_utilization;
+ return TRUE;
+}
+
+static gboolean
+watchdog_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ pcmk__str_update(&options.injections->watchdog, optarg);
+ return TRUE;
+}
+
+static gboolean
+xml_file_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ pcmk__str_update(&options.xml_file, optarg);
+ options.flags |= pcmk_sim_sanitized;
+ return TRUE;
+}
+
+static gboolean
+xml_pipe_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ pcmk__str_update(&options.xml_file, "-");
+ options.flags |= pcmk_sim_sanitized;
+ return TRUE;
+}
+
+static GOptionEntry operation_entries[] = {
+ { "run", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, process_cb,
+ "Process the supplied input and show what actions the cluster will take in response",
+ NULL },
+ { "simulate", 'S', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, simulate_cb,
+ "Like --run, but also simulate taking those actions and show the resulting new status",
+ NULL },
+ { "in-place", 'X', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, in_place_cb,
+ "Like --simulate, but also store the results back to the input file",
+ NULL },
+ { "show-attrs", 'A', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attrs_cb,
+ "Show node attributes",
+ NULL },
+ { "show-failcounts", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, failcounts_cb,
+ "Show resource fail counts",
+ NULL },
+ { "show-scores", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, show_scores_cb,
+ "Show allocation scores",
+ NULL },
+ { "show-utilization", 'U', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, utilization_cb,
+ "Show utilization information",
+ NULL },
+ { "profile", 'P', 0, G_OPTION_ARG_FILENAME, &options.test_dir,
+ "Process all the XML files in the named directory to create profiling data",
+ "DIR" },
+ { "repeat", 'N', 0, G_OPTION_ARG_INT, &options.repeat,
+ "With --profile, repeat each test N times and print timings",
+ "N" },
+ /* Deprecated */
+ { "pending", 'j', G_OPTION_FLAG_NO_ARG|G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, pending_cb,
+ "Display pending state if 'record-pending' is enabled",
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry synthetic_entries[] = {
+ { "node-up", 'u', 0, G_OPTION_ARG_CALLBACK, node_up_cb,
+ "Simulate bringing a node online",
+ "NODE" },
+ { "node-down", 'd', 0, G_OPTION_ARG_CALLBACK, node_down_cb,
+ "Simulate taking a node offline",
+ "NODE" },
+ { "node-fail", 'f', 0, G_OPTION_ARG_CALLBACK, node_fail_cb,
+ "Simulate a node failing",
+ "NODE" },
+ { "op-inject", 'i', 0, G_OPTION_ARG_CALLBACK, op_inject_cb,
+ "Generate a failure for the cluster to react to in the simulation.\n"
+ INDENT "See `Operation Specification` help for more information.",
+ "OPSPEC" },
+ { "op-fail", 'F', 0, G_OPTION_ARG_CALLBACK, op_fail_cb,
+ "If the specified task occurs during the simulation, have it fail with return code ${rc}.\n"
+ INDENT "The transition will normally stop at the failed action.\n"
+ INDENT "Save the result with --save-output and re-run with --xml-file.\n"
+ INDENT "See `Operation Specification` help for more information.",
+ "OPSPEC" },
+ { "set-datetime", 't', 0, G_OPTION_ARG_STRING, &options.use_date,
+ "Set date/time (ISO 8601 format, see https://en.wikipedia.org/wiki/ISO_8601)",
+ "DATETIME" },
+ { "quorum", 'q', 0, G_OPTION_ARG_CALLBACK, quorum_cb,
+ "Set to '1' (or 'true') to indicate cluster has quorum",
+ "QUORUM" },
+ { "watchdog", 'w', 0, G_OPTION_ARG_CALLBACK, watchdog_cb,
+ "Set to '1' (or 'true') to indicate cluster has an active watchdog device",
+ "DEVICE" },
+ { "ticket-grant", 'g', 0, G_OPTION_ARG_CALLBACK, ticket_grant_cb,
+ "Simulate granting a ticket",
+ "TICKET" },
+ { "ticket-revoke", 'r', 0, G_OPTION_ARG_CALLBACK, ticket_revoke_cb,
+ "Simulate revoking a ticket",
+ "TICKET" },
+ { "ticket-standby", 'b', 0, G_OPTION_ARG_CALLBACK, ticket_standby_cb,
+ "Simulate making a ticket standby",
+ "TICKET" },
+ { "ticket-activate", 'e', 0, G_OPTION_ARG_CALLBACK, ticket_activate_cb,
+ "Simulate activating a ticket",
+ "TICKET" },
+
+ { NULL }
+};
+
+static GOptionEntry artifact_entries[] = {
+ { "save-input", 'I', 0, G_OPTION_ARG_FILENAME, &options.input_file,
+ "Save the input configuration to the named file",
+ "FILE" },
+ { "save-output", 'O', 0, G_OPTION_ARG_FILENAME, &options.output_file,
+ "Save the output configuration to the named file",
+ "FILE" },
+ { "save-graph", 'G', 0, G_OPTION_ARG_CALLBACK, save_graph_cb,
+ "Save the transition graph (XML format) to the named file",
+ "FILE" },
+ { "save-dotfile", 'D', 0, G_OPTION_ARG_CALLBACK, save_dotfile_cb,
+ "Save the transition graph (DOT format) to the named file",
+ "FILE" },
+ { "all-actions", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, all_actions_cb,
+ "Display all possible actions in DOT graph (even if not part of transition)",
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry source_entries[] = {
+ { "live-check", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, live_check_cb,
+ "Connect to CIB manager and use the current CIB contents as input",
+ NULL },
+ { "xml-file", 'x', 0, G_OPTION_ARG_CALLBACK, xml_file_cb,
+ "Retrieve XML from the named file",
+ "FILE" },
+ { "xml-pipe", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, xml_pipe_cb,
+ "Retrieve XML from stdin",
+ NULL },
+
+ { NULL }
+};
+
+static int
+setup_input(pcmk__output_t *out, const char *input, const char *output,
+ GError **error)
+{
+ int rc = pcmk_rc_ok;
+ xmlNode *cib_object = NULL;
+ char *local_output = NULL;
+
+ if (input == NULL) {
+ /* Use live CIB */
+ rc = cib__signon_query(out, NULL, &cib_object);
+ if (rc != pcmk_rc_ok) {
+ // cib__signon_query() outputs any relevant error
+ return rc;
+ }
+
+ } else if (pcmk__str_eq(input, "-", pcmk__str_casei)) {
+ cib_object = filename2xml(NULL);
+
+ } else {
+ cib_object = filename2xml(input);
+ }
+
+ if (pcmk_find_cib_element(cib_object, XML_CIB_TAG_STATUS) == NULL) {
+ create_xml_node(cib_object, XML_CIB_TAG_STATUS);
+ }
+
+ if (cli_config_update(&cib_object, NULL, FALSE) == FALSE) {
+ free_xml(cib_object);
+ return pcmk_rc_transform_failed;
+ }
+
+ if (validate_xml(cib_object, NULL, FALSE) != TRUE) {
+ free_xml(cib_object);
+ return pcmk_rc_schema_validation;
+ }
+
+ if (output == NULL) {
+ char *pid = pcmk__getpid_s();
+
+ local_output = get_shadow_file(pid);
+ temp_shadow = strdup(local_output);
+ output = local_output;
+ free(pid);
+ }
+
+ rc = write_xml_file(cib_object, output, FALSE);
+ free_xml(cib_object);
+ cib_object = NULL;
+
+ if (rc < 0) {
+ rc = pcmk_legacy2rc(rc);
+ g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_CANTCREAT,
+ "Could not create '%s': %s", output, pcmk_rc_str(rc));
+ return rc;
+ } else {
+ setenv("CIB_file", output, 1);
+ free(local_output);
+ return pcmk_rc_ok;
+ }
+}
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
+ GOptionContext *context = NULL;
+
+ GOptionEntry extra_prog_entries[] = {
+ { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &(args->quiet),
+ "Display only essential output",
+ NULL },
+
+ { NULL }
+ };
+
+ const char *description = "Operation Specification:\n\n"
+ "The OPSPEC in any command line option is of the form\n"
+ "${resource}_${task}_${interval_in_ms}@${node}=${rc}\n"
+ "(memcached_monitor_20000@bart.example.com=7, for example).\n"
+ "${rc} is an OCF return code. For more information on these\n"
+ "return codes, refer to https://clusterlabs.org/pacemaker/doc/2.1/Pacemaker_Administration/html/agents.html#ocf-return-codes\n\n"
+ "Examples:\n\n"
+ "Pretend a recurring monitor action found memcached stopped on node\n"
+ "fred.example.com and, during recovery, that the memcached stop\n"
+ "action failed:\n\n"
+ "\tcrm_simulate -LS --op-inject memcached:0_monitor_20000@bart.example.com=7 "
+ "--op-fail memcached:0_stop_0@fred.example.com=1 --save-output /tmp/memcached-test.xml\n\n"
+ "Now see what the reaction to the stop failed would be:\n\n"
+ "\tcrm_simulate -S --xml-file /tmp/memcached-test.xml\n\n";
+
+ context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
+ pcmk__add_main_args(context, extra_prog_entries);
+ g_option_context_set_description(context, description);
+
+ pcmk__add_arg_group(context, "operations", "Operations:",
+ "Show operations options", operation_entries);
+ pcmk__add_arg_group(context, "synthetic", "Synthetic Cluster Events:",
+ "Show synthetic cluster event options", synthetic_entries);
+ pcmk__add_arg_group(context, "artifact", "Artifact Options:",
+ "Show artifact options", artifact_entries);
+ pcmk__add_arg_group(context, "source", "Data Source:",
+ "Show data source options", source_entries);
+
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ int rc = pcmk_rc_ok;
+ pe_working_set_t *data_set = NULL;
+ pcmk__output_t *out = NULL;
+
+ GError *error = NULL;
+
+ GOptionGroup *output_group = NULL;
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, "bdefgiqrtuwxDFGINOP");
+ GOptionContext *context = build_arg_context(args, &output_group);
+
+ options.injections = calloc(1, sizeof(pcmk_injections_t));
+ if (options.injections == NULL) {
+ rc = ENOMEM;
+ goto done;
+ }
+
+ /* This must come before g_option_context_parse_strv. */
+ options.xml_file = strdup("-");
+
+ pcmk__register_formats(output_group, formats);
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("crm_simulate", args->verbosity);
+
+ rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
+ if (rc != pcmk_rc_ok) {
+ fprintf(stderr, "Error creating output format %s: %s\n",
+ args->output_ty, pcmk_rc_str(rc));
+ exit_code = CRM_EX_ERROR;
+ goto done;
+ }
+
+ if (pcmk__str_eq(args->output_ty, "text", pcmk__str_null_matches) &&
+ !pcmk_is_set(options.flags, pcmk_sim_show_scores) &&
+ !pcmk_is_set(options.flags, pcmk_sim_show_utilization)) {
+ pcmk__force_args(context, &error, "%s --text-fancy", g_get_prgname());
+ } else if (pcmk__str_eq(args->output_ty, "xml", pcmk__str_none)) {
+ pcmk__force_args(context, &error, "%s --xml-simple-list --xml-substitute", g_get_prgname());
+ }
+
+ pe__register_messages(out);
+ pcmk__register_lib_messages(out);
+
+ out->quiet = args->quiet;
+
+ if (args->version) {
+ out->version(out, false);
+ goto done;
+ }
+
+ if (args->verbosity > 0) {
+ options.flags |= pcmk_sim_verbose;
+
+#ifdef PCMK__COMPAT_2_0
+ /* Redirect stderr to stdout so we can grep the output */
+ close(STDERR_FILENO);
+ dup2(STDOUT_FILENO, STDERR_FILENO);
+#endif
+ }
+
+ data_set = pe_new_working_set();
+ if (data_set == NULL) {
+ rc = ENOMEM;
+ g_set_error(&error, PCMK__RC_ERROR, rc, "Could not allocate working set");
+ goto done;
+ }
+
+ if (pcmk_is_set(options.flags, pcmk_sim_show_scores)) {
+ pe__set_working_set_flags(data_set, pe_flag_show_scores);
+ }
+ if (pcmk_is_set(options.flags, pcmk_sim_show_utilization)) {
+ pe__set_working_set_flags(data_set, pe_flag_show_utilization);
+ }
+ pe__set_working_set_flags(data_set, pe_flag_no_compat);
+
+ if (options.test_dir != NULL) {
+ data_set->priv = out;
+ pcmk__profile_dir(options.test_dir, options.repeat, data_set, options.use_date);
+ rc = pcmk_rc_ok;
+ goto done;
+ }
+
+ rc = setup_input(out, options.xml_file,
+ options.store? options.xml_file : options.output_file,
+ &error);
+ if (rc != pcmk_rc_ok) {
+ goto done;
+ }
+
+ rc = pcmk__simulate(data_set, out, options.injections, options.flags, section_opts,
+ options.use_date, options.input_file, options.graph_file,
+ options.dot_file);
+
+ done:
+ pcmk__output_and_clear_error(&error, NULL);
+
+ /* There sure is a lot to free in options. */
+ free(options.dot_file);
+ free(options.graph_file);
+ g_free(options.input_file);
+ g_free(options.output_file);
+ g_free(options.test_dir);
+ free(options.use_date);
+ free(options.xml_file);
+
+ pcmk_free_injections(options.injections);
+ pcmk__free_arg_context(context);
+ g_strfreev(processed_args);
+
+ if (data_set) {
+ pe_free_working_set(data_set);
+ }
+
+ fflush(stderr);
+
+ if (temp_shadow) {
+ unlink(temp_shadow);
+ free(temp_shadow);
+ }
+
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ }
+
+ if (out != NULL) {
+ out->finish(out, exit_code, true, NULL);
+ pcmk__output_free(out);
+ }
+
+ pcmk__unregister_formats();
+ crm_exit(exit_code);
+}
diff --git a/tools/crm_standby.in b/tools/crm_standby.in
new file mode 100755
index 0000000..0911b9d
--- /dev/null
+++ b/tools/crm_standby.in
@@ -0,0 +1,158 @@
+#!@BASH_PATH@
+#
+# Copyright 2009-2018 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.
+#
+
+USAGE_TEXT="Usage: crm_standby <command> [options]
+
+Common options:
+ --help Display this text, then exit
+ --version Display version information, then exit
+ -V, --verbose Specify multiple times to increase debug output
+ -q, --quiet Print only the standby status (if querying)
+
+Commands:
+ -G, --query Query the current value of standby mode (on/off)
+ -v, --update=VALUE Update the value of standby mode (on/off)
+ -D, --delete Let standby mode use default value
+
+Additional Options:
+ -N, --node=NODE Operate on the named node instead of the current one
+ -l, --lifetime=VALUE Until when should the setting take effect
+ (valid values: reboot, forever)
+ -i, --id=VALUE (Advanced) XML ID used to identify standby attribute"
+
+HELP_TEXT="crm_standby - Query, enable, or disable standby mode for a node
+
+Nodes in standby mode may not host cluster resources.
+
+$USAGE_TEXT
+"
+
+exit_usage() {
+ if [ $# -gt 0 ]; then
+ echo "error:" "$@" >&2
+ fi
+ echo
+ echo "$USAGE_TEXT"
+ exit 1
+}
+
+op=""
+options=""
+lifetime=0
+target=""
+
+SHORTOPTS_DEPRECATED="U:Q"
+LONGOPTS_DEPRECATED="uname:,get-value,delete-attr,attr-value:,attr-id:"
+SHORTOPTS="VqGv:DN:l:i:"
+LONGOPTS="help,version,verbose,quiet,query,update:,delete,node:,lifetime:,id:"
+
+TEMP=$(@GETOPT_PATH@ -o ${SHORTOPTS}${SHORTOPTS_DEPRECATED} \
+ --long ${LONGOPTS},${LONGOPTS_DEPRECATED} \
+ -n crm_standby -- "$@")
+if [ $? -ne 0 ]; then
+ exit_usage
+fi
+
+eval set -- "$TEMP" # Quotes around $TEMP are essential
+
+while true ; do
+ case "$1" in
+ --help)
+ echo "$HELP_TEXT"
+ exit 0
+ ;;
+ --version)
+ crm_attribute --version
+ exit 0
+ ;;
+ -q|--quiet|-V|--verbose|-Q)
+ options="$options $1"
+ shift
+ ;;
+ -N|--node|-U|--uname)
+ target="$2"
+ shift
+ shift
+ ;;
+ -G|--query|--get-value)
+ options="$options --query"
+ op=g
+ shift
+ ;;
+ -v|--update|--attr-value)
+ options="$options --update $2"
+ op=u
+ shift
+ shift
+ ;;
+ -D|--delete|--delete-attr)
+ options="$options --delete"
+ op=d
+ shift
+ ;;
+ -l|--lifetime)
+ options="$options --lifetime $2"
+ lifetime=1
+ shift
+ shift
+ ;;
+ -i|--id|--attr-id)
+ options="$options --id $2"
+ shift
+ shift
+ ;;
+ --)
+ shift
+ break
+ ;;
+ *)
+ exit_usage "unknown option '$1'"
+ ;;
+ esac
+done
+
+# It's important to call cluster commands only after arguments are processed,
+# so --version and --help work without problems even if those commands don't.
+if [ "$target" = "" ]; then
+ target=$(crm_node -n)
+fi
+
+options="-N $target -n standby $options"
+if [ x$op = x ]; then
+ options="$options -G"; op=g
+fi
+
+# If the user didn't explicitly specify a lifetime ...
+if [ $lifetime -eq 0 ]; then
+ case $op in
+ g)
+ # For query, report the forever entry if one exists, otherwise
+ # report the reboot entry if one exists, otherwise report off.
+ crm_attribute $options -l forever >/dev/null 2>&1
+ if [ $? -eq 0 ]; then
+ options="$options -l forever"
+ else
+ options="$options -l reboot -d off"
+ fi
+ ;;
+ u)
+ # For update, default to updating the forever entry.
+ options="$options -l forever"
+ ;;
+ d)
+ # For delete, default to deleting both forever and reboot entries.
+ crm_attribute $options -l forever
+ crm_attribute $options -l reboot
+ exit 0
+ ;;
+ esac
+fi
+
+crm_attribute $options
diff --git a/tools/crm_ticket.8.inc b/tools/crm_ticket.8.inc
new file mode 100644
index 0000000..5ae5124
--- /dev/null
+++ b/tools/crm_ticket.8.inc
@@ -0,0 +1,5 @@
+[synopsis]
+crm_ticket <query>|<command> [options]
+
+/and deleted./
+.SH OPTIONS
diff --git a/tools/crm_ticket.c b/tools/crm_ticket.c
new file mode 100644
index 0000000..c451e8a
--- /dev/null
+++ b/tools/crm_ticket.c
@@ -0,0 +1,1007 @@
+/*
+ * Copyright 2012-2023 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 <sys/param.h>
+
+#include <crm/crm.h>
+
+#include <stdio.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+
+#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
+#include <crm/common/ipc.h>
+#include <crm/common/cmdline_internal.h>
+
+#include <crm/cib.h>
+#include <crm/cib/internal.h>
+#include <crm/pengine/rules.h>
+#include <crm/pengine/status.h>
+
+#include <pacemaker-internal.h>
+
+GError *error = NULL;
+
+#define SUMMARY "Perform tasks related to cluster tickets\n\n" \
+ "Allows ticket attributes to be queried, modified and deleted."
+
+struct {
+ gchar *attr_default;
+ gchar *attr_id;
+ char *attr_name;
+ char *attr_value;
+ gboolean force;
+ char *get_attr_name;
+ gboolean quiet;
+ gchar *set_name;
+ char ticket_cmd;
+ gchar *ticket_id;
+ gchar *xml_file;
+} options = {
+ .ticket_cmd = 'S'
+};
+
+GList *attr_delete;
+GHashTable *attr_set;
+bool modified = false;
+int cib_options = cib_sync_call;
+
+#define INDENT " "
+
+static gboolean
+attr_value_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ pcmk__str_update(&options.attr_value, optarg);
+
+ if (!options.attr_name || !options.attr_value) {
+ return TRUE;
+ }
+
+ g_hash_table_insert(attr_set, strdup(options.attr_name), strdup(options.attr_value));
+ pcmk__str_update(&options.attr_name, NULL);
+ pcmk__str_update(&options.attr_value, NULL);
+
+ modified = true;
+
+ return TRUE;
+}
+
+static gboolean
+command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ if (pcmk__str_any_of(option_name, "--info", "-l", NULL)) {
+ options.ticket_cmd = 'l';
+ } else if (pcmk__str_any_of(option_name, "--details", "-L", NULL)) {
+ options.ticket_cmd = 'L';
+ } else if (pcmk__str_any_of(option_name, "--raw", "-w", NULL)) {
+ options.ticket_cmd = 'w';
+ } else if (pcmk__str_any_of(option_name, "--query-xml", "-q", NULL)) {
+ options.ticket_cmd = 'q';
+ } else if (pcmk__str_any_of(option_name, "--constraints", "-c", NULL)) {
+ options.ticket_cmd = 'c';
+ } else if (pcmk__str_any_of(option_name, "--cleanup", "-C", NULL)) {
+ options.ticket_cmd = 'C';
+ }
+
+ return TRUE;
+}
+
+static gboolean
+delete_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ attr_delete = g_list_append(attr_delete, strdup(optarg));
+ modified = true;
+ return TRUE;
+}
+
+static gboolean
+get_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ pcmk__str_update(&options.get_attr_name, optarg);
+ options.ticket_cmd = 'G';
+ return TRUE;
+}
+
+static gboolean
+grant_standby_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ if (pcmk__str_any_of(option_name, "--grant", "-g", NULL)) {
+ g_hash_table_insert(attr_set, strdup("granted"), strdup("true"));
+ modified = true;
+ } else if (pcmk__str_any_of(option_name, "--revoke", "-r", NULL)) {
+ g_hash_table_insert(attr_set, strdup("granted"), strdup("false"));
+ modified = true;
+ } else if (pcmk__str_any_of(option_name, "--standby", "-s", NULL)) {
+ g_hash_table_insert(attr_set, strdup("standby"), strdup("true"));
+ modified = true;
+ } else if (pcmk__str_any_of(option_name, "--activate", "-a", NULL)) {
+ g_hash_table_insert(attr_set, strdup("standby"), strdup("false"));
+ modified = true;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+set_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
+ pcmk__str_update(&options.attr_name, optarg);
+
+ if (!options.attr_name || !options.attr_value) {
+ return TRUE;
+ }
+
+ g_hash_table_insert(attr_set, strdup(options.attr_name), strdup(options.attr_value));
+ pcmk__str_update(&options.attr_name, NULL);
+ pcmk__str_update(&options.attr_value, NULL);
+
+ modified = true;
+
+ return TRUE;
+}
+
+static GOptionEntry query_entries[] = {
+ { "info", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the information of ticket(s)",
+ NULL },
+
+ { "details", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the details of ticket(s)",
+ NULL },
+
+ { "raw", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the IDs of ticket(s)",
+ NULL },
+
+ { "query-xml", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Query the XML of ticket(s)",
+ NULL },
+
+ { "constraints", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the rsc_ticket constraints that apply to ticket(s)",
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry command_entries[] = {
+ { "grant", 'g', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb,
+ "Grant a ticket to this cluster site",
+ NULL },
+
+ { "revoke", 'r', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb,
+ "Revoke a ticket from this cluster site",
+ NULL },
+
+ { "standby", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb,
+ "Tell this cluster site this ticket is standby",
+ NULL },
+
+ { "activate", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb,
+ "Tell this cluster site this ticket is active",
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry advanced_entries[] = {
+ { "get-attr", 'G', 0, G_OPTION_ARG_CALLBACK, get_attr_cb,
+ "Display the named attribute for a ticket",
+ "ATTRIBUTE" },
+
+ { "set-attr", 'S', 0, G_OPTION_ARG_CALLBACK, set_attr_cb,
+ "Set the named attribute for a ticket",
+ "ATTRIBUTE" },
+
+ { "delete-attr", 'D', 0, G_OPTION_ARG_CALLBACK, delete_attr_cb,
+ "Delete the named attribute for a ticket",
+ "ATTRIBUTE" },
+
+ { "cleanup", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Delete all state of a ticket at this cluster site",
+ NULL },
+
+ { NULL}
+};
+
+static GOptionEntry addl_entries[] = {
+ { "attr-value", 'v', 0, G_OPTION_ARG_CALLBACK, attr_value_cb,
+ "Attribute value to use with -S",
+ "VALUE" },
+
+ { "default", 'd', 0, G_OPTION_ARG_STRING, &options.attr_default,
+ "(Advanced) Default attribute value to display if none is found\n"
+ INDENT "(for use with -G)",
+ "VALUE" },
+
+ { "force", 'f', 0, G_OPTION_ARG_NONE, &options.force,
+ "(Advanced) Force the action to be performed",
+ NULL },
+
+ { "ticket", 't', 0, G_OPTION_ARG_STRING, &options.ticket_id,
+ "Ticket ID",
+ "ID" },
+
+ { "xml-file", 'x', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.xml_file,
+ NULL,
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry deprecated_entries[] = {
+ { "set-name", 'n', 0, G_OPTION_ARG_STRING, &options.set_name,
+ "(Advanced) ID of the instance_attributes object to change",
+ "ID" },
+
+ { "nvpair", 'i', 0, G_OPTION_ARG_STRING, &options.attr_id,
+ "(Advanced) ID of the nvpair object to change/delete",
+ "ID" },
+
+ { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &options.quiet,
+ "Print only the value on stdout",
+ NULL },
+
+ { NULL }
+};
+
+static pe_ticket_t *
+find_ticket(gchar *ticket_id, pe_working_set_t * data_set)
+{
+ return g_hash_table_lookup(data_set->tickets, ticket_id);
+}
+
+static void
+print_date(time_t time)
+{
+ int lpc = 0;
+ char date_str[26];
+
+ asctime_r(localtime(&time), date_str);
+ for (; lpc < 26; lpc++) {
+ if (date_str[lpc] == '\n') {
+ date_str[lpc] = 0;
+ }
+ }
+ fprintf(stdout, "'%s'", date_str);
+}
+
+static void
+print_ticket(pe_ticket_t * ticket, bool raw, bool details)
+{
+ if (raw) {
+ fprintf(stdout, "%s\n", ticket->id);
+ return;
+ }
+
+ fprintf(stdout, "%s\t%s %s",
+ ticket->id, ticket->granted ? "granted" : "revoked",
+ ticket->standby ? "[standby]" : " ");
+
+ if (details && g_hash_table_size(ticket->state) > 0) {
+ GHashTableIter iter;
+ const char *name = NULL;
+ const char *value = NULL;
+ int lpc = 0;
+
+ fprintf(stdout, " (");
+
+ g_hash_table_iter_init(&iter, ticket->state);
+ while (g_hash_table_iter_next(&iter, (void **)&name, (void **)&value)) {
+ if (lpc > 0) {
+ fprintf(stdout, ", ");
+ }
+ fprintf(stdout, "%s=", name);
+ if (pcmk__str_any_of(name, "last-granted", "expires", NULL)) {
+ long long time_ll;
+
+ pcmk__scan_ll(value, &time_ll, 0);
+ print_date((time_t) time_ll);
+ } else {
+ fprintf(stdout, "%s", value);
+ }
+ lpc++;
+ }
+
+ fprintf(stdout, ")\n");
+
+ } else {
+ if (ticket->last_granted > -1) {
+ fprintf(stdout, " last-granted=");
+ print_date(ticket->last_granted);
+ }
+ fprintf(stdout, "\n");
+ }
+
+ return;
+}
+
+static void
+print_ticket_list(pe_working_set_t * data_set, bool raw, bool details)
+{
+ GHashTableIter iter;
+ pe_ticket_t *ticket = NULL;
+
+ g_hash_table_iter_init(&iter, data_set->tickets);
+
+ while (g_hash_table_iter_next(&iter, NULL, (void **)&ticket)) {
+ print_ticket(ticket, raw, details);
+ }
+}
+
+static int
+find_ticket_state(cib_t * the_cib, gchar *ticket_id, xmlNode ** ticket_state_xml)
+{
+ int rc = pcmk_rc_ok;
+ xmlNode *xml_search = NULL;
+
+ GString *xpath = NULL;
+
+ CRM_ASSERT(ticket_state_xml != NULL);
+ *ticket_state_xml = NULL;
+
+ xpath = g_string_sized_new(1024);
+ g_string_append(xpath,
+ "/" XML_TAG_CIB "/" XML_CIB_TAG_STATUS
+ "/" XML_CIB_TAG_TICKETS);
+
+ if (ticket_id != NULL) {
+ pcmk__g_strcat(xpath,
+ "/" XML_CIB_TAG_TICKET_STATE
+ "[@" XML_ATTR_ID "=\"", ticket_id, "\"]", NULL);
+ }
+
+ rc = the_cib->cmds->query(the_cib, (const char *) xpath->str, &xml_search,
+ cib_sync_call | cib_scope_local | cib_xpath);
+ rc = pcmk_legacy2rc(rc);
+ g_string_free(xpath, TRUE);
+
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
+
+ crm_log_xml_debug(xml_search, "Match");
+ if (xml_has_children(xml_search)) {
+ if (ticket_id) {
+ fprintf(stdout, "Multiple ticket_states match ticket_id=%s\n", ticket_id);
+ }
+ *ticket_state_xml = xml_search;
+ } else {
+ *ticket_state_xml = xml_search;
+ }
+ return rc;
+}
+
+static int
+find_ticket_constraints(cib_t * the_cib, gchar *ticket_id, xmlNode ** ticket_cons_xml)
+{
+ int rc = pcmk_rc_ok;
+ xmlNode *xml_search = NULL;
+
+ GString *xpath = NULL;
+ const char *xpath_base = NULL;
+
+ CRM_ASSERT(ticket_cons_xml != NULL);
+ *ticket_cons_xml = NULL;
+
+ xpath_base = pcmk_cib_xpath_for(XML_CIB_TAG_CONSTRAINTS);
+ if (xpath_base == NULL) {
+ crm_err(XML_CIB_TAG_CONSTRAINTS " CIB element not known (bug?)");
+ return -ENOMSG;
+ }
+
+ xpath = g_string_sized_new(1024);
+ pcmk__g_strcat(xpath, xpath_base, "/" XML_CONS_TAG_RSC_TICKET, NULL);
+
+ if (ticket_id != NULL) {
+ pcmk__g_strcat(xpath,
+ "[@" XML_TICKET_ATTR_TICKET "=\"", ticket_id, "\"]",
+ NULL);
+ }
+
+ rc = the_cib->cmds->query(the_cib, (const char *) xpath->str, &xml_search,
+ cib_sync_call | cib_scope_local | cib_xpath);
+ rc = pcmk_legacy2rc(rc);
+ g_string_free(xpath, TRUE);
+
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
+
+ crm_log_xml_debug(xml_search, "Match");
+ *ticket_cons_xml = xml_search;
+
+ return rc;
+}
+
+static int
+dump_ticket_xml(cib_t * the_cib, gchar *ticket_id)
+{
+ int rc = pcmk_rc_ok;
+ xmlNode *state_xml = NULL;
+
+ rc = find_ticket_state(the_cib, ticket_id, &state_xml);
+
+ if (state_xml == NULL) {
+ return rc;
+ }
+
+ fprintf(stdout, "State XML:\n");
+ if (state_xml) {
+ char *state_xml_str = NULL;
+
+ state_xml_str = dump_xml_formatted(state_xml);
+ fprintf(stdout, "\n%s", pcmk__s(state_xml_str, "<null>\n"));
+ free_xml(state_xml);
+ free(state_xml_str);
+ }
+
+ return rc;
+}
+
+static int
+dump_constraints(cib_t * the_cib, gchar *ticket_id)
+{
+ int rc = pcmk_rc_ok;
+ xmlNode *cons_xml = NULL;
+ char *cons_xml_str = NULL;
+
+ rc = find_ticket_constraints(the_cib, ticket_id, &cons_xml);
+
+ if (cons_xml == NULL) {
+ return rc;
+ }
+
+ cons_xml_str = dump_xml_formatted(cons_xml);
+ fprintf(stdout, "Constraints XML:\n\n%s",
+ pcmk__s(cons_xml_str, "<null>\n"));
+ free_xml(cons_xml);
+ free(cons_xml_str);
+
+ return rc;
+}
+
+static int
+get_ticket_state_attr(gchar *ticket_id, const char *attr_name, const char **attr_value,
+ pe_working_set_t * data_set)
+{
+ pe_ticket_t *ticket = NULL;
+
+ CRM_ASSERT(attr_value != NULL);
+ *attr_value = NULL;
+
+ ticket = g_hash_table_lookup(data_set->tickets, ticket_id);
+ if (ticket == NULL) {
+ return ENXIO;
+ }
+
+ *attr_value = g_hash_table_lookup(ticket->state, attr_name);
+ if (*attr_value == NULL) {
+ return ENXIO;
+ }
+
+ return pcmk_rc_ok;
+}
+
+static void
+ticket_warning(gchar *ticket_id, const char *action)
+{
+ GString *warning = g_string_sized_new(1024);
+ const char *word = NULL;
+
+ CRM_ASSERT(action != NULL);
+
+ if (strcmp(action, "grant") == 0) {
+ pcmk__g_strcat(warning,
+ "This command cannot help you verify whether '",
+ ticket_id,
+ "' has been already granted elsewhere.\n", NULL);
+ word = "to";
+
+ } else {
+ pcmk__g_strcat(warning,
+ "Revoking '", ticket_id, "' can trigger the specified "
+ "'loss-policy'(s) relating to '", ticket_id, "'.\n\n"
+ "You can check that with:\n"
+ "crm_ticket --ticket ", ticket_id, " --constraints\n\n"
+ "Otherwise before revoking '", ticket_id, "', "
+ "you may want to make '", ticket_id, "' "
+ "standby with:\n"
+ "crm_ticket --ticket ", ticket_id, " --standby\n\n",
+ NULL);
+ word = "from";
+ }
+
+ pcmk__g_strcat(warning,
+ "If you really want to ", action, " '", ticket_id, "' ",
+ word, " this site now, and you know what you are doing,\n"
+ "please specify --force.", NULL);
+
+ fprintf(stdout, "%s\n", (const char *) warning->str);
+
+ g_string_free(warning, TRUE);
+}
+
+static bool
+allow_modification(gchar *ticket_id)
+{
+ const char *value = NULL;
+ GList *list_iter = NULL;
+
+ if (options.force) {
+ return true;
+ }
+
+ if (g_hash_table_lookup_extended(attr_set, "granted", NULL, (gpointer *) & value)) {
+ if (crm_is_true(value)) {
+ ticket_warning(ticket_id, "grant");
+ return false;
+
+ } else {
+ ticket_warning(ticket_id, "revoke");
+ return false;
+ }
+ }
+
+ for(list_iter = attr_delete; list_iter; list_iter = list_iter->next) {
+ const char *key = (const char *)list_iter->data;
+
+ if (pcmk__str_eq(key, "granted", pcmk__str_casei)) {
+ ticket_warning(ticket_id, "revoke");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static int
+modify_ticket_state(gchar * ticket_id, cib_t * cib, pe_working_set_t * data_set)
+{
+ int rc = pcmk_rc_ok;
+ xmlNode *xml_top = NULL;
+ xmlNode *ticket_state_xml = NULL;
+ bool found = false;
+
+ GList *list_iter = NULL;
+ GHashTableIter hash_iter;
+
+ char *key = NULL;
+ char *value = NULL;
+
+ pe_ticket_t *ticket = NULL;
+
+ rc = find_ticket_state(cib, ticket_id, &ticket_state_xml);
+ if (rc == pcmk_rc_ok) {
+ crm_debug("Found a match state for ticket: id=%s", ticket_id);
+ xml_top = ticket_state_xml;
+ found = true;
+
+ } else if (rc != ENXIO) {
+ return rc;
+
+ } else if (g_hash_table_size(attr_set) == 0){
+ return pcmk_rc_ok;
+
+ } else {
+ xmlNode *xml_obj = NULL;
+
+ xml_top = create_xml_node(NULL, XML_CIB_TAG_STATUS);
+ xml_obj = create_xml_node(xml_top, XML_CIB_TAG_TICKETS);
+ ticket_state_xml = create_xml_node(xml_obj, XML_CIB_TAG_TICKET_STATE);
+ crm_xml_add(ticket_state_xml, XML_ATTR_ID, ticket_id);
+ }
+
+ for(list_iter = attr_delete; list_iter; list_iter = list_iter->next) {
+ const char *key = (const char *)list_iter->data;
+ xml_remove_prop(ticket_state_xml, key);
+ }
+
+ ticket = find_ticket(ticket_id, data_set);
+
+ g_hash_table_iter_init(&hash_iter, attr_set);
+ while (g_hash_table_iter_next(&hash_iter, (gpointer *) & key, (gpointer *) & value)) {
+ crm_xml_add(ticket_state_xml, key, value);
+
+ if (pcmk__str_eq(key, "granted", pcmk__str_casei)
+ && (ticket == NULL || ticket->granted == FALSE)
+ && crm_is_true(value)) {
+
+ char *now = pcmk__ttoa(time(NULL));
+
+ crm_xml_add(ticket_state_xml, "last-granted", now);
+ free(now);
+ }
+ }
+
+ if (found && (attr_delete != NULL)) {
+ crm_log_xml_debug(xml_top, "Replace");
+ rc = cib->cmds->replace(cib, XML_CIB_TAG_STATUS, ticket_state_xml, cib_options);
+ rc = pcmk_legacy2rc(rc);
+
+ } else {
+ crm_log_xml_debug(xml_top, "Update");
+ rc = cib->cmds->modify(cib, XML_CIB_TAG_STATUS, xml_top, cib_options);
+ rc = pcmk_legacy2rc(rc);
+ }
+
+ free_xml(xml_top);
+ return rc;
+}
+
+static int
+delete_ticket_state(gchar *ticket_id, cib_t * cib)
+{
+ xmlNode *ticket_state_xml = NULL;
+
+ int rc = pcmk_rc_ok;
+
+ rc = find_ticket_state(cib, ticket_id, &ticket_state_xml);
+
+ if (rc == ENXIO) {
+ return pcmk_rc_ok;
+
+ } else if (rc != pcmk_rc_ok) {
+ return rc;
+ }
+
+ crm_log_xml_debug(ticket_state_xml, "Delete");
+
+ rc = cib->cmds->remove(cib, XML_CIB_TAG_STATUS, ticket_state_xml, cib_options);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc == pcmk_rc_ok) {
+ fprintf(stdout, "Cleaned up %s\n", ticket_id);
+ }
+
+ free_xml(ticket_state_xml);
+ return rc;
+}
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args) {
+ GOptionContext *context = NULL;
+
+ const char *description = "Examples:\n\n"
+ "Display the info of tickets:\n\n"
+ "\tcrm_ticket --info\n\n"
+ "Display the detailed info of tickets:\n\n"
+ "\tcrm_ticket --details\n\n"
+ "Display the XML of 'ticketA':\n\n"
+ "\tcrm_ticket --ticket ticketA --query-xml\n\n"
+ "Display the rsc_ticket constraints that apply to 'ticketA':\n\n"
+ "\tcrm_ticket --ticket ticketA --constraints\n\n"
+ "Grant 'ticketA' to this cluster site:\n\n"
+ "\tcrm_ticket --ticket ticketA --grant\n\n"
+ "Revoke 'ticketA' from this cluster site:\n\n"
+ "\tcrm_ticket --ticket ticketA --revoke\n\n"
+ "Make 'ticketA' standby (the cluster site will treat a granted\n"
+ "'ticketA' as 'standby', and the dependent resources will be\n"
+ "stopped or demoted gracefully without triggering loss-policies):\n\n"
+ "\tcrm_ticket --ticket ticketA --standby\n\n"
+ "Activate 'ticketA' from being standby:\n\n"
+ "\tcrm_ticket --ticket ticketA --activate\n\n"
+ "Get the value of the 'granted' attribute for 'ticketA':\n\n"
+ "\tcrm_ticket --ticket ticketA --get-attr granted\n\n"
+ "Set the value of the 'standby' attribute for 'ticketA':\n\n"
+ "\tcrm_ticket --ticket ticketA --set-attr standby --attr-value true\n\n"
+ "Delete the 'granted' attribute for 'ticketA':\n\n"
+ "\tcrm_ticket --ticket ticketA --delete-attr granted\n\n"
+ "Erase the operation history of 'ticketA' at this cluster site,\n"
+ "causing the cluster site to 'forget' the existing ticket state:\n\n"
+ "\tcrm_ticket --ticket ticketA --cleanup\n\n";
+
+ context = pcmk__build_arg_context(args, NULL, NULL, NULL);
+ g_option_context_set_description(context, description);
+
+ pcmk__add_arg_group(context, "queries", "Queries:",
+ "Show queries", query_entries);
+ pcmk__add_arg_group(context, "commands", "Commands:",
+ "Show command options", command_entries);
+ pcmk__add_arg_group(context, "advanced", "Advanced Options:",
+ "Show advanced options", advanced_entries);
+ pcmk__add_arg_group(context, "additional", "Additional Options:",
+ "Show additional options", addl_entries);
+ pcmk__add_arg_group(context, "deprecated", "Deprecated Options:",
+ "Show deprecated options", deprecated_entries);
+
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ pe_working_set_t *data_set = NULL;
+ xmlNode *cib_xml_copy = NULL;
+
+ cib_t *cib_conn = NULL;
+ crm_exit_t exit_code = CRM_EX_OK;
+ int rc = pcmk_rc_ok;
+
+ pcmk__common_args_t *args = NULL;
+ GOptionContext *context = NULL;
+ gchar **processed_args = NULL;
+
+ attr_set = pcmk__strkey_table(free, free);
+ attr_delete = NULL;
+
+ args = pcmk__new_common_args(SUMMARY);
+ context = build_arg_context(args);
+ processed_args = pcmk__cmdline_preproc(argv, "dintvxCDGS");
+
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("crm_ticket", args->verbosity);
+
+ if (args->version) {
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+ /* FIXME: When crm_ticket is converted to use formatted output, this can go. */
+ pcmk__cli_help('v');
+ }
+
+ data_set = pe_new_working_set();
+ if (data_set == NULL) {
+ crm_perror(LOG_CRIT, "Could not allocate working set");
+ exit_code = CRM_EX_OSERR;
+ goto done;
+ }
+ pe__set_working_set_flags(data_set, pe_flag_no_counts|pe_flag_no_compat);
+
+ cib_conn = cib_new();
+ if (cib_conn == NULL) {
+ exit_code = CRM_EX_DISCONNECT;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB manager");
+ goto done;
+ }
+
+ rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB: %s",
+ pcmk_rc_str(rc));
+ goto done;
+ }
+
+ if (options.xml_file != NULL) {
+ cib_xml_copy = filename2xml(options.xml_file);
+
+ } else {
+ rc = cib_conn->cmds->query(cib_conn, NULL, &cib_xml_copy, cib_scope_local | cib_sync_call);
+ rc = pcmk_legacy2rc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ exit_code = pcmk_rc2exitc(rc);
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not get local CIB: %s",
+ pcmk_rc_str(rc));
+ goto done;
+ }
+ }
+
+ if (!cli_config_update(&cib_xml_copy, NULL, FALSE)) {
+ exit_code = CRM_EX_CONFIG;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Could not update local CIB to latest schema version");
+ goto done;
+ }
+
+ data_set->input = cib_xml_copy;
+ data_set->now = crm_time_new(NULL);
+
+ cluster_status(data_set);
+
+ /* For recording the tickets that are referenced in rsc_ticket constraints
+ * but have never been granted yet. */
+ pcmk__unpack_constraints(data_set);
+
+ if (options.ticket_cmd == 'l' || options.ticket_cmd == 'L' || options.ticket_cmd == 'w') {
+ bool raw = false;
+ bool details = false;
+
+ if (options.ticket_cmd == 'L') {
+ details = true;
+ } else if (options.ticket_cmd == 'w') {
+ raw = true;
+ }
+
+ if (options.ticket_id) {
+ pe_ticket_t *ticket = find_ticket(options.ticket_id, data_set);
+
+ if (ticket == NULL) {
+ exit_code = CRM_EX_NOSUCH;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "No such ticket '%s'", options.ticket_id);
+ goto done;
+ }
+ print_ticket(ticket, raw, details);
+
+ } else {
+ print_ticket_list(data_set, raw, details);
+ }
+
+ } else if (options.ticket_cmd == 'q') {
+ rc = dump_ticket_xml(cib_conn, options.ticket_id);
+ exit_code = pcmk_rc2exitc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Could not query ticket XML: %s", pcmk_rc_str(rc));
+ }
+
+ } else if (options.ticket_cmd == 'c') {
+ rc = dump_constraints(cib_conn, options.ticket_id);
+ exit_code = pcmk_rc2exitc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Could not show ticket constraints: %s", pcmk_rc_str(rc));
+ }
+
+ } else if (options.ticket_cmd == 'G') {
+ const char *value = NULL;
+
+ if (options.ticket_id == NULL) {
+ exit_code = CRM_EX_NOSUCH;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Must supply ticket ID with -t");
+ goto done;
+ }
+
+ rc = get_ticket_state_attr(options.ticket_id, options.get_attr_name, &value, data_set);
+ if (rc == pcmk_rc_ok) {
+ fprintf(stdout, "%s\n", value);
+ } else if (rc == ENXIO && options.attr_default) {
+ fprintf(stdout, "%s\n", options.attr_default);
+ rc = pcmk_rc_ok;
+ }
+ exit_code = pcmk_rc2exitc(rc);
+
+ } else if (options.ticket_cmd == 'C') {
+ if (options.ticket_id == NULL) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Must supply ticket ID with -t");
+ goto done;
+ }
+
+ if (options.force == FALSE) {
+ pe_ticket_t *ticket = NULL;
+
+ ticket = find_ticket(options.ticket_id, data_set);
+ if (ticket == NULL) {
+ exit_code = CRM_EX_NOSUCH;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "No such ticket '%s'", options.ticket_id);
+ goto done;
+ }
+
+ if (ticket->granted) {
+ ticket_warning(options.ticket_id, "revoke");
+ exit_code = CRM_EX_INSUFFICIENT_PRIV;
+ goto done;
+ }
+ }
+
+ rc = delete_ticket_state(options.ticket_id, cib_conn);
+ exit_code = pcmk_rc2exitc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Could not clean up ticket: %s", pcmk_rc_str(rc));
+ }
+
+ } else if (modified) {
+ if (options.ticket_id == NULL) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Must supply ticket ID with -t");
+ goto done;
+ }
+
+ if (options.attr_value
+ && (pcmk__str_empty(options.attr_name))) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Must supply attribute name with -S for -v %s", options.attr_value);
+ goto done;
+ }
+
+ if (options.attr_name
+ && (pcmk__str_empty(options.attr_value))) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Must supply attribute value with -v for -S %s", options.attr_value);
+ goto done;
+ }
+
+ if (!allow_modification(options.ticket_id)) {
+ exit_code = CRM_EX_INSUFFICIENT_PRIV;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Ticket modification not allowed");
+ goto done;
+ }
+
+ rc = modify_ticket_state(options.ticket_id, cib_conn, data_set);
+ exit_code = pcmk_rc2exitc(rc);
+
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Could not modify ticket: %s", pcmk_rc_str(rc));
+ }
+
+ } else if (options.ticket_cmd == 'S') {
+ /* Correct usage was handled in the "if (modified)" block above, so
+ * this is just for reporting usage errors
+ */
+
+ if (pcmk__str_empty(options.attr_name)) {
+ // We only get here if ticket_cmd was left as default
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply a command");
+ goto done;
+ }
+
+ if (options.ticket_id == NULL) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Must supply ticket ID with -t");
+ goto done;
+ }
+
+ if (pcmk__str_empty(options.attr_value)) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Must supply value with -v for -S %s", options.attr_name);
+ goto done;
+ }
+
+ } else {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Unknown command: %c", options.ticket_cmd);
+ }
+
+ done:
+ if (attr_set) {
+ g_hash_table_destroy(attr_set);
+ }
+ attr_set = NULL;
+
+ if (attr_delete) {
+ g_list_free_full(attr_delete, free);
+ }
+ attr_delete = NULL;
+
+ pe_free_working_set(data_set);
+ data_set = NULL;
+
+ cib__clean_up_connection(&cib_conn);
+
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+ g_free(options.attr_default);
+ g_free(options.attr_id);
+ free(options.attr_name);
+ free(options.attr_value);
+ free(options.get_attr_name);
+ g_free(options.set_name);
+ g_free(options.ticket_id);
+ g_free(options.xml_file);
+
+ pcmk__output_and_clear_error(&error, NULL);
+
+ crm_exit(exit_code);
+}
diff --git a/tools/crm_verify.8.inc b/tools/crm_verify.8.inc
new file mode 100644
index 0000000..27b0a55
--- /dev/null
+++ b/tools/crm_verify.8.inc
@@ -0,0 +1,5 @@
+[=synopsis]
+crm_verify [<options>]
+
+/reported as warnings./
+.SH OPTIONS
diff --git a/tools/crm_verify.c b/tools/crm_verify.c
new file mode 100644
index 0000000..43b09da
--- /dev/null
+++ b/tools/crm_verify.c
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2004-2023 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 <crm/crm.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
+
+#include <stdio.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/util.h>
+#include <crm/msg_xml.h>
+#include <crm/cib.h>
+#include <crm/cib/internal.h>
+#include <crm/pengine/status.h>
+#include <pacemaker-internal.h>
+
+const char *SUMMARY = "Check a Pacemaker configuration for errors\n\n"
+ "Check the well-formedness of a complete Pacemaker XML configuration,\n"
+ "its conformance to the configured schema, and the presence of common\n"
+ "misconfigurations. Problems reported as errors must be fixed before the\n"
+ "cluster will work properly. It is left to the administrator to decide\n"
+ "whether to fix problems reported as warnings.";
+
+struct {
+ char *cib_save;
+ gboolean use_live_cib;
+ char *xml_file;
+ gboolean xml_stdin;
+ char *xml_string;
+} options;
+
+static GOptionEntry data_entries[] = {
+ { "live-check", 'L', 0, G_OPTION_ARG_NONE,
+ &options.use_live_cib, "Check the configuration used by the running cluster",
+ NULL },
+ { "xml-file", 'x', 0, G_OPTION_ARG_FILENAME,
+ &options.xml_file, "Check the configuration in the named file",
+ "FILE" },
+ { "xml-pipe", 'p', 0, G_OPTION_ARG_NONE,
+ &options.xml_stdin, "Check the configuration piped in via stdin",
+ NULL },
+ { "xml-text", 'X', 0, G_OPTION_ARG_STRING,
+ &options.xml_string, "Check the configuration in the supplied string",
+ "XML" },
+
+ { NULL }
+};
+
+static GOptionEntry addl_entries[] = {
+ { "save-xml", 'S', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME,
+ &options.cib_save, "Save verified XML to named file (most useful with -L)",
+ "FILE" },
+
+ { NULL }
+};
+
+static pcmk__supported_format_t formats[] = {
+ PCMK__SUPPORTED_FORMAT_NONE,
+ PCMK__SUPPORTED_FORMAT_TEXT,
+ PCMK__SUPPORTED_FORMAT_XML,
+ { NULL, NULL, NULL }
+};
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
+ GOptionContext *context = NULL;
+
+ const char *description = "Examples:\n\n"
+ "Check the consistency of the configuration in the running cluster:\n\n"
+ "\tcrm_verify --live-check\n\n"
+ "Check the consistency of the configuration in a given file and "
+ "produce verbose output:\n\n"
+ "\tcrm_verify --xml-file file.xml --verbose\n\n";
+
+ context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
+ g_option_context_set_description(context, description);
+
+ pcmk__add_arg_group(context, "data", "Data sources:",
+ "Show data options", data_entries);
+ pcmk__add_arg_group(context, "additional", "Additional options:",
+ "Show additional options", addl_entries);
+
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ xmlNode *cib_object = NULL;
+ xmlNode *status = NULL;
+
+ pe_working_set_t *data_set = NULL;
+ const char *xml_tag = NULL;
+
+ int rc = pcmk_rc_ok;
+ crm_exit_t exit_code = CRM_EX_OK;
+
+ GError *error = NULL;
+
+ pcmk__output_t *out = NULL;
+
+ GOptionGroup *output_group = NULL;
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, "xSX");
+ GOptionContext *context = build_arg_context(args, &output_group);
+
+ pcmk__register_formats(output_group, formats);
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("crm_verify", args->verbosity);
+
+ rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
+ if (rc != pcmk_rc_ok) {
+ exit_code = CRM_EX_ERROR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s",
+ args->output_ty, pcmk_rc_str(rc));
+ goto done;
+ }
+
+ if (args->version) {
+ out->version(out, false);
+ goto done;
+ }
+
+ pcmk__register_lib_messages(out);
+
+ crm_info("=#=#=#=#= Getting XML =#=#=#=#=");
+
+ if (options.use_live_cib) {
+ crm_info("Reading XML from: live cluster");
+ rc = cib__signon_query(out, NULL, &cib_object);
+
+ if (rc != pcmk_rc_ok) {
+ // cib__signon_query() outputs any relevant error
+ goto done;
+ }
+
+ } else if (options.xml_file != NULL) {
+ cib_object = filename2xml(options.xml_file);
+ if (cib_object == NULL) {
+ rc = ENODATA;
+ g_set_error(&error, PCMK__RC_ERROR, rc, "Couldn't parse input file: %s", options.xml_file);
+ goto done;
+ }
+
+ } else if (options.xml_string != NULL) {
+ cib_object = string2xml(options.xml_string);
+ if (cib_object == NULL) {
+ rc = ENODATA;
+ g_set_error(&error, PCMK__RC_ERROR, rc, "Couldn't parse input string: %s", options.xml_string);
+ goto done;
+ }
+ } else if (options.xml_stdin) {
+ cib_object = stdin2xml();
+ if (cib_object == NULL) {
+ rc = ENODATA;
+ g_set_error(&error, PCMK__RC_ERROR, rc, "Couldn't parse input from STDIN.");
+ goto done;
+ }
+
+ } else {
+ rc = ENODATA;
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ "No configuration source specified. Use --help for usage information.");
+ goto done;
+ }
+
+ xml_tag = crm_element_name(cib_object);
+ if (!pcmk__str_eq(xml_tag, XML_TAG_CIB, pcmk__str_casei)) {
+ rc = EBADMSG;
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ "This tool can only check complete configurations (i.e. those starting with <cib>).");
+ goto done;
+ }
+
+ if (options.cib_save != NULL) {
+ write_xml_file(cib_object, options.cib_save, FALSE);
+ }
+
+ status = pcmk_find_cib_element(cib_object, XML_CIB_TAG_STATUS);
+ if (status == NULL) {
+ create_xml_node(cib_object, XML_CIB_TAG_STATUS);
+ }
+
+ if (validate_xml(cib_object, NULL, FALSE) == FALSE) {
+ pcmk__config_err("CIB did not pass schema validation");
+ free_xml(cib_object);
+ cib_object = NULL;
+
+ } else if (cli_config_update(&cib_object, NULL, FALSE) == FALSE) {
+ crm_config_error = TRUE;
+ free_xml(cib_object);
+ cib_object = NULL;
+ out->err(out, "The cluster will NOT be able to use this configuration.\n"
+ "Please manually update the configuration to conform to the %s syntax.",
+ xml_latest_schema());
+ }
+
+ data_set = pe_new_working_set();
+ if (data_set == NULL) {
+ rc = errno;
+ crm_perror(LOG_CRIT, "Unable to allocate working set");
+ goto done;
+ }
+ data_set->priv = out;
+
+ /* Process the configuration to set crm_config_error/crm_config_warning.
+ *
+ * @TODO Some parts of the configuration are unpacked only when needed (for
+ * example, action configuration), so we aren't necessarily checking those.
+ */
+ if (cib_object != NULL) {
+ unsigned long long flags = pe_flag_no_counts|pe_flag_no_compat;
+
+ if ((status == NULL) && !options.use_live_cib) {
+ // No status available, so do minimal checks
+ flags |= pe_flag_check_config;
+ }
+ pcmk__schedule_actions(cib_object, flags, data_set);
+ }
+ pe_free_working_set(data_set);
+
+ if (crm_config_error) {
+ rc = pcmk_rc_schema_validation;
+
+ if (args->verbosity > 0) {
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ "Errors found during check: config not valid");
+ } else {
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ "Errors found during check: config not valid\n-V may provide more details");
+ }
+
+ } else if (crm_config_warning) {
+ rc = pcmk_rc_schema_validation;
+
+ if (args->verbosity > 0) {
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ "Warnings found during check: config may not be valid");
+ } else {
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ "Warnings found during check: config may not be valid\n-V may provide more details");
+ }
+ }
+
+ done:
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+ free(options.cib_save);
+ free(options.xml_file);
+ free(options.xml_string);
+
+ if (exit_code == CRM_EX_OK) {
+ exit_code = pcmk_rc2exitc(rc);
+ }
+
+ pcmk__output_and_clear_error(&error, NULL);
+
+ if (out != NULL) {
+ out->finish(out, exit_code, true, NULL);
+ pcmk__output_free(out);
+ }
+
+ pcmk__unregister_formats();
+ crm_exit(exit_code);
+}
diff --git a/tools/crmadmin.8.inc b/tools/crmadmin.8.inc
new file mode 100644
index 0000000..ab5efbe
--- /dev/null
+++ b/tools/crmadmin.8.inc
@@ -0,0 +1,5 @@
+[synopsis]
+crmadmin [options] [node]
+
+/the Pacemaker controller/
+.SH OPTIONS
diff --git a/tools/crmadmin.c b/tools/crmadmin.c
new file mode 100644
index 0000000..0b400ae
--- /dev/null
+++ b/tools/crmadmin.c
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2004-2023 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 <stdio.h>
+#include <stdbool.h>
+#include <stdlib.h> // atoi()
+
+#include <glib.h> // gboolean, GMainLoop, etc.
+#include <libxml/tree.h> // xmlNode
+
+#include <pacemaker-internal.h>
+
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
+
+#define SUMMARY "query and manage the Pacemaker controller"
+
+static enum {
+ cmd_none,
+ cmd_health,
+ cmd_whois_dc,
+ cmd_list_nodes,
+ cmd_pacemakerd_health,
+} command = cmd_none;
+
+struct {
+ gboolean health;
+ gint timeout;
+ char *optarg;
+ char *ipc_name;
+ gboolean bash_export;
+} options = {
+ .optarg = NULL,
+ .ipc_name = NULL,
+ .bash_export = FALSE
+};
+
+gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+
+static GOptionEntry command_options[] = {
+ { "status", 'S', 0, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the status of the specified node."
+ "\n Result is state of node's internal finite state"
+ "\n machine, which can be useful for debugging",
+ "NODE"
+ },
+ { "pacemakerd", 'P', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the status of local pacemakerd."
+ "\n Result is the state of the sub-daemons watched"
+ "\n by pacemakerd.",
+ NULL
+ },
+ { "dc_lookup", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the uname of the node co-ordinating the cluster."
+ "\n This is an internal detail rarely useful to"
+ "\n administrators except when deciding on which"
+ "\n node to examine the logs.",
+ NULL
+ },
+ { "nodes", 'N', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, command_cb,
+ "Display the uname of all member nodes [optionally filtered by type (comma-separated)]"
+ "\n Types: all (default), cluster, guest, remote",
+ "TYPE"
+ },
+ { "health", 'H', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.health,
+ NULL,
+ NULL
+ },
+
+ { NULL }
+};
+
+static GOptionEntry additional_options[] = {
+ { "timeout", 't', 0, G_OPTION_ARG_CALLBACK, command_cb,
+ "Time to wait before declaring the operation"
+ "\n failed",
+ "TIMESPEC"
+ },
+ { "bash-export", 'B', 0, G_OPTION_ARG_NONE, &options.bash_export,
+ "Display nodes as shell commands of the form 'export uname=uuid'"
+ "\n (valid with -N/--nodes)",
+ },
+ { "ipc-name", 'i', 0, G_OPTION_ARG_STRING, &options.ipc_name,
+ "Name to use for ipc instead of 'crmadmin' (with -P/--pacemakerd).",
+ "NAME"
+ },
+
+ { NULL }
+};
+
+gboolean
+command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error)
+{
+ if (!strcmp(option_name, "--status") || !strcmp(option_name, "-S")) {
+ command = cmd_health;
+ crm_trace("Option %c => %s", 'S', optarg);
+ }
+
+ if (!strcmp(option_name, "--pacemakerd") || !strcmp(option_name, "-P")) {
+ command = cmd_pacemakerd_health;
+ }
+
+ if (!strcmp(option_name, "--dc_lookup") || !strcmp(option_name, "-D")) {
+ command = cmd_whois_dc;
+ }
+
+ if (!strcmp(option_name, "--nodes") || !strcmp(option_name, "-N")) {
+ command = cmd_list_nodes;
+ }
+
+ if (!strcmp(option_name, "--timeout") || !strcmp(option_name, "-t")) {
+ options.timeout = crm_parse_interval_spec(optarg);
+ if (errno == EINVAL) {
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ pcmk__str_update(&options.optarg, optarg);
+ return TRUE;
+}
+
+static pcmk__supported_format_t formats[] = {
+ PCMK__SUPPORTED_FORMAT_NONE,
+ PCMK__SUPPORTED_FORMAT_TEXT,
+ PCMK__SUPPORTED_FORMAT_XML,
+ { NULL, NULL, NULL }
+};
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
+ GOptionContext *context = NULL;
+
+ const char *description = "Notes:\n\n"
+ "Time Specification:\n\n"
+ "The TIMESPEC in any command line option can be specified in many different\n"
+ "formats. It can be just an integer number of seconds, a number plus units\n"
+ "(ms/msec/us/usec/s/sec/m/min/h/hr), or an ISO 8601 period specification.\n\n"
+ "Report bugs to " PCMK__BUG_URL;
+
+ GOptionEntry extra_prog_entries[] = {
+ { "quiet", 'q', 0, G_OPTION_ARG_NONE, &(args->quiet),
+ "Display only the essential query information",
+ NULL },
+
+ { NULL }
+ };
+
+ context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
+ g_option_context_set_description(context, description);
+
+ /* Add the -q option, which cannot be part of the globally supported options
+ * because some tools use that flag for something else.
+ */
+ pcmk__add_main_args(context, extra_prog_entries);
+
+ pcmk__add_arg_group(context, "command", "Commands:",
+ "Show command options", command_options);
+ pcmk__add_arg_group(context, "additional", "Additional Options:",
+ "Show additional options", additional_options);
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ crm_exit_t exit_code = CRM_EX_OK;
+ int rc;
+ int argerr = 0;
+
+ GError *error = NULL;
+
+ pcmk__output_t *out = NULL;
+
+ GOptionGroup *output_group = NULL;
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, "itKNS");
+ GOptionContext *context = build_arg_context(args, &output_group);
+
+ pcmk__register_formats(output_group, formats);
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("crmadmin", args->verbosity);
+
+ rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
+ if (rc != pcmk_rc_ok) {
+ exit_code = CRM_EX_ERROR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s",
+ args->output_ty, pcmk_rc_str(rc));
+ goto done;
+ }
+
+ pcmk__register_lib_messages(out);
+
+ out->quiet = args->quiet;
+
+ if (!pcmk__force_args(context, &error, "%s --xml-simple-list --xml-substitute", g_get_prgname())) {
+ goto done;
+ }
+
+ if (args->version) {
+ out->version(out, false);
+ goto done;
+ }
+
+ if (options.health) {
+ out->err(out, "Cluster-wide health option not supported");
+ ++argerr;
+ }
+
+ if (command == cmd_none) {
+ out->err(out, "error: Must specify a command option");
+ ++argerr;
+ }
+
+ if (argerr) {
+ char *help = g_option_context_get_help(context, TRUE, NULL);
+
+ out->err(out, "%s", help);
+ g_free(help);
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ switch (command) {
+ case cmd_health:
+ rc = pcmk__controller_status(out, options.optarg,
+ (unsigned int) options.timeout);
+ break;
+ case cmd_pacemakerd_health:
+ rc = pcmk__pacemakerd_status(out, options.ipc_name,
+ (unsigned int) options.timeout, true,
+ NULL);
+ break;
+ case cmd_list_nodes:
+ rc = pcmk__list_nodes(out, options.optarg, options.bash_export);
+ break;
+ case cmd_whois_dc:
+ rc = pcmk__designated_controller(out,
+ (unsigned int) options.timeout);
+ break;
+ case cmd_none:
+ rc = pcmk_rc_error;
+ break;
+ }
+
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "error: Command failed: %s", pcmk_rc_str(rc));
+ exit_code = pcmk_rc2exitc(rc);
+ }
+
+done:
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+
+ pcmk__output_and_clear_error(&error, out);
+
+ if (out != NULL) {
+ out->finish(out, exit_code, true, NULL);
+ pcmk__output_free(out);
+ }
+ pcmk__unregister_formats();
+ return crm_exit(exit_code);
+}
diff --git a/tools/fix-manpages b/tools/fix-manpages
new file mode 100644
index 0000000..f1f6f0d
--- /dev/null
+++ b/tools/fix-manpages
@@ -0,0 +1,33 @@
+# Because tools/*.8.inc include a synopsis, the following line removes
+# a redundant Usage: header from the man page and the couple lines after
+# it.
+/.SS "Usage:"/,+3d
+
+# The tools/*.8.inc files also include some additional section headers
+# on a per-tool basis. These section headers will get printed out as
+# .SH lines, but then the header from the --help-all output will also
+# get turned into groff. For instance, the following will be in the
+# man page for NOTES:
+#
+# .SH NOTES
+# .PP
+# Notes:
+# .PP
+#
+# The following block looks for any of those additional headers. The
+# 'n' command puts the next line in the pattern space, the two 'N'
+# commands append the next two lines, and then the 'd' command deletes
+# them. So basically, this just deletes
+#
+# .PP
+# Notes:
+# .PP
+#
+# This leaves the --help-all output looking good and removes redundant
+# stuff from the man page. Feel free to add additional headers here.
+# Not all tools will have all headers.
+/.SH NOTES\|.SH INTERACTIVE USE\|.SH OPERATION SPECIFICATION\|.SH OUTPUT CONTROL\|.SH TIME SPECIFICATION/{ n
+N
+N
+d
+}
diff --git a/tools/iso8601.8.inc b/tools/iso8601.8.inc
new file mode 100644
index 0000000..faa7f76
--- /dev/null
+++ b/tools/iso8601.8.inc
@@ -0,0 +1,5 @@
+[synopsis]
+iso8601 <command> [options]
+
+/dates and times/
+.SH OPTIONS
diff --git a/tools/iso8601.c b/tools/iso8601.c
new file mode 100644
index 0000000..e53bca0
--- /dev/null
+++ b/tools/iso8601.c
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2005-2023 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 <crm/crm.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/iso8601.h>
+#include <crm/common/util.h> /* CRM_ASSERT */
+#include <unistd.h>
+
+#define SUMMARY "Display and parse ISO 8601 dates and times"
+
+struct {
+ char *date_time_s;
+ gchar *duration_s;
+ gchar *expected_s;
+ gchar *period_s;
+ int print_options;
+} options;
+
+#define INDENT " "
+
+static gboolean
+date_now_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (pcmk__str_any_of(option_name, "--now", "-n", NULL)) {
+ pcmk__str_update(&options.date_time_s, "now");
+ } else if (pcmk__str_any_of(option_name, "--date", "-d", NULL)) {
+ pcmk__str_update(&options.date_time_s, optarg);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+modifier_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ if (pcmk__str_any_of(option_name, "--seconds", "-s", NULL)) {
+ options.print_options |= crm_time_seconds;
+ } else if (pcmk__str_any_of(option_name, "--epoch", "-S", NULL)) {
+ options.print_options |= crm_time_epoch;
+ } else if (pcmk__str_any_of(option_name, "--local", "-L", NULL)) {
+ options.print_options |= crm_time_log_with_timezone;
+ } else if (pcmk__str_any_of(option_name, "--ordinal", "-O", NULL)) {
+ options.print_options |= crm_time_ordinal;
+ } else if (pcmk__str_any_of(option_name, "--week", "-W", NULL)) {
+ options.print_options |= crm_time_weeks;
+ }
+
+ return TRUE;
+}
+
+static GOptionEntry command_entries[] = {
+ { "now", 'n', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, date_now_cb,
+ "Display the current date/time",
+ NULL },
+
+ { "date", 'd', 0, G_OPTION_ARG_CALLBACK, date_now_cb,
+ "Parse an ISO 8601 date/time (for example,\n"
+ INDENT "'2019-09-24 00:30:00 +01:00' or '2019-040')",
+ "DATE" },
+
+ { "period", 'p', 0, G_OPTION_ARG_STRING, &options.period_s,
+ "Parse an ISO 8601 period (interval) with start time (for example,\n"
+ INDENT "'2005-040/2005-043')",
+ "PERIOD" },
+
+ { "duration", 'D', 0, G_OPTION_ARG_STRING, &options.duration_s,
+ "Parse an ISO 8601 duration (for example, 'P1M')",
+ "DURATION" },
+
+ { "expected", 'E', 0, G_OPTION_ARG_STRING, &options.expected_s,
+ "Exit with error status if result does not match this text.\n"
+ INDENT "Requires: -n or -d",
+ "TEXT" },
+
+ { NULL }
+};
+
+static GOptionEntry modifier_entries[] = {
+ { "seconds", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, modifier_cb,
+ "Show result as a seconds since 0000-001 00:00:00Z",
+ NULL },
+
+ { "epoch", 'S', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, modifier_cb,
+ "Show result as a seconds since EPOCH (1970-001 00:00:00Z)",
+ NULL },
+
+ { "local", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, modifier_cb,
+ "Show result as a 'local' date/time",
+ NULL },
+
+ { "ordinal", 'O', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, modifier_cb,
+ "Show result as an 'ordinal' date/time",
+ NULL },
+
+ { "week", 'W', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, modifier_cb,
+ "Show result as an 'calendar week' date/time",
+ NULL },
+
+ { NULL }
+};
+
+static void
+log_time_period(int log_level, crm_time_period_t * dtp, int flags)
+{
+ char *start = crm_time_as_string(dtp->start, flags);
+ char *end = crm_time_as_string(dtp->end, flags);
+
+ CRM_ASSERT(start != NULL && end != NULL);
+ do_crm_log(log_level, "Period: %s to %s", start, end);
+ free(start);
+ free(end);
+}
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args) {
+ GOptionContext *context = NULL;
+
+ const char *description = "For more information on the ISO 8601 standard, see " \
+ "https://en.wikipedia.org/wiki/ISO_8601";
+
+ context = pcmk__build_arg_context(args, NULL, NULL, NULL);
+ g_option_context_set_description(context, description);
+
+ pcmk__add_arg_group(context, "commands", "Commands:",
+ "Show command options", command_entries);
+ pcmk__add_arg_group(context, "modifiers", "Output modifiers:",
+ "Show output modifiers", modifier_entries);
+
+ return context;
+}
+
+int
+main(int argc, char **argv)
+{
+ crm_exit_t exit_code = CRM_EX_OK;
+ crm_time_t *duration = NULL;
+ crm_time_t *date_time = NULL;
+
+ GError *error = NULL;
+
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ GOptionContext *context = build_arg_context(args);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, "dpDE");
+
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("iso8601", args->verbosity);
+
+ if (args->version) {
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+ /* FIXME: When iso8601 is converted to use formatted output, this can go. */
+ pcmk__cli_help('v');
+ }
+
+ if (pcmk__str_eq("now", options.date_time_s, pcmk__str_casei)) {
+ date_time = crm_time_new(NULL);
+
+ if (date_time == NULL) {
+ exit_code = CRM_EX_SOFTWARE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Internal error: couldn't determine 'now'!");
+ goto done;
+ }
+
+ crm_time_log(LOG_TRACE, "Current date/time", date_time,
+ crm_time_ordinal | crm_time_log_date | crm_time_log_timeofday);
+ crm_time_log(LOG_STDOUT, "Current date/time", date_time,
+ options.print_options | crm_time_log_date | crm_time_log_timeofday);
+
+ } else if (options.date_time_s) {
+ date_time = crm_time_new(options.date_time_s);
+
+ if (date_time == NULL) {
+ exit_code = CRM_EX_INVALID_PARAM;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Invalid date/time specified: %s", options.date_time_s);
+ goto done;
+ }
+
+ crm_time_log(LOG_TRACE, "Date", date_time,
+ crm_time_ordinal | crm_time_log_date | crm_time_log_timeofday);
+ crm_time_log(LOG_STDOUT, "Date", date_time,
+ options.print_options | crm_time_log_date | crm_time_log_timeofday);
+ }
+
+ if (options.duration_s) {
+ duration = crm_time_parse_duration(options.duration_s);
+
+ if (duration == NULL) {
+ exit_code = CRM_EX_INVALID_PARAM;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Invalid duration specified: %s", options.duration_s);
+ goto done;
+ }
+
+ crm_time_log(LOG_TRACE, "Duration", duration, crm_time_log_duration);
+ crm_time_log(LOG_STDOUT, "Duration", duration,
+ options.print_options | crm_time_log_duration);
+ }
+
+ if (options.period_s) {
+ crm_time_period_t *period = crm_time_parse_period(options.period_s);
+
+ if (period == NULL) {
+ exit_code = CRM_EX_INVALID_PARAM;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Invalid interval specified: %s", options.period_s);
+ goto done;
+ }
+
+ log_time_period(LOG_TRACE, period,
+ options.print_options | crm_time_log_date | crm_time_log_timeofday);
+ log_time_period(LOG_STDOUT, period,
+ options.print_options | crm_time_log_date | crm_time_log_timeofday);
+ crm_time_free_period(period);
+ }
+
+ if (date_time && duration) {
+ crm_time_t *later = crm_time_add(date_time, duration);
+
+ if (later == NULL) {
+ exit_code = CRM_EX_SOFTWARE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Unable to calculate ending time of %s plus %s",
+ options.date_time_s, options.duration_s);
+ goto done;
+ }
+
+ crm_time_log(LOG_TRACE, "Duration ends at", later,
+ crm_time_ordinal | crm_time_log_date | crm_time_log_timeofday);
+ crm_time_log(LOG_STDOUT, "Duration ends at", later,
+ options.print_options | crm_time_log_date | crm_time_log_timeofday |
+ crm_time_log_with_timezone);
+
+ if (options.expected_s) {
+ char *dt_s = crm_time_as_string(later,
+ options.print_options | crm_time_log_date |
+ crm_time_log_timeofday);
+ if (!pcmk__str_eq(options.expected_s, dt_s, pcmk__str_casei)) {
+ exit_code = CRM_EX_ERROR;
+ goto done;
+ }
+ free(dt_s);
+ }
+ crm_time_free(later);
+
+ } else if (date_time && options.expected_s) {
+ char *dt_s = crm_time_as_string(date_time,
+ options.print_options | crm_time_log_date | crm_time_log_timeofday);
+
+ if (!pcmk__str_eq(options.expected_s, dt_s, pcmk__str_casei)) {
+ exit_code = CRM_EX_ERROR;
+ goto done;
+ }
+ free(dt_s);
+ }
+
+done:
+ crm_time_free(date_time);
+ crm_time_free(duration);
+
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+
+ free(options.date_time_s);
+ g_free(options.duration_s);
+ g_free(options.expected_s);
+ g_free(options.period_s);
+
+ pcmk__output_and_clear_error(&error, NULL);
+ crm_exit(exit_code);
+}
diff --git a/tools/pcmk_simtimes.in b/tools/pcmk_simtimes.in
new file mode 100644
index 0000000..c8b0af6
--- /dev/null
+++ b/tools/pcmk_simtimes.in
@@ -0,0 +1,159 @@
+#!@PYTHON@
+""" Timing comparisons for crm_simulate profiling output
+"""
+
+__copyright__ = "Copyright 2019-2023 the Pacemaker project contributors"
+__license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY"
+
+import io
+import re
+import sys
+import errno
+import argparse
+import os
+
+# These imports allow running from a source checkout after running `make`.
+# Note that while this doesn't necessarily mean it will successfully run tests,
+# but being able to see --help output can be useful.
+if os.path.exists("@abs_top_srcdir@/python"):
+ sys.path.insert(0, "@abs_top_srcdir@/python")
+
+if os.path.exists("@abs_top_builddir@/python") and "@abs_top_builddir@" != "@abs_top_srcdir@":
+ sys.path.insert(0, "@abs_top_builddir@/python")
+
+from pacemaker.exitstatus import ExitStatus
+
+DESC = """Compare timings from crm_simulate profiling output"""
+
+BEFORE_HELP = """Output of "crm_simulate --profile cts/scheduler --repeat <N>" from earlier Pacemaker build"""
+
+# line like: * Testing cts/scheduler/xml/1360.xml ... 0.07 secs
+PATTERN = r"""^\s*\*\s+Testing\s+.*/([^/]+)\.xml\s+\.+\s+([.0-9]+)\s+secs\s*$"""
+
+def parse_args(argv=sys.argv):
+ """ Parse command-line arguments """
+
+ parser = argparse.ArgumentParser(description=DESC)
+
+ parser.add_argument('-V', '--verbose', action='count',
+ help='Increase verbosity')
+
+ parser.add_argument('-p', '--threshold-percent', type=float, default=0,
+ help="Don't show tests with less than this percentage difference in times")
+
+ parser.add_argument('-s', '--threshold-seconds', type=float, default=0,
+ help="Don't show tests with less than this seconds difference in times")
+
+ parser.add_argument('-S', '--sort', choices=['test', 'before', 'after', 'diff', 'percent'],
+ default='test', help="Sort results by this column")
+
+ parser.add_argument('-r', '--reverse', action='store_true',
+ help="Sort results in descending order")
+
+ parser.add_argument('before_file', metavar='BEFORE',
+ type=argparse.FileType('r'),
+ help=BEFORE_HELP)
+
+ parser.add_argument('after_file', metavar='AFTER',
+ type=argparse.FileType('r'),
+ help='Output of same command from later Pacemaker build')
+
+ return parser.parse_args(argv[1:])
+
+
+def extract_times(infile):
+ """ Extract test names and times into hash table from file """
+
+ result = {}
+ for line in infile:
+ match = re.search(PATTERN, line)
+ if match is not None:
+ result[match.group(1)] = match.group(2)
+ return result
+
+
+def compare_test(test, before, after, args):
+ """ Compare one test's timings """
+
+ try:
+ before_time = float(before[test])
+ except KeyError:
+ if args.verbose > 0:
+ print("No previous test " + test + " to compare")
+ return None
+
+ after_time = float(after[test])
+
+ time_diff = after_time - before_time
+ time_diff_percent = (time_diff / before_time) * 100
+
+ if ((abs(time_diff) >= args.threshold_seconds)
+ and (abs(time_diff_percent) >= args.threshold_percent)):
+ return { 'test': test,
+ 'before': before_time,
+ 'after': after_time,
+ 'diff': time_diff,
+ 'percent': time_diff_percent
+ }
+ return None
+
+def sort_diff(result):
+ """ Sort two test results by time difference """
+
+ global sort_field
+
+ return result[sort_field]
+
+
+def print_results(results, sort_reverse):
+ """ Output the comparison results """
+
+ if results == []:
+ return
+
+ # Sort and print test differences
+ results.sort(reverse=sort_reverse, key=sort_diff)
+ for result in results:
+ print("%-40s %6.2fs vs %6.2fs (%+.2fs = %+6.2f%%)" % (result['test'],
+ result['before'], result['after'], result['diff'],
+ result['percent']))
+
+ # Print average differences
+ diff_total = sum(d['diff'] for d in results)
+ percent_total = sum(d['percent'] for d in results)
+ nresults = len(results)
+ print("\nAverages: %+.2fs %+6.2f%%" % ((diff_total / nresults),
+ (percent_total / nresults)))
+
+
+if __name__ == "__main__":
+
+ global sort_field
+
+ try:
+ args = parse_args()
+ if args.verbose is None:
+ args.verbose = 0
+
+ before = extract_times(args.before_file)
+ after = extract_times(args.after_file)
+ sort_field = args.sort
+
+ # Build a list of test differences
+ results = []
+ for test in after.keys():
+ result = compare_test(test, before, after, args)
+ if result is not None:
+ results = results + [ result ]
+
+ print_results(results, sort_reverse=args.reverse)
+
+ except KeyboardInterrupt:
+ pass
+ except IOError as e:
+ if e.errno != errno.EPIPE:
+ raise
+
+ sys.exit(ExitStatus.OK)
+
+# vim: set filetype=python expandtab tabstop=4 softtabstop=4 shiftwidth=4 textwidth=120:
diff --git a/tools/report.collector.in b/tools/report.collector.in
new file mode 100644
index 0000000..315b785
--- /dev/null
+++ b/tools/report.collector.in
@@ -0,0 +1,885 @@
+#
+# Originally based on hb_report
+# Copyright 2007 Dejan Muhamedagic <dmuhamedagic@suse.de>
+# Later changes copyright 2010-2018 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.
+#
+
+if
+ echo $REPORT_HOME | grep -qs '^/'
+then
+ debug "Using full path to working directory: $REPORT_HOME"
+else
+ REPORT_HOME="$HOME/$REPORT_HOME"
+ debug "Canonicalizing working directory path: $REPORT_HOME"
+fi
+
+detect_host
+
+#
+# find files newer than a and older than b
+#
+isnumber() {
+ echo "$*" | grep -qs '^[0-9][0-9]*$'
+}
+
+touchfile() {
+ t=`mktemp` &&
+ perl -e "\$file=\"$t\"; \$tm=$1;" -e 'utime $tm, $tm, $file;' &&
+ echo $t
+}
+
+find_files_clean() {
+ [ -z "$from_stamp" ] || rm -f "$from_stamp"
+ [ -z "$to_stamp" ] || rm -f "$to_stamp"
+ from_stamp=""
+ to_stamp=""
+}
+
+find_files() {
+ dirs=
+ from_time=$2
+ to_time=$3
+ for d in $1; do
+ if [ -d $d ]; then
+ dirs="$dirs $d"
+ fi
+ done
+
+ if [ x"$dirs" = x ]; then
+ return
+ fi
+
+ isnumber "$from_time" && [ "$from_time" -gt 0 ] || {
+ warning "sorry, can't find files in [ $1 ] based on time if you don't supply time"
+ return
+ }
+ trap find_files_clean 0
+ if ! from_stamp=`touchfile $from_time`; then
+ warning "sorry, can't create temporary file for find_files"
+ return
+ fi
+ findexp="-newer $from_stamp"
+ if isnumber "$to_time" && [ "$to_time" -gt 0 ]; then
+ if ! to_stamp=`touchfile $to_time`; then
+ warning "sorry, can't create temporary file for find_files"
+ find_files_clean
+ return
+ fi
+ findexp="$findexp ! -newer $to_stamp"
+ fi
+ find $dirs -type f $findexp
+ find_files_clean
+ trap "" 0
+}
+
+#
+# check permissions of files/dirs
+#
+pl_checkperms() {
+ perl -e '
+ # check permissions and ownership
+ # uid and gid are numeric
+ # everything must match exactly
+ # no error checking! (file should exist, etc)
+ ($filename, $perms, $in_uid, $in_gid) = @ARGV;
+ ($mode,$uid,$gid) = (stat($filename))[2,4,5];
+ $p=sprintf("%04o", $mode & 07777);
+ $p ne $perms and exit(1);
+ $uid ne $in_uid and exit(1);
+ $gid ne $in_gid and exit(1);
+ ' $*
+}
+
+num_id() {
+ getent $1 $2 | awk -F: '{print $3}'
+}
+
+chk_id() {
+ [ "$2" ] && return 0
+ echo "$1: id not found"
+ return 1
+}
+
+check_perms() {
+ while read type f p uid gid; do
+ if [ ! -e "$f" ]; then
+ echo "$f doesn't exist"
+ continue
+ elif [ ! -$type "$f" ]; then
+ echo "$f has wrong type"
+ continue
+ fi
+ n_uid=`num_id passwd $uid`
+ chk_id "$uid" "$n_uid" || continue
+ n_gid=`num_id group $gid`
+ chk_id "$gid" "$n_gid" || continue
+ pl_checkperms $f $p $n_uid $n_gid || {
+ echo "wrong permissions or ownership for $f:"
+ ls -ld $f
+ }
+ done
+}
+
+#
+# coredumps
+#
+findbinary() {
+ random_binary=`which cat 2>/dev/null` # suppose we are lucky
+ binary=`gdb $random_binary $1 < /dev/null 2>/dev/null |
+ grep 'Core was generated' | awk '{print $5}' |
+ sed "s/^.//;s/[.':]*$//"`
+ if [ x = x"$binary" ]; then
+ debug "Could not detect the program name for core $1 from the gdb output; will try with file(1)"
+ binary=$(file $1 | awk '/from/{
+ for( i=1; i<=NF; i++ )
+ if( $i == "from" ) {
+ print $(i+1)
+ break
+ }
+ }')
+ binary=`echo $binary | tr -d "'"`
+ binary=$(echo $binary | tr -d '`')
+ if [ "$binary" ]; then
+ binary=`which $binary 2>/dev/null`
+ fi
+ fi
+ if [ x = x"$binary" ]; then
+ warning "Could not find the program path for core $1"
+ return
+ fi
+ fullpath=`which $binary 2>/dev/null`
+ if [ x = x"$fullpath" ]; then
+ if [ -x $CRM_DAEMON_DIR/$binary ]; then
+ echo $CRM_DAEMON_DIR/$binary
+ debug "Found the program at $CRM_DAEMON_DIR/$binary for core $1"
+ else
+ warning "Could not find the program path for core $1"
+ fi
+ else
+ echo $fullpath
+ debug "Found the program at $fullpath for core $1"
+ fi
+}
+
+getbt() {
+ which gdb > /dev/null 2>&1 || {
+ warning "Please install gdb to get backtraces"
+ return
+ }
+ for corefile; do
+ absbinpath=`findbinary $corefile`
+ [ x = x"$absbinpath" ] && continue
+ echo "====================== start backtrace ======================"
+ ls -l $corefile
+ # Summary first...
+ gdb -batch -n -quiet -ex ${BT_OPTS:-"thread apply all bt"} -ex quit \
+ $absbinpath $corefile 2>/dev/null
+ echo "====================== start detail ======================"
+ # Now the unreadable details...
+ gdb -batch -n -quiet -ex ${BT_OPTS:-"thread apply all bt full"} -ex quit \
+ $absbinpath $corefile 2>/dev/null
+ echo "======================= end backtrace ======================="
+ done
+}
+
+dump_status_and_config() {
+ crm_mon -1 2>&1 | grep -v '^Last upd' > $target/$CRM_MON_F
+ cibadmin -Ql 2>/dev/null > $target/${CIB_F}.live
+}
+
+getconfig() {
+ cluster=$1; shift;
+ target=$1; shift;
+
+ for cf in $*; do
+ if [ -e "$cf" ]; then
+ cp -a "$cf" $target/
+ fi
+ done
+
+ if is_running pacemaker-controld; then
+ dump_status_and_config
+ crm_node -p > "$target/$MEMBERSHIP_F" 2>&1
+ echo "$host" > $target/RUNNING
+
+ elif is_running pacemaker-remoted; then
+ dump_status_and_config
+ echo "$host" > $target/RUNNING
+
+ # Pre-2.0.0 daemon name in case we're collecting on a mixed-version cluster
+ elif is_running pacemaker_remoted; then
+ dump_status_and_config
+ echo "$host" > $target/RUNNING
+
+ else
+ echo "$host" > $target/STOPPED
+ fi
+}
+
+get_readable_cib() {
+ target="$1"; shift;
+
+ if [ -f "$target/$CIB_F" ]; then
+ crm_verify -V -x "$target/$CIB_F" >"$target/$CRM_VERIFY_F" 2>&1
+ if which crm >/dev/null 2>&1 ; then
+ CIB_file="$target/$CIB_F" crm configure show >"$target/$CIB_TXT_F" 2>&1
+ elif which pcs >/dev/null 2>&1 ; then
+ pcs config -f "$target/$CIB_F" >"$target/$CIB_TXT_F" 2>&1
+ fi
+ fi
+}
+
+#
+# remove values of sensitive attributes
+#
+# this is not proper xml parsing, but it will work under the
+# circumstances
+sanitize_xml_attrs() {
+ sed $(
+ for patt in $SANITIZE; do
+ echo "-e /name=\"$patt\"/s/value=\"[^\"]*\"/value=\"****\"/"
+ done
+ )
+}
+
+sanitize_hacf() {
+ awk '
+ $1=="stonith_host"{ for( i=5; i<=NF; i++ ) $i="****"; }
+ {print}
+ '
+}
+
+sanitize_one_clean() {
+ [ -z "$tmp" ] || rm -f "$tmp"
+ tmp=""
+ [ -z "$ref" ] || rm -f "$ref"
+ ref=""
+}
+
+sanitize() {
+ file=$1
+ compress=""
+ if [ -z "$SANITIZE" ]; then
+ return
+ fi
+ echo $file | grep -qs 'gz$' && compress=gzip
+ echo $file | grep -qs 'bz2$' && compress=bzip2
+ if [ "$compress" ]; then
+ decompress="$compress -dc"
+ else
+ compress=cat
+ decompress=cat
+ fi
+ trap sanitize_one_clean 0
+ tmp=`mktemp`
+ ref=`mktemp`
+ if [ -z "$tmp" -o -z "$ref" ]; then
+ sanitize_one_clean
+ fatal "cannot create temporary files"
+ fi
+ touch -r $file $ref # save the mtime
+ if [ "`basename $file`" = ha.cf ]; then
+ sanitize_hacf
+ else
+ $decompress | sanitize_xml_attrs | $compress
+ fi < $file > $tmp
+ mv $tmp $file
+ # note: cleaning $tmp up is still needed even after it's renamed
+ # because its temp directory is still there.
+
+ touch -r $ref $file
+ sanitize_one_clean
+ trap "" 0
+}
+
+#
+# get some system info
+#
+distro() {
+ if
+ which lsb_release >/dev/null 2>&1
+ then
+ lsb_release -d | sed -e 's/^Description:\s*//'
+ debug "Using lsb_release for distribution info"
+ return
+ fi
+
+ relf=`ls /etc/debian_version 2>/dev/null` ||
+ relf=`ls /etc/slackware-version 2>/dev/null` ||
+ relf=`ls -d /etc/*-release 2>/dev/null` && {
+ for f in $relf; do
+ test -f $f && {
+ echo "`ls $f` `cat $f`"
+ debug "Found `echo $relf | tr '\n' ' '` distribution release file(s)"
+ return
+ }
+ done
+ }
+ warning "No lsb_release, no /etc/*-release, no /etc/debian_version: no distro information"
+}
+
+pkg_ver() {
+ if which dpkg >/dev/null 2>&1 ; then
+ pkg_mgr="deb"
+ elif which rpm >/dev/null 2>&1 ; then
+ pkg_mgr="rpm"
+ elif which pkg_info >/dev/null 2>&1 ; then
+ pkg_mgr="pkg_info"
+ elif which pkginfo >/dev/null 2>&1 ; then
+ pkg_mgr="pkginfo"
+ else
+ warning "Unknown package manager"
+ return
+ fi
+ debug "The package manager is: $pkg_mgr"
+ echo "The package manager is: $pkg_mgr"
+
+ echo "Installed packages:"
+ case $pkg_mgr in
+ deb)
+ dpkg-query -f '${Package} ${Version} ${Architecture}\n' -W | sort
+ echo
+ for pkg in $*; do
+ if dpkg-query -W $pkg 2>/dev/null ; then
+ debug "Verifying installation of: $pkg"
+ echo "Verifying installation of: $pkg"
+ debsums -s $pkg 2>/dev/null
+ fi
+ done
+ ;;
+ rpm)
+ rpm -qa --qf '%{name} %{version}-%{release} - %{distribution} %{arch}\n' | sort
+ echo
+ for pkg in $*; do
+ if rpm -q $pkg >/dev/null 2>&1 ; then
+ debug "Verifying installation of: $pkg"
+ echo "Verifying installation of: $pkg"
+ rpm --verify $pkg 2>&1
+ fi
+ done
+ ;;
+ pkg_info)
+ pkg_info
+ ;;
+ pkginfo)
+ pkginfo | awk '{print $3}' # format?
+ ;;
+ esac
+}
+
+getbacktraces() {
+ debug "Looking for backtraces: $*"
+ flist=$(
+ for f in `find_files "$CRM_CORE_DIRS" $1 $2`; do
+ bf=`basename $f`
+ test `expr match $bf core` -gt 0 &&
+ echo $f
+ done)
+ if [ "$flist" ]; then
+ for core in $flist; do
+ log "Found core file: `ls -al $core`"
+ done
+
+ # Make a copy of them in case we need more data later
+ # Luckily they compress well
+ mkdir cores >/dev/null 2>&1
+ cp -a $flist cores/
+ shrink cores
+ rm -rf cores
+
+ # Now get as much as we can from them automagically
+ for f in $flist; do
+ getbt $f
+ done
+ fi
+}
+
+getpeinputs() {
+ if [ -n "$PE_STATE_DIR" ]; then
+ flist=$(
+ find_files "$PE_STATE_DIR" "$1" "$2" | sed "s,`dirname $PE_STATE_DIR`/,,g"
+ )
+ if [ "$flist" ]; then
+ (cd $(dirname "$PE_STATE_DIR") && tar cf - $flist) | (cd "$3" && tar xf -)
+ debug "found `echo $flist | wc -w` scheduler input files in $PE_STATE_DIR"
+ fi
+ fi
+}
+
+getblackboxes() {
+ flist=$(
+ find_files $BLACKBOX_DIR $1 $2
+ )
+
+ for bb in $flist; do
+ bb_short=`basename $bb`
+ qb-blackbox $bb > $3/${bb_short}.blackbox 2>&1
+ info "Extracting contents of blackbox: $bb_short"
+ done
+}
+
+#
+# some basic system info and stats
+#
+sys_info() {
+ cluster=$1; shift
+ echo "Platform: `uname`"
+ echo "Kernel release: `uname -r`"
+ echo "Architecture: `uname -m`"
+ if [ `uname` = Linux ]; then
+ echo "Distribution: `distro`"
+ fi
+
+ echo
+ cibadmin --version 2>&1 | head -1
+ cibadmin -! 2>&1
+ case $cluster in
+ corosync)
+ /usr/sbin/corosync -v 2>&1 | head -1
+ ;;
+ esac
+
+ # Cluster glue version hash (if available)
+ stonith -V 2>/dev/null
+
+ # Resource agents version hash
+ echo "resource-agents: `grep 'Build version:' /usr/lib/ocf/resource.d/heartbeat/.ocf-shellfuncs`"
+
+ echo
+ pkg_ver $*
+}
+
+sys_stats() {
+ set -x
+ uname -n
+ uptime
+ ps axf
+ ps auxw
+ top -b -n 1
+ ifconfig -a
+ ip addr list
+ netstat -i
+ arp -an
+ test -d /proc && {
+ cat /proc/cpuinfo
+ }
+ lsscsi
+ lspci
+ lsblk
+ mount
+ df
+ set +x
+}
+
+dlm_dump() {
+ if which dlm_tool >/dev/null 2>&1 ; then
+ if is_running dlm_controld; then
+ echo "--- Lockspace overview:"
+ dlm_tool ls -n
+
+ echo "---Lockspace history:"
+ dlm_tool dump
+
+ echo "---Lockspace status:"
+ dlm_tool status
+ dlm_tool status -v
+
+ echo "---Lockspace config:"
+ dlm_tool dump_config
+
+ dlm_tool log_plock
+
+ dlm_tool ls | grep name |
+ while read X N ; do
+ echo "--- Lockspace $N:"
+ dlm_tool lockdump "$N"
+ dlm_tool lockdebug -svw "$N"
+ done
+ fi
+ fi
+}
+
+drbd_info() {
+ test -f /proc/drbd && {
+ echo "--- /proc/drbd:"
+ cat /proc/drbd 2>&1
+ echo
+ }
+
+ if which drbdadm >/dev/null 2>&1; then
+ echo "--- drbdadm dump:"
+ if [ -z "$SANITIZE"]; then
+ drbdadm dump 2>&1
+ else
+ drbdadm dump 2>&1 | sed "s/\(shared-secret[ ]*\"\)[^\"]*\";/\1****\";/"
+ fi
+ echo
+
+ echo "--- drbdadm status:"
+ drbdadm status 2>&1
+ echo
+
+ echo "--- drbdadm show-gi:"
+ for res in $(drbdsetup status | grep -e ^\\S | awk '{ print $1 }'); do
+ echo "$res:"
+ drbdadm show-gi $res 2>&1
+ echo
+ done
+ fi
+
+ if which drbd-overview >/dev/null 2>&1; then
+ echo "--- drbd-overview:"
+ drbd-overview 2>&1
+ echo
+ fi
+
+ if which drbdsetup >/dev/null 2>&1; then
+ echo "--- drbdsetup status:"
+ drbdsetup status --verbose --statistics 2>&1
+ echo
+
+ echo "--- drbdsetup events2:"
+ drbdsetup events2 --timestamps --statistics --now 2>&1
+ echo
+ fi
+}
+
+iscfvarset() {
+ test "`getcfvar $1 $2`"
+}
+
+iscfvartrue() {
+ getcfvar $1 $2 $3 | grep -E -qsi "^(true|y|yes|on|1)"
+}
+
+iscfvarfalse() {
+ getcfvar $1 $2 $3 | grep -E -qsi "^(false|n|no|off|0)"
+}
+
+find_syslog() {
+ priority="$1"
+
+ # Always include system logs (if we can find them)
+ msg="Mark:pcmk:`perl -e 'print time()'`"
+ logger -p "$priority" "$msg" >/dev/null 2>&1
+
+ # Force buffer flush
+ killall -HUP rsyslogd >/dev/null 2>&1
+
+ sleep 2 # Give syslog time to catch up in case it's busy
+ findmsg 1 "$msg"
+}
+
+get_logfiles_cs() {
+ if [ ! -f "$cf_file" ]; then
+ return
+ fi
+
+ debug "Reading $cf_type log settings from $cf_file"
+
+ # The default value of to_syslog is yes.
+ if ! iscfvarfalse $cf_type to_syslog "$cf_file"; then
+ facility_cs=$(getcfvar $cf_type syslog_facility "$cf_file")
+ if [ -z "$facility_cs" ]; then
+ facility_cs="daemon"
+ fi
+
+ find_syslog "$facility_cs.info"
+ fi
+ if [ "$SOS_MODE" = "1" ]; then
+ return
+ fi
+
+ if iscfvartrue $cf_type to_logfile "$cf_file"; then
+ logfile=$(getcfvar $cf_type logfile "$cf_file")
+ if [ -f "$logfile" ]; then
+ debug "Log settings found for cluster type $cf_type: $logfile"
+ echo "$logfile"
+ fi
+ fi
+}
+
+get_logfiles() {
+ cf_type=$1
+ cf_file="$2"
+
+ case $cf_type in
+ corosync) get_logfiles_cs;;
+ esac
+
+ . @CONFIGDIR@/pacemaker
+
+ facility="$PCMK_logfacility"
+ if [ -z "$facility" ]; then
+ facility="daemon"
+ fi
+ if [ "$facility" != "$facility_cs" ]&&[ "$facility" != none ]; then
+ find_syslog "$facility.notice"
+ fi
+ if [ "$SOS_MODE" = "1" ]; then
+ return
+ fi
+
+ logfile="$PCMK_logfile"
+ if [ "$logfile" != none ]; then
+ if [ -z "$logfile" ]; then
+ for logfile in "@CRM_LOG_DIR@/pacemaker.log" "/var/log/pacemaker.log"; do
+ if [ -f "$logfile" ]; then
+ debug "Log settings not found for Pacemaker, assuming $logfile"
+ echo "$logfile"
+ break
+ fi
+ done
+
+ elif [ -f "$logfile" ]; then
+ debug "Log settings found for Pacemaker: $logfile"
+ echo "$logfile"
+ fi
+ fi
+
+ # Look for detail logs:
+
+ # - initial pacemakerd logs and tracing might go to a different file
+ pattern="Starting Pacemaker"
+
+ # - make sure we get something from the scheduler
+ pattern="$pattern\\|Calculated transition"
+
+ # - cib and pacemaker-execd updates
+ # (helpful on non-DC nodes and when cluster has been up for a long time)
+ pattern="$pattern\\|cib_perform_op\\|process_lrm_event"
+
+ # - pacemaker_remote might use a different file
+ pattern="$pattern\\|pacemaker[-_]remoted:"
+
+ findmsg 3 "$pattern"
+}
+
+essential_files() {
+ cat<<EOF
+d $PE_STATE_DIR 0750 hacluster haclient
+d $CRM_CONFIG_DIR 0750 hacluster haclient
+d $CRM_STATE_DIR 0750 hacluster haclient
+EOF
+}
+
+# Trim leading and ending whitespace (using only POSIX expressions)
+trim() {
+ TRIM_S="$1"
+
+ TRIM_S="${TRIM_S#"${TRIM_S%%[![:space:]]*}"}"
+ TRIM_S="${TRIM_S%"${TRIM_S##*[![:space:]]}"}"
+ echo -n "$TRIM_S"
+}
+
+collect_logs() {
+ CL_START="$1"
+ shift
+ CL_END="$1"
+ shift
+ CL_LOGFILES="$@"
+
+ which journalctl > /dev/null 2>&1
+ if [ $? -eq 0 ]; then
+ cl_have_journald=1
+ else
+ cl_have_journald=0
+ fi
+
+ cl_lognames="$CL_LOGFILES"
+ if [ $cl_have_journald -eq 1 ]; then
+ cl_lognames="$cl_lognames journalctl"
+ fi
+ cl_lognames=$(trim "$cl_lognames")
+ if [ -z "$cl_lognames" ]; then
+ return
+ fi
+
+ # YYYY-MM-DD HH:MM:SS
+ cl_start_ymd=$(date -d @${CL_START} +"%F %T")
+ cl_end_ymd=$(date -d @${CL_END} +"%F %T")
+
+ debug "Gathering logs from $cl_start_ymd to $cl_end_ymd:"
+ debug " $cl_lognames"
+
+ # Remove our temporary file if we get interrupted here
+ trap '[ -z "$cl_pattfile" ] || rm -f "$cl_pattfile"' 0
+
+ # Create a temporary file with patterns to grep for
+ cl_pattfile=$(mktemp) || fatal "cannot create temporary files"
+ for cl_pattern in $LOG_PATTERNS; do
+ echo "$cl_pattern"
+ done > $cl_pattfile
+
+ echo "Log pattern matches from $REPORT_TARGET:" > $ANALYSIS_F
+ if [ -n "$CL_LOGFILES" ]; then
+ for cl_logfile in $CL_LOGFILES; do
+ cl_extract="$(basename $cl_logfile).extract.txt"
+
+ if [ ! -f "$cl_logfile" ]; then
+ # Not a file
+ continue
+
+ elif [ -f "$cl_extract" ]; then
+ # We already have it
+ continue
+ fi
+
+ dumplogset "$cl_logfile" $LOG_START $LOG_END > "$cl_extract"
+ sanitize "$cl_extract"
+
+ grep -f "$cl_pattfile" "$cl_extract" >> $ANALYSIS_F
+ done
+ fi
+
+ # Collect systemd logs if present
+ if [ $cl_have_journald -eq 1 ]; then
+ journalctl --since "$cl_start_ymd" --until "$cl_end_ymd" > journal.log
+ grep -f "$cl_pattfile" journal.log >> $ANALYSIS_F
+ fi
+
+ rm -f $cl_pattfile
+ trap "" 0
+}
+
+require_tar
+
+debug "Initializing $REPORT_TARGET subdir"
+if [ "$REPORT_MASTER" != "$REPORT_TARGET" ]; then
+ if [ -e $REPORT_HOME/$REPORT_TARGET ]; then
+ warning "Directory $REPORT_HOME/$REPORT_TARGET already exists, using /tmp/$$/$REPORT_TARGET instead"
+ REPORT_HOME=/tmp/$$
+ fi
+fi
+
+mkdir -p $REPORT_HOME/$REPORT_TARGET
+cd $REPORT_HOME/$REPORT_TARGET
+
+case $CLUSTER in
+ any) cluster=`get_cluster_type`;;
+ *) cluster=$CLUSTER;;
+esac
+
+cluster_cf=`find_cluster_cf $cluster`
+
+# If cluster stack is still "any", this might be a Pacemaker Remote node,
+# so don't complain in that case.
+if [ -z "$cluster_cf" ] && [ $cluster != "any" ]; then
+ warning "Could not determine the location of your cluster configuration"
+fi
+
+if [ "$SEARCH_LOGS" = "1" ]; then
+ logfiles=$(get_logfiles "$cluster" "$cluster_cf" | sort -u)
+fi
+logfiles="$(trim "$logfiles $EXTRA_LOGS")"
+
+if [ -z "$logfiles" ]; then
+ which journalctl > /dev/null 2>&1
+ if [ $? -eq 0 ]; then
+ info "Systemd journal will be only log collected"
+ else
+ info "No logs will be collected"
+ fi
+ info "No log files found or specified with --logfile /some/path"
+fi
+
+debug "Config: $cluster ($cluster_cf) $logfiles"
+
+sys_info $cluster $PACKAGES > $SYSINFO_F
+essential_files $cluster | check_perms > $PERMISSIONS_F 2>&1
+getconfig $cluster "$REPORT_HOME/$REPORT_TARGET" "$cluster_cf" "$CRM_CONFIG_DIR/$CIB_F" "/etc/drbd.conf" "/etc/drbd.d" "/etc/booth"
+
+getpeinputs $LOG_START $LOG_END $REPORT_HOME/$REPORT_TARGET
+getbacktraces $LOG_START $LOG_END > $REPORT_HOME/$REPORT_TARGET/$BT_F
+getblackboxes $LOG_START $LOG_END $REPORT_HOME/$REPORT_TARGET
+
+case $cluster in
+ corosync)
+ if is_running corosync; then
+ corosync-blackbox >corosync-blackbox-live.txt 2>&1
+# corosync-fplay > corosync-blackbox.txt
+ tool=`pickfirst corosync-objctl corosync-cmapctl`
+ case $tool in
+ *objctl) $tool -a > corosync.dump 2>/dev/null;;
+ *cmapctl) $tool > corosync.dump 2>/dev/null;;
+ esac
+ corosync-quorumtool -s -i > corosync.quorum 2>&1
+ fi
+ ;;
+esac
+
+dc=`crm_mon -1 2>/dev/null | awk '/Current DC/ {print $3}'`
+if [ "$REPORT_TARGET" = "$dc" ]; then
+ echo "$REPORT_TARGET" > DC
+fi
+
+dlm_dump > $DLM_DUMP_F 2>&1
+sys_stats > $SYSSTATS_F 2>&1
+drbd_info > $DRBD_INFO_F 2>&1
+
+debug "Sanitizing files: $SANITIZE"
+#
+# replace sensitive info with '****'
+#
+cf=""
+if [ ! -z "$cluster_cf" ]; then
+ cf=`basename $cluster_cf`
+fi
+for f in "$cf" "$CIB_F" "$CIB_F.live" pengine/*; do
+ if [ -f "$f" ]; then
+ sanitize "$f"
+ fi
+done
+
+# For convenience, generate human-readable version of CIB and any XML errors
+# in it (AFTER sanitizing, so we don't need to sanitize this output).
+# sosreport does this itself, so we do not need to when run by sosreport.
+if [ "$SOS_MODE" != "1" ]; then
+ get_readable_cib "$REPORT_HOME/$REPORT_TARGET"
+fi
+
+collect_logs "$LOG_START" "$LOG_END" $logfiles
+
+# Purge files containing no information
+for f in `ls -1`; do
+ if [ -d "$f" ]; then
+ continue
+ elif [ ! -s "$f" ]; then
+ case $f in
+ *core*) log "Detected empty core file: $f";;
+ *) debug "Removing empty file: `ls -al $f`"
+ rm -f $f
+ ;;
+ esac
+ fi
+done
+
+# Parse for events
+for l in $logfiles; do
+ b="$(basename $l).extract.txt"
+ node_events "$b" > $EVENTS_F
+
+ # Link the first logfile to a standard name if it doesn't yet exist
+ if [ -e "$b" -a ! -e "$HALOG_F" ]; then
+ ln -s "$b" "$HALOG_F"
+ fi
+done
+
+if [ -e $REPORT_HOME/.env ]; then
+ debug "Localhost: $REPORT_MASTER $REPORT_TARGET"
+
+elif [ "$REPORT_MASTER" != "$REPORT_TARGET" ]; then
+ debug "Streaming report back to $REPORT_MASTER"
+ (cd $REPORT_HOME && tar cf - $REPORT_TARGET)
+ if [ "$REMOVE" = "1" ]; then
+ cd
+ rm -rf $REPORT_HOME
+ fi
+fi
+
+# vim: set expandtab tabstop=8 softtabstop=4 shiftwidth=4 textwidth=80:
diff --git a/tools/report.common.in b/tools/report.common.in
new file mode 100644
index 0000000..aa90c05
--- /dev/null
+++ b/tools/report.common.in
@@ -0,0 +1,890 @@
+#
+# Originally based on hb_report
+# Copyright 2007 Dejan Muhamedagic <dmuhamedagic@suse.de>
+# Later changes copyright 2010-2022 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.
+#
+
+host=`uname -n`
+shorthost=`echo $host | sed s:\\\\..*::`
+if [ -z $verbose ]; then
+ verbose=0
+fi
+
+# Target Files
+EVENTS_F=events.txt
+ANALYSIS_F=analysis.txt
+HALOG_F=cluster-log.txt
+BT_F=backtraces.txt
+SYSINFO_F=sysinfo.txt
+SYSSTATS_F=sysstats.txt
+DLM_DUMP_F=dlm_dump.txt
+CRM_MON_F=crm_mon.txt
+MEMBERSHIP_F=members.txt
+CRM_VERIFY_F=crm_verify.txt
+PERMISSIONS_F=permissions.txt
+CIB_F=cib.xml
+CIB_TXT_F=cib.txt
+DRBD_INFO_F=drbd_info.txt
+
+EVENT_PATTERNS="
+state do_state_transition
+membership pcmk_peer_update.*(lost|memb):
+quorum (crmd|pacemaker-controld).*crm_update_quorum
+pause Process.pause.detected
+resources (lrmd|pacemaker-execd).*rsc:(start|stop)
+stonith te_fence_node|fenced.*(requests|(Succeeded|Failed).to.|result=)
+start_stop shutdown.decision|Corosync.Cluster.Engine|corosync.*Initializing.transport|Executive.Service.RELEASE|crm_shutdown:.Requesting.shutdown|pcmk_shutdown:.Shutdown.complete
+"
+
+# superset of all packages of interest on all distros
+# (the package manager will be used to validate the installation
+# of any of these packages that are installed)
+PACKAGES="pacemaker pacemaker-libs pacemaker-cluster-libs libpacemaker3
+pacemaker-remote pacemaker-pygui pacemaker-pymgmt pymgmt-client
+corosync corosynclib libcorosync4
+resource-agents cluster-glue-libs cluster-glue libglue2 ldirectord
+ocfs2-tools ocfs2-tools-o2cb ocfs2console
+ocfs2-kmp-default ocfs2-kmp-pae ocfs2-kmp-xen ocfs2-kmp-debug ocfs2-kmp-trace
+drbd drbd-kmp-xen drbd-kmp-pae drbd-kmp-default drbd-kmp-debug drbd-kmp-trace
+drbd-pacemaker drbd-utils drbd-bash-completion drbd-xen
+lvm2 lvm2-clvm cmirrord
+libdlm libdlm2 libdlm3
+hawk ruby lighttpd
+kernel-default kernel-pae kernel-xen
+glibc
+"
+
+# Potential locations of system log and cluster daemon logs
+SYSLOGS="
+ /var/log/*
+ /var/logs/*
+ /var/syslog/*
+ /var/adm/*
+ /var/log/ha/*
+ /var/log/cluster/*
+ /var/log/pacemaker/*
+"
+
+# Whether pacemaker-remoted was found (0 = yes, 1 = no, -1 = haven't looked yet)
+REMOTED_STATUS=-1
+
+#
+# keep the user posted
+#
+record() {
+ if [ x != x"$REPORT_HOME" -a -d "${REPORT_HOME}/$shorthost" ]; then
+ rec="${REPORT_HOME}/$shorthost/report.out"
+
+ elif [ x != x"${l_base}" -a -d "${l_base}" ]; then
+ rec="${l_base}/report.summary"
+
+ else
+ rec="/dev/null"
+ fi
+ printf "%-10s $*\n" "$shorthost:" 2>&1 >> "${rec}"
+}
+
+log() {
+ printf "%-10s $*\n" "$shorthost:" 1>&2
+ record "$*"
+}
+
+debug() {
+ if [ $verbose -gt 0 ]; then
+ log "Debug: $*"
+ else
+ record "Debug: $*"
+ fi
+}
+
+info() {
+ log "$*"
+}
+
+warning() {
+ log "WARN: $*"
+}
+
+fatal() {
+ log "ERROR: $*"
+ exit 1
+}
+
+require_tar() {
+ which tar >/dev/null 2>&1
+ if [ $? -ne 0 ]; then
+ fatal "Required program 'tar' not found, please install and re-run"
+ fi
+}
+
+# check if process of given substring in its name does exist;
+# only look for processes originated by user 0 (by UID), "@CRM_DAEMON_USER@"
+# or effective user running this script, and/or group 0 (by GID),
+# "@CRM_DAEMON_GROUP@" or one of the groups the effective user belongs to
+# (there's no business in probing any other processes)
+is_running() {
+ ps -G "0 $(getent group '@CRM_DAEMON_GROUP@' 2>/dev/null | cut -d: -f3) $(id -G)" \
+ -u "0 @CRM_DAEMON_USER@ $(id -u)" -f \
+ | grep -Eqs $(echo "$1" | sed -e 's/^\(.\)/[\1]/')
+}
+
+has_remoted() {
+ if [ $REMOTED_STATUS -eq -1 ]; then
+ REMOTED_STATUS=1
+ if which pacemaker-remoted >/dev/null 2>&1; then
+ REMOTED_STATUS=0
+ # Check for pre-2.0.0 daemon name in case we have mixed-version cluster
+ elif which pacemaker_remoted >/dev/null 2>&1; then
+ REMOTED_STATUS=0
+ elif [ -x "@sbindir@/pacemaker-remoted" ]; then
+ REMOTED_STATUS=0
+ elif [ -x "@sbindir@/pacemaker_remoted" ]; then
+ REMOTED_STATUS=0
+ else
+ # @TODO: the binary might be elsewhere,
+ # but a global search is too expensive
+ for d in /{usr,opt}/{local/,}{s,}bin; do
+ if [ -x "${d}/pacemaker-remoted" ]; then
+ REMOTED_STATUS=0
+ elif [ -x "${d}/pacemaker_remoted" ]; then
+ REMOTED_STATUS=0
+ fi
+ done
+ fi
+ fi
+ return $REMOTED_STATUS
+}
+
+# found_dir <description> <dirname>
+found_dir() {
+ echo "$2"
+ info "Pacemaker $1 found in: $2"
+}
+
+detect_daemon_dir() {
+ info "Searching for where Pacemaker daemons live... this may take a while"
+
+ for d in \
+ {/usr,/usr/local,/opt/local,@exec_prefix@}/{libexec,lib64,lib}/pacemaker
+ do
+ # pacemaker and pacemaker-cts packages can install to daemon directory,
+ # so check for a file from each
+ if [ -e $d/pacemaker-schedulerd ] || [ -e $d/cts-exec-helper ]; then
+ found_dir "daemons" "$d"
+ return
+ fi
+ done
+
+ # Pacemaker Remote nodes don't need to install daemons
+ if has_remoted; then
+ info "Pacemaker daemons not found (this appears to be a Pacemaker Remote node)"
+ return
+ fi
+
+ for f in $(find / -maxdepth $maxdepth -type f -name pacemaker-schedulerd -o -name cts-exec-helper); do
+ d=$(dirname "$f")
+ found_dir "daemons" "$d"
+ return
+ done
+
+ fatal "Pacemaker daemons not found (nonstandard installation?)"
+}
+
+detect_cib_dir() {
+ d="${local_state_dir}/lib/pacemaker/cib"
+ if [ -f "$d/cib.xml" ]; then
+ found_dir "config files" "$d"
+ return
+ fi
+
+ # Pacemaker Remote nodes don't need a CIB
+ if has_remoted; then
+ info "Pacemaker config not found (this appears to be a Pacemaker Remote node)"
+ return
+ fi
+
+ info "Searching for where Pacemaker keeps config information... this may take a while"
+ # TODO: What about false positives where someone copied the CIB?
+ for f in $(find / -maxdepth $maxdepth -type f -name cib.xml); do
+ d=$(dirname $f)
+ found_dir "config files" "$d"
+ return
+ done
+
+ warning "Pacemaker config not found (nonstandard installation?)"
+}
+
+detect_state_dir() {
+ if [ -n "$CRM_CONFIG_DIR" ]; then
+ # Assume new layout
+ # $local_state_dir/lib/pacemaker/(cib,pengine,blackbox,cores)
+ dirname "$CRM_CONFIG_DIR"
+
+ # Pacemaker Remote nodes might not have a CRM_CONFIG_DIR
+ elif [ -d "$local_state_dir/lib/pacemaker" ]; then
+ echo $local_state_dir/lib/pacemaker
+ fi
+}
+
+detect_pe_dir() {
+ config_root="$1"
+
+ d="$config_root/pengine"
+ if [ -d "$d" ]; then
+ found_dir "scheduler inputs" "$d"
+ return
+ fi
+
+ if has_remoted; then
+ info "Pacemaker scheduler inputs not found (this appears to be a Pacemaker Remote node)"
+ return
+ fi
+
+ info "Searching for where Pacemaker keeps scheduler inputs... this may take a while"
+ for d in $(find / -maxdepth $maxdepth -type d -name pengine); do
+ found_dir "scheduler inputs" "$d"
+ return
+ done
+
+ fatal "Pacemaker scheduler inputs not found (nonstandard installation?)"
+}
+
+detect_host() {
+ local_state_dir=@localstatedir@
+
+ if [ -d $local_state_dir/run ]; then
+ CRM_STATE_DIR=$local_state_dir/run/crm
+ else
+ info "Searching for where Pacemaker keeps runtime data... this may take a while"
+ for d in `find / -maxdepth $maxdepth -type d -name run`; do
+ local_state_dir=`dirname $d`
+ CRM_STATE_DIR=$d/crm
+ break
+ done
+ info "Found: $CRM_STATE_DIR"
+ fi
+ debug "Machine runtime directory: $local_state_dir"
+ debug "Pacemaker runtime data located in: $CRM_STATE_DIR"
+
+ CRM_DAEMON_DIR=$(detect_daemon_dir)
+ CRM_CONFIG_DIR=$(detect_cib_dir)
+ config_root=$(detect_state_dir)
+
+ # Older versions had none
+ BLACKBOX_DIR=$config_root/blackbox
+ debug "Pacemaker blackboxes (if any) located in: $BLACKBOX_DIR"
+
+ PE_STATE_DIR=$(detect_pe_dir "$config_root")
+
+ CRM_CORE_DIRS=""
+ for d in $config_root/cores $local_state_dir/lib/corosync; do
+ if [ -d $d ]; then
+ CRM_CORE_DIRS="$CRM_CORE_DIRS $d"
+ fi
+ done
+ debug "Core files located under: $CRM_CORE_DIRS"
+}
+
+time2str() {
+ perl -e "use POSIX; print strftime('%x %X',localtime($1));"
+}
+
+get_time() {
+ perl -e "\$time=\"$*\";" -e '
+ $unix_tm = 0;
+ eval "use Date::Parse";
+ if (index($time, ":") < 0) {
+ } elsif (!$@) {
+ $unix_tm = str2time($time);
+ } else {
+ eval "use Date::Manip";
+ if (!$@) {
+ $unix_tm = UnixDate(ParseDateString($time), "%s");
+ }
+ }
+ if ($unix_tm != "") {
+ print int($unix_tm);
+ } else {
+ print "";
+ }
+ '
+}
+
+get_time_syslog() {
+ awk '{print $1,$2,$3}'
+}
+
+get_time_legacy() {
+ awk '{print $2}' | sed 's/_/ /'
+}
+
+get_time_iso8601() {
+ awk '{print $1}'
+}
+
+get_time_format_for_string() {
+ l="$*"
+ t=$(get_time `echo $l | get_time_syslog`)
+ if [ "x$t" != x ]; then
+ echo syslog
+ return
+ fi
+
+ t=$(get_time `echo $l | get_time_iso8601`)
+ if [ "x$t" != x ]; then
+ echo iso8601
+ return
+ fi
+
+ t=$(get_time `echo $l | get_time_legacy`)
+ if [ "x$t" != x ]; then
+ echo legacy
+ return
+ fi
+}
+
+get_time_format() {
+ t=0 l="" func=""
+ trycnt=10
+ while [ $trycnt -gt 0 ] && read l; do
+ func=$(get_time_format_for_string $l)
+ if [ "x$func" != x ]; then
+ break
+ fi
+ trycnt=$(($trycnt-1))
+ done
+ #debug "Logfile uses the $func time format"
+ echo $func
+}
+
+get_time_from_line() {
+ GTFL_FORMAT="$1"
+ shift
+ if [ "$GTFL_FORMAT" = "" ]; then
+ GTFL_FORMAT=$(get_time_format_for_string "$@")
+ fi
+ case $GTFL_FORMAT in
+ syslog|legacy|iso8601)
+ get_time $(echo "$@" | get_time_${GTFL_FORMAT})
+ ;;
+ *)
+ warning "Unknown time format in: $@"
+ ;;
+ esac
+}
+
+get_first_time() {
+ l=""
+ format=$1
+ while read l; do
+ ts=$(get_time_from_line "$format" "$l")
+ if [ "x$ts" != x ]; then
+ echo "$ts"
+ return
+ fi
+ done
+}
+
+get_last_time() {
+ l=""
+ best=`date +%s` # Now
+ format=$1
+ while read l; do
+ ts=$(get_time_from_line "$format" "$l")
+ if [ "x$ts" != x ]; then
+ best=$ts
+ fi
+ done
+ echo $best
+}
+
+linetime() {
+ get_time_from_line "" $(tail -n +$2 $1 | grep -a ":[0-5][0-9]:" | head -n 1)
+}
+
+#
+# findmsg <max> <pattern>
+#
+# Print the names of up to <max> system logs that contain <pattern>,
+# ordered by most recently modified.
+#
+findmsg() {
+ max=$1
+ pattern="$2"
+ found=0
+
+ # List all potential system logs ordered by most recently modified.
+ candidates=$(ls -1td $SYSLOGS 2>/dev/null)
+ if [ -z "$candidates" ]; then
+ debug "No system logs found to search for pattern \'$pattern\'"
+ return
+ fi
+
+ # Portable way to handle files with spaces in their names.
+ SAVE_IFS=$IFS
+ IFS="
+"
+
+ # Check each log file for matches.
+ logfiles=""
+ for f in $candidates; do
+ local cat=""
+
+ # We only care about readable files with something in them.
+ if [ ! -f "$f" ] || [ ! -r "$f" ] || [ ! -s "$f" ] ; then
+ continue
+ fi
+
+ cat=$(find_decompressor "$f")
+
+ # We want to avoid grepping through potentially huge binary logs such
+ # as lastlog. However, control characters sometimes find their way into
+ # text logs, so we use a heuristic of more than 256 nonprintable
+ # characters in the file's first kilobyte.
+ if [ $($cat "$f" 2>/dev/null | head -c 1024 | tr -d '[:print:][:space:]' | wc -c) -gt 256 ]
+ then
+ continue
+ fi
+
+ # Our patterns are ASCII, so we can use LC_ALL="C" to speed up grep
+ $cat "$f" 2>/dev/null | LC_ALL="C" grep -q -e "$pattern"
+ if [ $? -eq 0 ]; then
+
+ # Add this file to the list of hits
+ # (using newline as separator to handle spaces in names).
+ if [ -z "$logfiles" ]; then
+ logfiles="$f"
+ else
+ logfiles="$logfiles
+$f"
+ fi
+
+ # If we have enough hits, print them and return.
+ found=$(($found+1))
+ if [ $found -ge $max ]; then
+ break
+ fi
+ fi
+ done 2>/dev/null
+ IFS=$SAVE_IFS
+ if [ -z "$logfiles" ]; then
+ debug "Pattern \'$pattern\' not found in any system logs"
+ else
+ debug "Pattern \'$pattern\' found in: [ $logfiles ]"
+ echo "$logfiles"
+ fi
+}
+
+node_events() {
+ if [ -e $1 ]; then
+ Epatt=`echo "$EVENT_PATTERNS" |
+ while read title p; do [ -n "$p" ] && echo -n "|$p"; done |
+ sed 's/.//'
+ `
+ grep -E "$Epatt" $1
+ fi
+}
+
+pickfirst() {
+ for x; do
+ which $x >/dev/null 2>&1 && {
+ echo $x
+ return 0
+ }
+ done
+ return 1
+}
+
+shrink() {
+ olddir=$PWD
+ dir=`dirname $1`
+ base=`basename $1`
+
+ target=$1.tar
+ tar_options="cf"
+
+ variant=`pickfirst bzip2 gzip xz false`
+ case $variant in
+ bz*)
+ tar_options="jcf"
+ target="$target.bz2"
+ ;;
+ gz*)
+ tar_options="zcf"
+ target="$target.gz"
+ ;;
+ xz*)
+ tar_options="Jcf"
+ target="$target.xz"
+ ;;
+ *)
+ warning "Could not find a compression program, the resulting tarball may be huge"
+ ;;
+ esac
+
+ if [ -e $target ]; then
+ fatal "Destination $target already exists, specify an alternate name with --dest"
+ fi
+
+ cd $dir >/dev/null 2>&1
+ tar $tar_options $target $base >/dev/null 2>&1
+ if [ $? -ne 0 ]; then
+ fatal "Could not archive $base, please investigate and collect manually"
+ fi
+ cd $olddir >/dev/null 2>&1
+
+ echo $target
+}
+
+findln_by_time() {
+ local logf=$1
+ local tm=$2
+ local first=1
+
+ # Some logs can be massive (over 1,500,000,000 lines have been seen in the wild)
+ # Even just 'wc -l' on these files can take 10+ minutes
+
+ local fileSize=`ls -lh "$logf" | awk '{ print $5 }' | grep -ie G`
+ if [ x$fileSize != x ]; then
+ warning "$logf is ${fileSize} in size and could take many hours to process. Skipping."
+ return
+ fi
+
+ local last=`wc -l < $logf`
+ while [ $first -le $last ]; do
+ mid=$((($last+$first)/2))
+ trycnt=10
+ while [ $trycnt -gt 0 ]; do
+ tmid=`linetime $logf $mid`
+ [ "$tmid" ] && break
+ warning "cannot extract time: $logf:$mid; will try the next one"
+ trycnt=$(($trycnt-1))
+ # shift the whole first-last segment
+ first=$(($first-1))
+ last=$(($last-1))
+ mid=$((($last+$first)/2))
+ done
+ if [ -z "$tmid" ]; then
+ warning "giving up on log..."
+ return
+ fi
+ if [ $tmid -gt $tm ]; then
+ last=$(($mid-1))
+ elif [ $tmid -lt $tm ]; then
+ first=$(($mid+1))
+ else
+ break
+ fi
+ done
+ echo $mid
+}
+
+dumplog() {
+ local logf=$1
+ local from_line=$2
+ local to_line=$3
+ [ "$from_line" ] ||
+ return
+ tail -n +$from_line $logf |
+ if [ "$to_line" ]; then
+ head -$(($to_line-$from_line+1))
+ else
+ cat
+ fi
+}
+
+#
+# find log/set of logs which are interesting for us
+#
+#
+# find log slices
+#
+
+find_decompressor() {
+ case $1 in
+ *bz2) echo "bzip2 -dc" ;;
+ *gz) echo "gzip -dc" ;;
+ *xz) echo "xz -dc" ;;
+ *) echo "cat" ;;
+ esac
+}
+
+#
+# check if the log contains a piece of our segment
+#
+is_our_log() {
+ local logf=$1
+ local from_time=$2
+ local to_time=$3
+
+ local cat=`find_decompressor $logf`
+ local format=`$cat $logf | get_time_format`
+ local first_time=`$cat $logf | head -10 | get_first_time $format`
+ local last_time=`$cat $logf | tail -10 | get_last_time $format`
+
+ if [ x = "x$first_time" -o x = "x$last_time" ]; then
+ warning "Skipping bad logfile '$1': Could not determine log dates"
+ return 0 # skip (empty log?)
+ fi
+ if [ $from_time -gt $last_time ]; then
+ # we shouldn't get here anyway if the logs are in order
+ return 2 # we're past good logs; exit
+ fi
+ if [ $from_time -ge $first_time ]; then
+ return 3 # this is the last good log
+ fi
+ # have to go further back
+ if [ x = "x$to_time" -o $to_time -ge $first_time ]; then
+ return 1 # include this log
+ else
+ return 0 # don't include this log
+ fi
+}
+#
+# go through archived logs (timewise backwards) and see if there
+# are lines belonging to us
+# (we rely on untouched log files, i.e. that modify time
+# hasn't been changed)
+#
+arch_logs() {
+ local logf=$1
+ local from_time=$2
+ local to_time=$3
+
+ # look for files such as: ha-log-20090308 or
+ # ha-log-20090308.gz (.bz2) or ha-log.0, etc
+ ls -t $logf $logf*[0-9z] 2>/dev/null |
+ while read next_log; do
+ is_our_log $next_log $from_time $to_time
+ case $? in
+ 0) ;; # noop, continue
+ 1) echo $next_log # include log and continue
+ debug "Found log $next_log"
+ ;;
+ 2) break;; # don't go through older logs!
+ 3) echo $next_log # include log and continue
+ debug "Found log $next_log"
+ break
+ ;; # don't go through older logs!
+ esac
+ done
+}
+
+#
+# print part of the log
+#
+drop_tmp_file() {
+ [ -z "$tmp" ] || rm -f "$tmp"
+}
+
+print_logseg() {
+ local logf=$1
+ local from_time=$2
+ local to_time=$3
+
+ # uncompress to a temp file (if necessary)
+ local cat=`find_decompressor $logf`
+ if [ "$cat" != "cat" ]; then
+ tmp=`mktemp`
+ $cat $logf > $tmp
+ trap drop_tmp_file 0
+ sourcef=$tmp
+ else
+ sourcef=$logf
+ tmp=""
+ fi
+
+ if [ "$from_time" = 0 ]; then
+ FROM_LINE=1
+ else
+ FROM_LINE=`findln_by_time $sourcef $from_time`
+ fi
+ if [ -z "$FROM_LINE" ]; then
+ warning "couldn't find line for time $from_time; corrupt log file?"
+ return
+ fi
+
+ TO_LINE=""
+ if [ "$to_time" != 0 ]; then
+ TO_LINE=`findln_by_time $sourcef $to_time`
+ if [ -z "$TO_LINE" ]; then
+ warning "couldn't find line for time $to_time; corrupt log file?"
+ return
+ fi
+ if [ $FROM_LINE -lt $TO_LINE ]; then
+ dumplog $sourcef $FROM_LINE $TO_LINE
+ log "Including segment [$FROM_LINE-$TO_LINE] from $logf"
+ else
+ debug "Empty segment [$FROM_LINE-$TO_LINE] from $logf"
+ fi
+ else
+ dumplog $sourcef $FROM_LINE $TO_LINE
+ log "Including all logs after line $FROM_LINE from $logf"
+ fi
+ drop_tmp_file
+ trap "" 0
+}
+
+#
+# find log/set of logs which are interesting for us
+#
+dumplogset() {
+ local logf=$1
+ local from_time=$2
+ local to_time=$3
+
+ local logf_set=`arch_logs $logf $from_time $to_time`
+ if [ x = "x$logf_set" ]; then
+ return
+ fi
+
+ local num_logs=`echo "$logf_set" | wc -l`
+ local oldest=`echo $logf_set | awk '{print $NF}'`
+ local newest=`echo $logf_set | awk '{print $1}'`
+ local mid_logfiles=`echo $logf_set | awk '{for(i=NF-1; i>1; i--) print $i}'`
+
+ # the first logfile: from $from_time to $to_time (or end)
+ # logfiles in the middle: all
+ # the last logfile: from beginning to $to_time (or end)
+ case $num_logs in
+ 1) print_logseg $newest $from_time $to_time;;
+ *)
+ print_logseg $oldest $from_time 0
+ for f in $mid_logfiles; do
+ `find_decompressor $f` $f
+ debug "including complete $f logfile"
+ done
+ print_logseg $newest 0 $to_time
+ ;;
+ esac
+}
+
+# cut out a stanza
+getstanza() {
+ awk -v name="$1" '
+ !in_stanza && NF==2 && /^[a-z][a-z]*[[:space:]]*{/ { # stanza start
+ if ($1 == name)
+ in_stanza = 1
+ }
+ in_stanza { print }
+ in_stanza && NF==1 && $1 == "}" { exit }
+ '
+}
+# supply stanza in $1 and variable name in $2
+# (stanza is optional)
+getcfvar() {
+ cf_type=$1; shift;
+ cf_var=$1; shift;
+ cf_file=$*
+
+ [ -f "$cf_file" ] || return
+ case $cf_type in
+ corosync)
+ sed 's/#.*//' < $cf_file |
+ if [ $# -eq 2 ]; then
+ getstanza "$cf_var"
+ shift 1
+ else
+ cat
+ fi |
+ awk -v varname="$cf_var" '
+ NF==2 && match($1,varname":$")==1 { print $2; exit; }
+ '
+ ;;
+ esac
+}
+
+pickfirst() {
+ for x; do
+ which $x >/dev/null 2>&1 && {
+ echo $x
+ return 0
+ }
+ done
+ return 1
+}
+
+#
+# figure out the cluster type, depending on the process list
+# and existence of configuration files
+#
+get_cluster_type() {
+ if is_running corosync; then
+ tool=`pickfirst corosync-objctl corosync-cmapctl`
+ case $tool in
+ *objctl) quorum=`$tool -a | grep quorum.provider | sed 's/.*=\s*//'`;;
+ *cmapctl) quorum=`$tool | grep quorum.provider | sed 's/.*=\s*//'`;;
+ esac
+ stack="corosync"
+
+ # Now we're guessing...
+
+ # TODO: Technically these could be anywhere :-/
+ elif [ -f "@PCMK__COROSYNC_CONF@" ]; then
+ stack="corosync"
+
+ else
+ # We still don't know. This might be a Pacemaker Remote node,
+ # or the configuration might be in a nonstandard location.
+ stack="any"
+ fi
+
+ debug "Detected the '$stack' cluster stack"
+ echo $stack
+}
+
+find_cluster_cf() {
+ case $1 in
+ corosync)
+ best_size=0
+ best_file=""
+
+ # TODO: Technically these could be anywhere :-/
+ for cf in "@PCMK__COROSYNC_CONF@"; do
+ if [ -f $cf ]; then
+ size=`wc -l $cf | awk '{print $1}'`
+ if [ $size -gt $best_size ]; then
+ best_size=$size
+ best_file=$cf
+ fi
+ fi
+ done
+ if [ -z "$best_file" ]; then
+ debug "Looking for corosync configuration file. This may take a while..."
+ for f in `find / -maxdepth $maxdepth -type f -name corosync.conf`; do
+ best_file=$f
+ break
+ done
+ fi
+ debug "Located corosync config file: $best_file"
+ echo "$best_file"
+ ;;
+ any)
+ # Cluster type is undetermined. Don't complain, because this
+ # might be a Pacemaker Remote node.
+ ;;
+ *)
+ warning "Unknown cluster type: $1"
+ ;;
+ esac
+}
+
+#
+# check for the major prereq for a) parameter parsing and b)
+# parsing logs
+#
+t=`get_time "12:00"`
+if [ "$t" = "" ]; then
+ fatal "please install the perl Date::Parse module (perl-DateTime-Format-DateParse on Fedora/Red Hat)"
+fi
+
+# Override any locale settings so collected output is in a common language
+LC_ALL="C"
+export LC_ALL
+
+# vim: set expandtab tabstop=8 softtabstop=4 shiftwidth=4 textwidth=80:
diff --git a/tools/stonith_admin.8.inc b/tools/stonith_admin.8.inc
new file mode 100644
index 0000000..681ed1f
--- /dev/null
+++ b/tools/stonith_admin.8.inc
@@ -0,0 +1,5 @@
+[=synopsis]
+stonith_admin <command> [<options>]
+
+/Access the Pacemaker fencing API/
+.SH OPTIONS
diff --git a/tools/stonith_admin.c b/tools/stonith_admin.c
new file mode 100644
index 0000000..1077de7
--- /dev/null
+++ b/tools/stonith_admin.c
@@ -0,0 +1,674 @@
+/*
+ * Copyright 2009-2023 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 <sys/param.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <sys/utsname.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <crm/crm.h>
+#include <crm/msg_xml.h>
+#include <crm/common/ipc.h>
+#include <crm/cluster/internal.h>
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
+
+#include <crm/stonith-ng.h>
+#include <crm/fencing/internal.h>
+#include <crm/cib.h>
+#include <crm/pengine/status.h>
+
+#include <crm/common/xml.h>
+#include <pacemaker-internal.h>
+
+#define SUMMARY "stonith_admin - Access the Pacemaker fencing API"
+
+char action = 0;
+
+struct {
+ gboolean as_nodeid;
+ gboolean broadcast;
+ gboolean cleanup;
+ gboolean installed;
+ gboolean metadata;
+ gboolean registered;
+ gboolean validate_cfg;
+ stonith_key_value_t *devices;
+ stonith_key_value_t *params;
+ int fence_level;
+ int timeout ;
+ int tolerance;
+ int delay;
+ char *agent;
+ char *confirm_host;
+ char *fence_host;
+ char *history;
+ char *last_fenced;
+ char *query;
+ char *reboot_host;
+ char *register_dev;
+ char *register_level;
+ char *targets;
+ char *terminate;
+ char *unfence_host;
+ char *unregister_dev;
+ char *unregister_level;
+} options = {
+ .timeout = 120,
+ .delay = 0
+};
+
+gboolean add_env_params(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean add_stonith_device(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean add_stonith_params(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean add_tolerance(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+gboolean set_tag(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+
+#define INDENT " "
+
+/* *INDENT-OFF* */
+static GOptionEntry defn_entries[] = {
+ { "register", 'R', 0, G_OPTION_ARG_STRING, &options.register_dev,
+ "Register the named stonith device. Requires: --agent.\n"
+ INDENT "Optional: --option, --env-option.",
+ "DEVICE" },
+ { "deregister", 'D', 0, G_OPTION_ARG_STRING, &options.unregister_dev,
+ "De-register the named stonith device.",
+ "DEVICE" },
+ { "register-level", 'r', 0, G_OPTION_ARG_STRING, &options.register_level,
+ "Register a stonith level for the named target,\n"
+ INDENT "specified as one of NAME, @PATTERN, or ATTR=VALUE.\n"
+ INDENT "Requires: --index and one or more --device entries.",
+ "TARGET" },
+ { "deregister-level", 'd', 0, G_OPTION_ARG_STRING, &options.unregister_level,
+ "Unregister a stonith level for the named target,\n"
+ INDENT "specified as for --register-level. Requires: --index",
+ "TARGET" },
+
+ { NULL }
+};
+
+static GOptionEntry query_entries[] = {
+ { "list", 'l', 0, G_OPTION_ARG_STRING, &options.terminate,
+ "List devices that can terminate the specified host.\n"
+ INDENT "Optional: --timeout",
+ "HOST" },
+ { "list-registered", 'L', 0, G_OPTION_ARG_NONE, &options.registered,
+ "List all registered devices. Optional: --timeout.",
+ NULL },
+ { "list-installed", 'I', 0, G_OPTION_ARG_NONE, &options.installed,
+ "List all installed devices. Optional: --timeout.",
+ NULL },
+ { "list-targets", 's', 0, G_OPTION_ARG_STRING, &options.targets,
+ "List the targets that can be fenced by the\n"
+ INDENT "named device. Optional: --timeout.",
+ "DEVICE" },
+ { "metadata", 'M', 0, G_OPTION_ARG_NONE, &options.metadata,
+ "Show agent metadata. Requires: --agent.\n"
+ INDENT "Optional: --timeout.",
+ NULL },
+ { "query", 'Q', 0, G_OPTION_ARG_STRING, &options.query,
+ "Check the named device's status. Optional: --timeout.",
+ "DEVICE" },
+ { "history", 'H', 0, G_OPTION_ARG_STRING, &options.history,
+ "Show last successful fencing operation for named node\n"
+ INDENT "(or '*' for all nodes). Optional: --timeout, --cleanup,\n"
+ INDENT "--quiet (show only the operation's epoch timestamp),\n"
+ INDENT "--verbose (show all recorded and pending operations),\n"
+ INDENT "--broadcast (update history from all nodes available).",
+ "NODE" },
+ { "last", 'h', 0, G_OPTION_ARG_STRING, &options.last_fenced,
+ "Indicate when the named node was last fenced.\n"
+ INDENT "Optional: --as-node-id.",
+ "NODE" },
+ { "validate", 'K', 0, G_OPTION_ARG_NONE, &options.validate_cfg,
+ "Validate a fence device configuration.\n"
+ INDENT "Requires: --agent. Optional: --option, --env-option,\n"
+ INDENT "--quiet (print no output, only return status).",
+ NULL },
+
+ { NULL }
+};
+
+static GOptionEntry fence_entries[] = {
+ { "fence", 'F', 0, G_OPTION_ARG_STRING, &options.fence_host,
+ "Fence named host. Optional: --timeout, --tolerance, --delay.",
+ "HOST" },
+ { "unfence", 'U', 0, G_OPTION_ARG_STRING, &options.unfence_host,
+ "Unfence named host. Optional: --timeout, --tolerance, --delay.",
+ "HOST" },
+ { "reboot", 'B', 0, G_OPTION_ARG_STRING, &options.reboot_host,
+ "Reboot named host. Optional: --timeout, --tolerance, --delay.",
+ "HOST" },
+ { "confirm", 'C', 0, G_OPTION_ARG_STRING, &options.confirm_host,
+ "Tell cluster that named host is now safely down.",
+ "HOST", },
+
+ { NULL }
+};
+
+static GOptionEntry addl_entries[] = {
+ { "cleanup", 'c', 0, G_OPTION_ARG_NONE, &options.cleanup,
+ "Cleanup wherever appropriate. Requires --history.",
+ NULL },
+ { "broadcast", 'b', 0, G_OPTION_ARG_NONE, &options.broadcast,
+ "Broadcast wherever appropriate.",
+ NULL },
+ { "agent", 'a', 0, G_OPTION_ARG_STRING, &options.agent,
+ "The agent to use (for example, fence_xvm;\n"
+ INDENT "with --register, --metadata, --validate).",
+ "AGENT" },
+ { "option", 'o', 0, G_OPTION_ARG_CALLBACK, add_stonith_params,
+ "Specify a device configuration parameter as NAME=VALUE\n"
+ INDENT "(may be specified multiple times; with --register,\n"
+ INDENT "--validate).",
+ "PARAM" },
+ { "env-option", 'e', 0, G_OPTION_ARG_CALLBACK, add_env_params,
+ "Specify a device configuration parameter with the\n"
+ INDENT "specified name, using the value of the\n"
+ INDENT "environment variable of the same name prefixed with\n"
+ INDENT "OCF_RESKEY_ (may be specified multiple times;\n"
+ INDENT "with --register, --validate).",
+ "PARAM" },
+ { "tag", 'T', 0, G_OPTION_ARG_CALLBACK, set_tag,
+ "Identify fencing operations in logs with the specified\n"
+ INDENT "tag; useful when multiple entities might invoke\n"
+ INDENT "stonith_admin (used with most commands).",
+ "TAG" },
+ { "device", 'v', 0, G_OPTION_ARG_CALLBACK, add_stonith_device,
+ "Device ID (with --register-level, device to associate with\n"
+ INDENT "a given host and level; may be specified multiple times)"
+#if SUPPORT_CIBSECRETS
+ "\n" INDENT "(with --validate, name to use to load CIB secrets)"
+#endif
+ ".",
+ "DEVICE" },
+ { "index", 'i', 0, G_OPTION_ARG_INT, &options.fence_level,
+ "The stonith level (1-9) (with --register-level,\n"
+ INDENT "--deregister-level).",
+ "LEVEL" },
+ { "timeout", 't', 0, G_OPTION_ARG_INT, &options.timeout,
+ "Operation timeout in seconds (default 120;\n"
+ INDENT "used with most commands).",
+ "SECONDS" },
+ { "delay", 'y', 0, G_OPTION_ARG_INT, &options.delay,
+ "Apply a fencing delay in seconds. Any static/random delays from\n"
+ INDENT "pcmk_delay_base/max will be added, otherwise all\n"
+ INDENT "disabled with the value -1\n"
+ INDENT "(default 0; with --fence, --reboot, --unfence).",
+ "SECONDS" },
+ { "as-node-id", 'n', 0, G_OPTION_ARG_NONE, &options.as_nodeid,
+ "(Advanced) The supplied node is the corosync node ID\n"
+ INDENT "(with --last).",
+ NULL },
+ { "tolerance", 0, 0, G_OPTION_ARG_CALLBACK, add_tolerance,
+ "(Advanced) Do nothing if an equivalent --fence request\n"
+ INDENT "succeeded less than this many seconds earlier\n"
+ INDENT "(with --fence, --unfence, --reboot).",
+ "SECONDS" },
+
+ { NULL }
+};
+/* *INDENT-ON* */
+
+static pcmk__supported_format_t formats[] = {
+ PCMK__SUPPORTED_FORMAT_HTML,
+ PCMK__SUPPORTED_FORMAT_NONE,
+ PCMK__SUPPORTED_FORMAT_TEXT,
+ PCMK__SUPPORTED_FORMAT_XML,
+ { NULL, NULL, NULL }
+};
+
+static const int st_opts = st_opt_sync_call | st_opt_allow_suicide;
+
+static char *name = NULL;
+
+gboolean
+add_env_params(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ char *key = crm_strdup_printf("OCF_RESKEY_%s", optarg);
+ const char *env = getenv(key);
+ gboolean retval = TRUE;
+
+ if (env == NULL) {
+ g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_INVALID_PARAM, "Invalid option: -e %s", optarg);
+ retval = FALSE;
+ } else {
+ crm_info("Got: '%s'='%s'", optarg, env);
+ options.params = stonith_key_value_add(options.params, optarg, env);
+ }
+
+ free(key);
+ return retval;
+}
+
+gboolean
+add_stonith_device(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.devices = stonith_key_value_add(options.devices, NULL, optarg);
+ return TRUE;
+}
+
+gboolean
+add_tolerance(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ options.tolerance = crm_get_msec(optarg) / 1000;
+ return TRUE;
+}
+
+gboolean
+add_stonith_params(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ char *name = NULL;
+ char *value = NULL;
+ int rc = 0;
+ gboolean retval = TRUE;
+
+ crm_info("Scanning: -o %s", optarg);
+
+ rc = pcmk__scan_nvpair(optarg, &name, &value);
+
+ if (rc != 2) {
+ rc = pcmk_legacy2rc(rc);
+ g_set_error(error, PCMK__RC_ERROR, rc, "Invalid option: -o %s: %s", optarg, pcmk_rc_str(rc));
+ retval = FALSE;
+ } else {
+ crm_info("Got: '%s'='%s'", name, value);
+ options.params = stonith_key_value_add(options.params, name, value);
+ }
+
+ free(name);
+ free(value);
+ return retval;
+}
+
+gboolean
+set_tag(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
+ free(name);
+ name = crm_strdup_printf("%s.%s", crm_system_name, optarg);
+ return TRUE;
+}
+
+static GOptionContext *
+build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
+ GOptionContext *context = NULL;
+
+ GOptionEntry extra_prog_entries[] = {
+ { "quiet", 'q', 0, G_OPTION_ARG_NONE, &(args->quiet),
+ "Be less descriptive in output.",
+ NULL },
+
+ { NULL }
+ };
+
+ context = pcmk__build_arg_context(args, "text (default), html, xml", group, NULL);
+
+ /* Add the -q option, which cannot be part of the globally supported options
+ * because some tools use that flag for something else.
+ */
+ pcmk__add_main_args(context, extra_prog_entries);
+
+ pcmk__add_arg_group(context, "definition", "Device Definition Commands:",
+ "Show device definition help", defn_entries);
+ pcmk__add_arg_group(context, "queries", "Queries:",
+ "Show query help", query_entries);
+ pcmk__add_arg_group(context, "fence", "Fencing Commands:",
+ "Show fence help", fence_entries);
+ pcmk__add_arg_group(context, "additional", "Additional Options:",
+ "Show additional options", addl_entries);
+ return context;
+}
+
+// \return Standard Pacemaker return code
+static int
+request_fencing(stonith_t *st, const char *target, const char *command,
+ GError **error)
+{
+ char *reason = NULL;
+ int rc = pcmk__request_fencing(st, target, command, name,
+ options.timeout * 1000,
+ options.tolerance * 1000,
+ options.delay, &reason);
+
+ if (rc != pcmk_rc_ok) {
+ const char *rc_str = pcmk_rc_str(rc);
+ const char *what = (strcmp(command, "on") == 0)? "unfence" : "fence";
+
+ // If reason is identical to return code string, don't display it twice
+ if (pcmk__str_eq(rc_str, reason, pcmk__str_none)) {
+ free(reason);
+ reason = NULL;
+ }
+
+ g_set_error(error, PCMK__RC_ERROR, rc,
+ "Couldn't %s %s: %s%s%s%s",
+ what, target, rc_str,
+ ((reason == NULL)? "" : " ("),
+ ((reason == NULL)? "" : reason),
+ ((reason == NULL)? "" : ")"));
+ }
+ free(reason);
+ return rc;
+}
+
+int
+main(int argc, char **argv)
+{
+ int rc = 0;
+ crm_exit_t exit_code = CRM_EX_OK;
+ bool no_connect = false;
+ bool required_agent = false;
+
+ char *target = NULL;
+ const char *device = NULL;
+ stonith_t *st = NULL;
+
+ GError *error = NULL;
+
+ pcmk__output_t *out = NULL;
+
+ GOptionGroup *output_group = NULL;
+ pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
+ gchar **processed_args = pcmk__cmdline_preproc(argv, "adehilorstvyBCDFHQRTU");
+ GOptionContext *context = build_arg_context(args, &output_group);
+
+ pcmk__register_formats(output_group, formats);
+ if (!g_option_context_parse_strv(context, &processed_args, &error)) {
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ pcmk__cli_init_logging("stonith_admin", args->verbosity);
+
+ if (name == NULL) {
+ name = strdup(crm_system_name);
+ }
+
+ rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
+ if (rc != pcmk_rc_ok) {
+ exit_code = CRM_EX_ERROR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s",
+ args->output_ty, pcmk_rc_str(rc));
+ goto done;
+ }
+
+ stonith__register_messages(out);
+
+ if (args->version) {
+ out->version(out, false);
+ goto done;
+ }
+
+ if (options.validate_cfg) {
+ required_agent = true;
+ no_connect = true;
+ action = 'K';
+ }
+
+ if (options.installed) {
+ no_connect = true;
+ action = 'I';
+ }
+
+ if (options.registered) {
+ action = 'L';
+ }
+
+ if (options.register_dev != NULL) {
+ required_agent = true;
+ action = 'R';
+ device = options.register_dev;
+ }
+
+ if (options.query != NULL) {
+ action = 'Q';
+ device = options.query;
+ }
+
+ if (options.unregister_dev != NULL) {
+ action = 'D';
+ device = options.unregister_dev;
+ }
+
+ if (options.targets != NULL) {
+ action = 's';
+ device = options.targets;
+ }
+
+ if (options.terminate != NULL) {
+ action = 'L';
+ target = options.terminate;
+ }
+
+ if (options.metadata) {
+ no_connect = true;
+ required_agent = true;
+ action = 'M';
+ }
+
+ if (options.reboot_host != NULL) {
+ no_connect = true;
+ action = 'B';
+ target = options.reboot_host;
+ crm_log_args(argc, argv);
+ }
+
+ if (options.fence_host != NULL) {
+ no_connect = true;
+ action = 'F';
+ target = options.fence_host;
+ crm_log_args(argc, argv);
+ }
+
+ if (options.unfence_host != NULL) {
+ no_connect = true;
+ action = 'U';
+ target = options.unfence_host;
+ crm_log_args(argc, argv);
+ }
+
+ if (options.confirm_host != NULL) {
+ action = 'C';
+ target = options.confirm_host;
+ crm_log_args(argc, argv);
+ }
+
+ if (options.last_fenced != NULL) {
+ action = 'h';
+ target = options.last_fenced;
+ }
+
+ if (options.history != NULL) {
+ action = 'H';
+ target = options.history;
+ }
+
+ if (options.register_level != NULL) {
+ action = 'r';
+ target = options.register_level;
+ }
+
+ if (options.unregister_level != NULL) {
+ action = 'd';
+ target = options.unregister_level;
+ }
+
+ if (action == 0) {
+ char *help = g_option_context_get_help(context, TRUE, NULL);
+
+ out->err(out, "%s", help);
+ g_free(help);
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ if (required_agent && options.agent == NULL) {
+ char *help = g_option_context_get_help(context, TRUE, NULL);
+
+ out->err(out, "Please specify an agent to query using -a,--agent [value]");
+ out->err(out, "%s", help);
+ g_free(help);
+ exit_code = CRM_EX_USAGE;
+ goto done;
+ }
+
+ out->quiet = args->quiet;
+
+ st = stonith_api_new();
+ if (st == NULL) {
+ rc = -ENOMEM;
+ } else if (!no_connect) {
+ rc = st->cmds->connect(st, name, NULL);
+ }
+ if (rc < 0) {
+ out->err(out, "Could not connect to fencer: %s", pcmk_strerror(rc));
+ exit_code = CRM_EX_DISCONNECT;
+ goto done;
+ }
+
+ switch (action) {
+ case 'I':
+ rc = pcmk__fence_installed(out, st, options.timeout*1000);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Failed to list installed devices: %s", pcmk_strerror(rc));
+ }
+
+ break;
+
+ case 'L':
+ rc = pcmk__fence_registered(out, st, target, options.timeout*1000);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Failed to list registered devices: %s", pcmk_strerror(rc));
+ }
+
+ break;
+
+ case 'Q':
+ rc = st->cmds->monitor(st, st_opts, device, options.timeout);
+ if (rc != pcmk_rc_ok) {
+ rc = st->cmds->list(st, st_opts, device, NULL, options.timeout);
+ }
+ rc = pcmk_legacy2rc(rc);
+ break;
+
+ case 's':
+ rc = pcmk__fence_list_targets(out, st, device, options.timeout*1000);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Couldn't list targets: %s", pcmk_strerror(rc));
+ }
+
+ break;
+
+ case 'R':
+ rc = st->cmds->register_device(st, st_opts, device, NULL, options.agent,
+ options.params);
+ rc = pcmk_legacy2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Can't register device %s using agent %s: %s",
+ device, options.agent, pcmk_rc_str(rc));
+ }
+ break;
+
+ case 'D':
+ rc = st->cmds->remove_device(st, st_opts, device);
+ rc = pcmk_legacy2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Can't unregister device %s: %s",
+ device, pcmk_rc_str(rc));
+ }
+ break;
+
+ case 'd':
+ rc = pcmk__fence_unregister_level(st, target, options.fence_level);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Can't unregister topology level %d for %s: %s",
+ options.fence_level, target, pcmk_rc_str(rc));
+ }
+ break;
+
+ case 'r':
+ rc = pcmk__fence_register_level(st, target, options.fence_level, options.devices);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Can't register topology level %d for %s: %s",
+ options.fence_level, target, pcmk_rc_str(rc));
+ }
+ break;
+
+ case 'M':
+ rc = pcmk__fence_metadata(out, st, options.agent, options.timeout*1000);
+ if (rc != pcmk_rc_ok) {
+ out->err(out, "Can't get fence agent meta-data: %s",
+ pcmk_rc_str(rc));
+ }
+
+ break;
+
+ case 'C':
+ rc = st->cmds->confirm(st, st_opts, target);
+ rc = pcmk_legacy2rc(rc);
+ break;
+
+ case 'B':
+ rc = request_fencing(st, target, "reboot", &error);
+ break;
+
+ case 'F':
+ rc = request_fencing(st, target, "off", &error);
+ break;
+
+ case 'U':
+ rc = request_fencing(st, target, "on", &error);
+ break;
+
+ case 'h':
+ rc = pcmk__fence_last(out, target, options.as_nodeid);
+ break;
+
+ case 'H':
+ rc = pcmk__fence_history(out, st, target, options.timeout*1000, args->verbosity,
+ options.broadcast, options.cleanup);
+ break;
+
+ case 'K':
+ device = options.devices ? options.devices->key : NULL;
+ rc = pcmk__fence_validate(out, st, options.agent, device, options.params,
+ options.timeout*1000);
+ break;
+ }
+
+ crm_info("Command returned: %s (%d)", pcmk_rc_str(rc), rc);
+ exit_code = pcmk_rc2exitc(rc);
+
+ done:
+ g_strfreev(processed_args);
+ pcmk__free_arg_context(context);
+
+ pcmk__output_and_clear_error(&error, out);
+
+ if (out != NULL) {
+ out->finish(out, exit_code, true, NULL);
+ pcmk__output_free(out);
+ }
+ pcmk__unregister_formats();
+ free(name);
+ stonith_key_value_freeall(options.params, 1, 1);
+
+ if (st != NULL) {
+ st->cmds->disconnect(st);
+ stonith_api_delete(st);
+ }
+
+ return exit_code;
+}