diff --git a/daemons/based/based_callbacks.c b/daemons/based/based_callbacks.c index 13646eb4be..1d34055d9d 100644 --- a/daemons/based/based_callbacks.c +++ b/daemons/based/based_callbacks.c @@ -1,1432 +1,1433 @@ /* * Copyright 2004-2025 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 #include #include #include #include #include #include // uint32_t, uint64_t, UINT64_C() #include #include #include // PRIu64 #include #include #include // xmlXPathObject, etc. #include #include #include #include #include #include #define EXIT_ESCALATION_MS 10000 qb_ipcs_service_t *ipcs_ro = NULL; qb_ipcs_service_t *ipcs_rw = NULL; qb_ipcs_service_t *ipcs_shm = NULL; static int cib_process_command(xmlNode *request, const cib__operation_t *operation, cib__op_fn_t op_function, xmlNode **reply, xmlNode **cib_diff, bool privileged); static gboolean cib_common_callback(qb_ipcs_connection_t *c, void *data, size_t size, gboolean privileged); static int32_t cib_ipc_accept(qb_ipcs_connection_t * c, uid_t uid, gid_t gid) { if (cib_shutdown_flag) { crm_info("Ignoring new IPC client [%d] during shutdown", pcmk__client_pid(c)); return -ECONNREFUSED; } if (pcmk__new_client(c, uid, gid) == NULL) { return -ENOMEM; } return 0; } static int32_t cib_ipc_dispatch_rw(qb_ipcs_connection_t * c, void *data, size_t size) { pcmk__client_t *client = pcmk__find_client(c); crm_trace("%p message from %s", c, client->id); return cib_common_callback(c, data, size, TRUE); } static int32_t cib_ipc_dispatch_ro(qb_ipcs_connection_t * c, void *data, size_t size) { pcmk__client_t *client = pcmk__find_client(c); crm_trace("%p message from %s", c, client->id); return cib_common_callback(c, data, size, FALSE); } /* Error code means? */ static int32_t cib_ipc_closed(qb_ipcs_connection_t * c) { pcmk__client_t *client = pcmk__find_client(c); if (client == NULL) { return 0; } crm_trace("Connection %p", c); pcmk__free_client(client); return 0; } static void cib_ipc_destroy(qb_ipcs_connection_t * c) { crm_trace("Connection %p", c); cib_ipc_closed(c); if (cib_shutdown_flag) { cib_shutdown(0); } } struct qb_ipcs_service_handlers ipc_ro_callbacks = { .connection_accept = cib_ipc_accept, .connection_created = NULL, .msg_process = cib_ipc_dispatch_ro, .connection_closed = cib_ipc_closed, .connection_destroyed = cib_ipc_destroy }; struct qb_ipcs_service_handlers ipc_rw_callbacks = { .connection_accept = cib_ipc_accept, .connection_created = NULL, .msg_process = cib_ipc_dispatch_rw, .connection_closed = cib_ipc_closed, .connection_destroyed = cib_ipc_destroy }; /*! * \internal * \brief Create reply XML for a CIB request * * \param[in] op CIB operation type * \param[in] call_id CIB call ID * \param[in] client_id CIB client ID * \param[in] call_options Group of enum cib_call_options flags * \param[in] rc Request return code * \param[in] call_data Request output data * * \return Reply XML (guaranteed not to be \c NULL) * * \note The caller is responsible for freeing the return value using * \p pcmk__xml_free(). */ static xmlNode * create_cib_reply(const char *op, const char *call_id, const char *client_id, uint32_t call_options, int rc, xmlNode *call_data) { xmlNode *reply = pcmk__xe_create(NULL, PCMK__XE_CIB_REPLY); crm_xml_add(reply, PCMK__XA_T, PCMK__VALUE_CIB); crm_xml_add(reply, PCMK__XA_CIB_OP, op); crm_xml_add(reply, PCMK__XA_CIB_CALLID, call_id); crm_xml_add(reply, PCMK__XA_CIB_CLIENTID, client_id); crm_xml_add_int(reply, PCMK__XA_CIB_CALLOPT, call_options); crm_xml_add_int(reply, PCMK__XA_CIB_RC, rc); if (call_data != NULL) { xmlNode *wrapper = pcmk__xe_create(reply, PCMK__XE_CIB_CALLDATA); crm_trace("Attaching reply output"); pcmk__xml_copy(wrapper, call_data); } crm_log_xml_explicit(reply, "cib:reply"); return reply; } static void do_local_notify(const xmlNode *notify_src, const char *client_id, bool sync_reply, bool from_peer) { int msg_id = 0; int rc = pcmk_rc_ok; pcmk__client_t *client_obj = NULL; uint32_t flags = crm_ipc_server_event; CRM_CHECK((notify_src != NULL) && (client_id != NULL), return); crm_element_value_int(notify_src, PCMK__XA_CIB_CALLID, &msg_id); client_obj = pcmk__find_client_by_id(client_id); if (client_obj == NULL) { crm_debug("Could not notify client %s%s %s of call %d result: " "client no longer exists", client_id, (from_peer? " (originator of delegated request)" : ""), (sync_reply? "synchronously" : "asynchronously"), msg_id); return; } if (sync_reply) { flags = crm_ipc_flags_none; if (client_obj->ipcs != NULL) { msg_id = client_obj->request_id; client_obj->request_id = 0; } } switch (PCMK__CLIENT_TYPE(client_obj)) { case pcmk__client_ipc: rc = pcmk__ipc_send_xml(client_obj, msg_id, notify_src, flags); break; case pcmk__client_tls: case pcmk__client_tcp: rc = pcmk__remote_send_xml(client_obj->remote, notify_src); break; default: rc = EPROTONOSUPPORT; break; } if (rc == pcmk_rc_ok) { crm_trace("Notified %s client %s%s %s of call %d result", pcmk__client_type_str(PCMK__CLIENT_TYPE(client_obj)), pcmk__client_name(client_obj), (from_peer? " (originator of delegated request)" : ""), (sync_reply? "synchronously" : "asynchronously"), msg_id); } else { crm_warn("Could not notify %s client %s%s %s of call %d result: %s", pcmk__client_type_str(PCMK__CLIENT_TYPE(client_obj)), pcmk__client_name(client_obj), (from_peer? " (originator of delegated request)" : ""), (sync_reply? "synchronously" : "asynchronously"), msg_id, pcmk_rc_str(rc)); } } void cib_common_callback_worker(uint32_t id, uint32_t flags, xmlNode * op_request, pcmk__client_t *cib_client, gboolean privileged) { const char *op = crm_element_value(op_request, PCMK__XA_CIB_OP); uint32_t call_options = cib_none; int rc = pcmk_rc_ok; rc = pcmk__xe_get_flags(op_request, PCMK__XA_CIB_CALLOPT, &call_options, cib_none); if (rc != pcmk_rc_ok) { crm_warn("Couldn't parse options from request: %s", pcmk_rc_str(rc)); } /* Requests with cib_transaction set should not be sent to based directly * (outside of a commit-transaction request) */ if (pcmk_is_set(call_options, cib_transaction)) { return; } if (pcmk__str_eq(op, CRM_OP_REGISTER, pcmk__str_none)) { if (flags & crm_ipc_client_response) { xmlNode *ack = pcmk__xe_create(NULL, __func__); crm_xml_add(ack, PCMK__XA_CIB_OP, CRM_OP_REGISTER); crm_xml_add(ack, PCMK__XA_CIB_CLIENTID, cib_client->id); pcmk__ipc_send_xml(cib_client, id, ack, flags); cib_client->request_id = 0; pcmk__xml_free(ack); } return; } else if (pcmk__str_eq(op, PCMK__VALUE_CIB_NOTIFY, pcmk__str_none)) { /* Update the notify filters for this client */ int on_off = 0; crm_exit_t status = CRM_EX_OK; uint64_t bit = UINT64_C(0); const char *type = crm_element_value(op_request, PCMK__XA_CIB_NOTIFY_TYPE); crm_element_value_int(op_request, PCMK__XA_CIB_NOTIFY_ACTIVATE, &on_off); crm_debug("Setting %s callbacks %s for client %s", type, (on_off? "on" : "off"), pcmk__client_name(cib_client)); if (pcmk__str_eq(type, PCMK__VALUE_CIB_POST_NOTIFY, pcmk__str_none)) { bit = cib_notify_post; } else if (pcmk__str_eq(type, PCMK__VALUE_CIB_PRE_NOTIFY, pcmk__str_none)) { bit = cib_notify_pre; } else if (pcmk__str_eq(type, PCMK__VALUE_CIB_UPDATE_CONFIRMATION, pcmk__str_none)) { bit = cib_notify_confirm; } else if (pcmk__str_eq(type, PCMK__VALUE_CIB_DIFF_NOTIFY, pcmk__str_none)) { bit = cib_notify_diff; } else { status = CRM_EX_INVALID_PARAM; } if (bit != 0) { if (on_off) { pcmk__set_client_flags(cib_client, bit); } else { pcmk__clear_client_flags(cib_client, bit); } } pcmk__ipc_send_ack(cib_client, id, flags, PCMK__XE_ACK, NULL, status); return; } cib_process_request(op_request, privileged, cib_client); } int32_t cib_common_callback(qb_ipcs_connection_t * c, void *data, size_t size, gboolean privileged) { int rc = pcmk_rc_ok; uint32_t id = 0; uint32_t flags = 0; uint32_t call_options = cib_none; pcmk__client_t *cib_client = pcmk__find_client(c); xmlNode *op_request = NULL; if (cib_client == NULL) { crm_trace("Invalid client %p", c); return 0; } rc = pcmk__ipc_msg_append(&cib_client->buffer, data); if (rc == pcmk_rc_ipc_more) { /* We haven't read the complete message yet, so just return. */ return 0; } else if (rc == pcmk_rc_ok) { /* We've read the complete message and there's already a header on * the front. Pass it off for processing. */ op_request = pcmk__client_data2xml(cib_client, &id, &flags); g_byte_array_free(cib_client->buffer, TRUE); cib_client->buffer = NULL; } else { /* Some sort of error occurred reassembling the message. All we can * do is clean up, log an error and return. */ crm_err("Error when reading IPC message: %s", pcmk_rc_str(rc)); if (cib_client->buffer != NULL) { g_byte_array_free(cib_client->buffer, TRUE); cib_client->buffer = NULL; } return 0; } if (op_request) { int rc = pcmk_rc_ok; rc = pcmk__xe_get_flags(op_request, PCMK__XA_CIB_CALLOPT, &call_options, cib_none); if (rc != pcmk_rc_ok) { crm_warn("Couldn't parse options from request: %s", pcmk_rc_str(rc)); } } if (op_request == NULL) { crm_trace("Invalid message from %p", c); pcmk__ipc_send_ack(cib_client, id, flags, PCMK__XE_NACK, NULL, CRM_EX_PROTOCOL); return 0; } if (pcmk_is_set(call_options, cib_sync_call)) { CRM_LOG_ASSERT(flags & crm_ipc_client_response); CRM_LOG_ASSERT(cib_client->request_id == 0); /* This means the client has two synchronous events in-flight */ cib_client->request_id = id; /* Reply only to the last one */ } if (cib_client->name == NULL) { const char *value = crm_element_value(op_request, PCMK__XA_CIB_CLIENTNAME); if (value == NULL) { cib_client->name = pcmk__itoa(cib_client->pid); } else { cib_client->name = pcmk__str_copy(value); if (pcmk__parse_server(value) != pcmk_ipc_unknown) { pcmk__set_client_flags(cib_client, cib_is_daemon); } } } /* Allow cluster daemons more leeway before being evicted */ if (pcmk_is_set(cib_client->flags, cib_is_daemon)) { const char *qmax = cib_config_lookup(PCMK_OPT_CLUSTER_IPC_LIMIT); pcmk__set_client_queue_max(cib_client, qmax); } crm_xml_add(op_request, PCMK__XA_CIB_CLIENTID, cib_client->id); crm_xml_add(op_request, PCMK__XA_CIB_CLIENTNAME, cib_client->name); CRM_LOG_ASSERT(cib_client->user != NULL); pcmk__update_acl_user(op_request, PCMK__XA_CIB_USER, cib_client->user); cib_common_callback_worker(id, flags, op_request, cib_client, privileged); pcmk__xml_free(op_request); return 0; } static uint64_t ping_seq = 0; static char *ping_digest = NULL; static bool ping_modified_since = FALSE; static gboolean cib_digester_cb(gpointer data) { if (based_is_primary) { char buffer[32]; xmlNode *ping = pcmk__xe_create(NULL, PCMK__XE_PING); ping_seq++; free(ping_digest); ping_digest = NULL; ping_modified_since = FALSE; - snprintf(buffer, 32, "%" PRIu64, ping_seq); + pcmk__assert(snprintf(buffer, 32, "%" PRIu64, ping_seq) >= 0); + crm_trace("Requesting peer digests (%s)", buffer); crm_xml_add(ping, PCMK__XA_T, PCMK__VALUE_CIB); crm_xml_add(ping, PCMK__XA_CIB_OP, CRM_OP_PING); crm_xml_add(ping, PCMK__XA_CIB_PING_ID, buffer); crm_xml_add(ping, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET); pcmk__cluster_send_message(NULL, pcmk_ipc_based, ping); pcmk__xml_free(ping); } return FALSE; } static void process_ping_reply(xmlNode *reply) { uint64_t seq = 0; const char *host = crm_element_value(reply, PCMK__XA_SRC); xmlNode *wrapper = pcmk__xe_first_child(reply, PCMK__XE_CIB_CALLDATA, NULL, NULL); xmlNode *pong = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); const char *seq_s = crm_element_value(pong, PCMK__XA_CIB_PING_ID); const char *digest = crm_element_value(pong, PCMK__XA_DIGEST); if (seq_s == NULL) { crm_debug("Ignoring ping reply with no " PCMK__XA_CIB_PING_ID); return; } else { long long seq_ll; int rc = pcmk__scan_ll(seq_s, &seq_ll, 0LL); if (rc != pcmk_rc_ok) { crm_debug("Ignoring ping reply with invalid " PCMK__XA_CIB_PING_ID " '%s': %s", seq_s, pcmk_rc_str(rc)); return; } seq = (uint64_t) seq_ll; } if(digest == NULL) { crm_trace("Ignoring ping reply %s from %s with no digest", seq_s, host); } else if(seq != ping_seq) { crm_trace("Ignoring out of sequence ping reply %s from %s", seq_s, host); } else if(ping_modified_since) { crm_trace("Ignoring ping reply %s from %s: cib updated since", seq_s, host); } else { if(ping_digest == NULL) { crm_trace("Calculating new digest"); ping_digest = pcmk__digest_xml(the_cib, true); } crm_trace("Processing ping reply %s from %s (%s)", seq_s, host, digest); if (!pcmk__str_eq(ping_digest, digest, pcmk__str_casei)) { xmlNode *wrapper = pcmk__xe_first_child(pong, PCMK__XE_CIB_CALLDATA, NULL, NULL); xmlNode *remote_cib = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); const char *admin_epoch_s = NULL; const char *epoch_s = NULL; const char *num_updates_s = NULL; if (remote_cib != NULL) { admin_epoch_s = crm_element_value(remote_cib, PCMK_XA_ADMIN_EPOCH); epoch_s = crm_element_value(remote_cib, PCMK_XA_EPOCH); num_updates_s = crm_element_value(remote_cib, PCMK_XA_NUM_UPDATES); } crm_notice("Local CIB %s.%s.%s.%s differs from %s: %s.%s.%s.%s %p", crm_element_value(the_cib, PCMK_XA_ADMIN_EPOCH), crm_element_value(the_cib, PCMK_XA_EPOCH), crm_element_value(the_cib, PCMK_XA_NUM_UPDATES), ping_digest, host, pcmk__s(admin_epoch_s, "_"), pcmk__s(epoch_s, "_"), pcmk__s(num_updates_s, "_"), digest, remote_cib); if(remote_cib && remote_cib->children) { // Additional debug pcmk__xml_mark_changes(the_cib, remote_cib); pcmk__log_xml_changes(LOG_INFO, remote_cib); crm_trace("End of differences"); } pcmk__xml_free(remote_cib); sync_our_cib(reply, FALSE); } } } static void parse_local_options(const pcmk__client_t *cib_client, const cib__operation_t *operation, const char *host, const char *op, gboolean *local_notify, gboolean *needs_reply, gboolean *process, gboolean *needs_forward) { // Process locally and notify local client *process = TRUE; *needs_reply = FALSE; *local_notify = TRUE; *needs_forward = FALSE; if (pcmk_is_set(operation->flags, cib__op_attr_local)) { /* Always process locally if cib__op_attr_local is set. * * @COMPAT: Currently host is ignored. At a compatibility break, throw * an error (from cib_process_request() or earlier) if host is not NULL or * OUR_NODENAME. */ crm_trace("Processing always-local %s op from client %s", op, pcmk__client_name(cib_client)); if (!pcmk__str_eq(host, OUR_NODENAME, pcmk__str_casei|pcmk__str_null_matches)) { crm_warn("Operation '%s' is always local but its target host is " "set to '%s'", op, host); } return; } if (pcmk_is_set(operation->flags, cib__op_attr_modifies) || !pcmk__str_eq(host, OUR_NODENAME, pcmk__str_casei|pcmk__str_null_matches)) { // Forward modifying and non-local requests via cluster *process = FALSE; *needs_reply = FALSE; *local_notify = FALSE; *needs_forward = TRUE; crm_trace("%s op from %s needs to be forwarded to %s", op, pcmk__client_name(cib_client), pcmk__s(host, "all nodes")); return; } if (stand_alone) { crm_trace("Processing %s op from client %s (stand-alone)", op, pcmk__client_name(cib_client)); } else { crm_trace("Processing %saddressed %s op from client %s", ((host != NULL)? "locally " : "un"), op, pcmk__client_name(cib_client)); } } static gboolean parse_peer_options(const cib__operation_t *operation, xmlNode *request, gboolean *local_notify, gboolean *needs_reply, gboolean *process) { /* TODO: What happens when an update comes in after node A * requests the CIB from node B, but before it gets the reply (and * sends out the replace operation)? * * (This may no longer be relevant since legacy mode was dropped; need to * trace code more closely to check.) */ const char *host = NULL; const char *delegated = crm_element_value(request, PCMK__XA_CIB_DELEGATED_FROM); const char *op = crm_element_value(request, PCMK__XA_CIB_OP); const char *originator = crm_element_value(request, PCMK__XA_SRC); const char *reply_to = crm_element_value(request, PCMK__XA_CIB_ISREPLYTO); gboolean is_reply = pcmk__str_eq(reply_to, OUR_NODENAME, pcmk__str_casei); if (originator == NULL) { // Shouldn't be possible originator = "peer"; } if (pcmk__str_eq(op, PCMK__CIB_REQUEST_REPLACE, pcmk__str_none)) { // sync_our_cib() sets PCMK__XA_CIB_ISREPLYTO if (reply_to) { delegated = reply_to; } goto skip_is_reply; } else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_SYNC_TO_ALL, pcmk__str_none)) { // Nothing to do } else if (is_reply && pcmk__str_eq(op, CRM_OP_PING, pcmk__str_casei)) { process_ping_reply(request); return FALSE; } else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_UPGRADE, pcmk__str_none)) { /* Only the DC (node with the oldest software) should process * this operation if PCMK__XA_CIB_SCHEMA_MAX is unset. * * If the DC is happy it will then send out another * PCMK__CIB_REQUEST_UPGRADE which will tell all nodes to do the actual * upgrade. * * Except this time PCMK__XA_CIB_SCHEMA_MAX will be set which puts a * limit on how far newer nodes will go */ const char *max = crm_element_value(request, PCMK__XA_CIB_SCHEMA_MAX); const char *upgrade_rc = crm_element_value(request, PCMK__XA_CIB_UPGRADE_RC); crm_trace("Parsing upgrade %s for %s with max=%s and upgrade_rc=%s", (is_reply? "reply" : "request"), (based_is_primary? "primary" : "secondary"), pcmk__s(max, "none"), pcmk__s(upgrade_rc, "none")); if (upgrade_rc != NULL) { // Our upgrade request was rejected by DC, notify clients of result crm_xml_add(request, PCMK__XA_CIB_RC, upgrade_rc); } else if ((max == NULL) && based_is_primary) { /* We are the DC, check if this upgrade is allowed */ goto skip_is_reply; } else if(max) { /* Ok, go ahead and upgrade to 'max' */ goto skip_is_reply; } else { // Ignore broadcast client requests when we're not primary return FALSE; } } else if (pcmk__xe_attr_is_true(request, PCMK__XA_CIB_UPDATE)) { crm_info("Detected legacy %s global update from %s", op, originator); send_sync_request(NULL); return FALSE; } else if (is_reply && pcmk_is_set(operation->flags, cib__op_attr_modifies)) { crm_trace("Ignoring legacy %s reply sent from %s to local clients", op, originator); return FALSE; } else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_SHUTDOWN, pcmk__str_none)) { *local_notify = FALSE; if (reply_to == NULL) { *process = TRUE; } else { // Not possible? crm_debug("Ignoring shutdown request from %s because reply_to=%s", originator, reply_to); } return *process; } if (is_reply) { crm_trace("Will notify local clients for %s reply from %s", op, originator); *process = FALSE; *needs_reply = FALSE; *local_notify = TRUE; return TRUE; } skip_is_reply: *process = TRUE; *needs_reply = FALSE; *local_notify = pcmk__str_eq(delegated, OUR_NODENAME, pcmk__str_casei); host = crm_element_value(request, PCMK__XA_CIB_HOST); if (pcmk__str_eq(host, OUR_NODENAME, pcmk__str_casei)) { crm_trace("Processing %s request sent to us from %s", op, originator); *needs_reply = TRUE; return TRUE; } else if (host != NULL) { crm_trace("Ignoring %s request intended for CIB manager on %s", op, host); return FALSE; } else if(is_reply == FALSE && pcmk__str_eq(op, CRM_OP_PING, pcmk__str_casei)) { *needs_reply = TRUE; } crm_trace("Processing %s request broadcast by %s call %s on %s " "(local clients will%s be notified)", op, pcmk__s(crm_element_value(request, PCMK__XA_CIB_CLIENTNAME), "client"), pcmk__s(crm_element_value(request, PCMK__XA_CIB_CALLID), "without ID"), originator, (*local_notify? "" : "not")); return TRUE; } /*! * \internal * \brief Forward a CIB request to the appropriate target host(s) * * \param[in] request CIB request to forward */ static void forward_request(xmlNode *request) { const char *op = crm_element_value(request, PCMK__XA_CIB_OP); const char *section = crm_element_value(request, PCMK__XA_CIB_SECTION); const char *host = crm_element_value(request, PCMK__XA_CIB_HOST); const char *originator = crm_element_value(request, PCMK__XA_SRC); const char *client_name = crm_element_value(request, PCMK__XA_CIB_CLIENTNAME); const char *call_id = crm_element_value(request, PCMK__XA_CIB_CALLID); pcmk__node_status_t *peer = NULL; int log_level = LOG_INFO; if (pcmk__str_eq(op, PCMK__CIB_REQUEST_NOOP, pcmk__str_none)) { log_level = LOG_DEBUG; } do_crm_log(log_level, "Forwarding %s operation for section %s to %s (origin=%s/%s/%s)", pcmk__s(op, "invalid"), pcmk__s(section, "all"), pcmk__s(host, "all"), pcmk__s(originator, "local"), pcmk__s(client_name, "unspecified"), pcmk__s(call_id, "unspecified")); crm_xml_add(request, PCMK__XA_CIB_DELEGATED_FROM, OUR_NODENAME); if (host != NULL) { peer = pcmk__get_node(0, host, NULL, pcmk__node_search_cluster_member); } pcmk__cluster_send_message(peer, pcmk_ipc_based, request); // Return the request to its original state pcmk__xe_remove_attr(request, PCMK__XA_CIB_DELEGATED_FROM); } static void send_peer_reply(xmlNode *msg, const char *originator) { const pcmk__node_status_t *node = NULL; if ((msg == NULL) || (originator == NULL)) { return; } // Send reply via cluster to originating node node = pcmk__get_node(0, originator, NULL, pcmk__node_search_cluster_member); crm_trace("Sending request result to %s only", originator); crm_xml_add(msg, PCMK__XA_CIB_ISREPLYTO, originator); pcmk__cluster_send_message(node, pcmk_ipc_based, msg); } /*! * \internal * \brief Handle an IPC or CPG message containing a request * * \param[in,out] request Request XML * \param[in] privileged Whether privileged commands may be run * (see cib_server_ops[] definition) * \param[in] cib_client IPC client that sent request (or NULL if CPG) * * \return Legacy Pacemaker return code */ int cib_process_request(xmlNode *request, gboolean privileged, const pcmk__client_t *cib_client) { // @TODO: Break into multiple smaller functions uint32_t call_options = cib_none; gboolean process = TRUE; // Whether to process request locally now gboolean is_update = TRUE; // Whether request would modify CIB gboolean needs_reply = TRUE; // Whether to build a reply gboolean local_notify = FALSE; // Whether to notify (local) requester gboolean needs_forward = FALSE; // Whether to forward request somewhere else xmlNode *op_reply = NULL; xmlNode *result_diff = NULL; int rc = pcmk_ok; const char *op = crm_element_value(request, PCMK__XA_CIB_OP); const char *originator = crm_element_value(request, PCMK__XA_SRC); const char *host = crm_element_value(request, PCMK__XA_CIB_HOST); const char *call_id = crm_element_value(request, PCMK__XA_CIB_CALLID); const char *client_id = crm_element_value(request, PCMK__XA_CIB_CLIENTID); const char *client_name = crm_element_value(request, PCMK__XA_CIB_CLIENTNAME); const char *reply_to = crm_element_value(request, PCMK__XA_CIB_ISREPLYTO); const cib__operation_t *operation = NULL; cib__op_fn_t op_function = NULL; rc = pcmk__xe_get_flags(request, PCMK__XA_CIB_CALLOPT, &call_options, cib_none); if (rc != pcmk_rc_ok) { crm_warn("Couldn't parse options from request: %s", pcmk_rc_str(rc)); } if ((host != NULL) && (*host == '\0')) { host = NULL; } if (cib_client == NULL) { crm_trace("Processing peer %s operation from %s/%s on %s intended for %s (reply=%s)", op, pcmk__s(client_name, "client"), call_id, originator, pcmk__s(host, "all"), reply_to); } else { crm_xml_add(request, PCMK__XA_SRC, OUR_NODENAME); crm_trace("Processing local %s operation from %s/%s intended for %s", op, pcmk__s(client_name, "client"), call_id, pcmk__s(host, "all")); } rc = cib__get_operation(op, &operation); rc = pcmk_rc2legacy(rc); if (rc != pcmk_ok) { /* TODO: construct error reply? */ crm_err("Pre-processing of command failed: %s", pcmk_strerror(rc)); return rc; } op_function = based_get_op_function(operation); if (op_function == NULL) { crm_err("Operation %s not supported by CIB manager", op); return -EOPNOTSUPP; } if (cib_client != NULL) { parse_local_options(cib_client, operation, host, op, &local_notify, &needs_reply, &process, &needs_forward); } else if (!parse_peer_options(operation, request, &local_notify, &needs_reply, &process)) { return rc; } if (pcmk_is_set(call_options, cib_transaction)) { /* All requests in a transaction are processed locally against a working * CIB copy, and we don't notify for individual requests because the * entire transaction is atomic. * * We still call the option parser functions above, for the sake of log * messages and checking whether we're the target for peer requests. */ process = TRUE; needs_reply = FALSE; local_notify = FALSE; needs_forward = FALSE; } is_update = pcmk_is_set(operation->flags, cib__op_attr_modifies); if (pcmk_is_set(call_options, cib_discard_reply)) { /* If the request will modify the CIB, and we are in legacy mode, we * need to build a reply so we can broadcast a diff, even if the * requester doesn't want one. */ needs_reply = FALSE; local_notify = FALSE; crm_trace("Client is not interested in the reply"); } if (needs_forward) { forward_request(request); return rc; } if (cib_status != pcmk_ok) { rc = cib_status; crm_err("Ignoring request because cluster configuration is invalid " "(please repair and restart): %s", pcmk_strerror(rc)); op_reply = create_cib_reply(op, call_id, client_id, call_options, rc, the_cib); } else if (process) { time_t finished = 0; time_t now = time(NULL); int level = LOG_INFO; const char *section = crm_element_value(request, PCMK__XA_CIB_SECTION); const char *admin_epoch_s = NULL; const char *epoch_s = NULL; const char *num_updates_s = NULL; rc = cib_process_command(request, operation, op_function, &op_reply, &result_diff, privileged); if (!is_update) { level = LOG_TRACE; } else if (pcmk__xe_attr_is_true(request, PCMK__XA_CIB_UPDATE)) { switch (rc) { case pcmk_ok: level = LOG_INFO; break; case -pcmk_err_old_data: case -pcmk_err_diff_resync: case -pcmk_err_diff_failed: level = LOG_TRACE; break; default: level = LOG_ERR; } } else if (rc != pcmk_ok) { level = LOG_WARNING; } if (the_cib != NULL) { admin_epoch_s = crm_element_value(the_cib, PCMK_XA_ADMIN_EPOCH); epoch_s = crm_element_value(the_cib, PCMK_XA_EPOCH); num_updates_s = crm_element_value(the_cib, PCMK_XA_NUM_UPDATES); } do_crm_log(level, "Completed %s operation for section %s: %s (rc=%d, origin=%s/%s/%s, version=%s.%s.%s)", op, section ? section : "'all'", pcmk_strerror(rc), rc, originator ? originator : "local", pcmk__s(client_name, "client"), call_id, pcmk__s(admin_epoch_s, "0"), pcmk__s(epoch_s, "0"), pcmk__s(num_updates_s, "0")); finished = time(NULL); if ((finished - now) > 3) { crm_trace("%s operation took %lds to complete", op, (long)(finished - now)); crm_write_blackbox(0, NULL); } if (op_reply == NULL && (needs_reply || local_notify)) { crm_err("Unexpected NULL reply to message"); crm_log_xml_err(request, "null reply"); needs_reply = FALSE; local_notify = FALSE; } } if (is_update) { crm_trace("Completed pre-sync update from %s/%s/%s%s", originator ? originator : "local", pcmk__s(client_name, "client"), call_id, local_notify?" with local notification":""); } else if (!needs_reply || stand_alone) { // This was a non-originating secondary update crm_trace("Completed update as secondary"); } else if ((cib_client == NULL) && !pcmk_is_set(call_options, cib_discard_reply)) { if (is_update == FALSE || result_diff == NULL) { crm_trace("Request not broadcast: R/O call"); } else if (rc != pcmk_ok) { crm_trace("Request not broadcast: call failed: %s", pcmk_strerror(rc)); } else { crm_trace("Directing reply to %s", originator); } send_peer_reply(op_reply, originator); } if (local_notify && client_id) { crm_trace("Performing local %ssync notification for %s", (pcmk_is_set(call_options, cib_sync_call)? "" : "a"), client_id); if (process == FALSE) { do_local_notify(request, client_id, pcmk_is_set(call_options, cib_sync_call), (cib_client == NULL)); } else { do_local_notify(op_reply, client_id, pcmk_is_set(call_options, cib_sync_call), (cib_client == NULL)); } } pcmk__xml_free(op_reply); pcmk__xml_free(result_diff); return rc; } /*! * \internal * \brief Get a CIB operation's input from the request XML * * \param[in] request CIB request XML * \param[in] type CIB operation type * \param[out] section Where to store CIB section name * * \return Input XML for CIB operation * * \note If not \c NULL, the return value is a non-const pointer to part of * \p request. The caller should not free it directly. */ static xmlNode * prepare_input(const xmlNode *request, enum cib__op_type type, const char **section) { xmlNode *wrapper = pcmk__xe_first_child(request, PCMK__XE_CIB_CALLDATA, NULL, NULL); xmlNode *input = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); if (type == cib__op_apply_patch) { *section = NULL; } else { *section = crm_element_value(request, PCMK__XA_CIB_SECTION); } // Grab the specified section if ((*section != NULL) && pcmk__xe_is(input, PCMK_XE_CIB)) { input = pcmk_find_cib_element(input, *section); } return input; } #define XPATH_CONFIG_CHANGE \ "//" PCMK_XE_CHANGE \ "[contains(@" PCMK_XA_PATH ",'/" PCMK_XE_CRM_CONFIG "/')]" static bool contains_config_change(xmlNode *diff) { bool changed = false; if (diff) { xmlXPathObject *xpathObj = pcmk__xpath_search(diff->doc, XPATH_CONFIG_CHANGE); if (pcmk__xpath_num_results(xpathObj) > 0) { changed = true; } xmlXPathFreeObject(xpathObj); } return changed; } static int cib_process_command(xmlNode *request, const cib__operation_t *operation, cib__op_fn_t op_function, xmlNode **reply, xmlNode **cib_diff, bool privileged) { xmlNode *input = NULL; xmlNode *output = NULL; xmlNode *result_cib = NULL; uint32_t call_options = cib_none; const char *op = NULL; const char *section = NULL; const char *call_id = crm_element_value(request, PCMK__XA_CIB_CALLID); const char *client_id = crm_element_value(request, PCMK__XA_CIB_CLIENTID); const char *client_name = crm_element_value(request, PCMK__XA_CIB_CLIENTNAME); const char *originator = crm_element_value(request, PCMK__XA_SRC); int rc = pcmk_ok; bool config_changed = false; bool manage_counters = true; static mainloop_timer_t *digest_timer = NULL; pcmk__assert(cib_status == pcmk_ok); if(digest_timer == NULL) { digest_timer = mainloop_timer_add("digester", 5000, FALSE, cib_digester_cb, NULL); } *reply = NULL; *cib_diff = NULL; /* Start processing the request... */ op = crm_element_value(request, PCMK__XA_CIB_OP); rc = pcmk__xe_get_flags(request, PCMK__XA_CIB_CALLOPT, &call_options, cib_none); if (rc != pcmk_rc_ok) { crm_warn("Couldn't parse options from request: %s", pcmk_rc_str(rc)); } if (!privileged && pcmk_is_set(operation->flags, cib__op_attr_privileged)) { rc = -EACCES; crm_trace("Failed due to lack of privileges: %s", pcmk_strerror(rc)); goto done; } input = prepare_input(request, operation->type, §ion); if (!pcmk_is_set(operation->flags, cib__op_attr_modifies)) { rc = cib_perform_op(NULL, op, call_options, op_function, true, section, request, input, false, &config_changed, &the_cib, &result_cib, NULL, &output); CRM_CHECK(result_cib == NULL, pcmk__xml_free(result_cib)); goto done; } /* @COMPAT: Handle a valid write action (legacy) * * @TODO: Re-evaluate whether this is all truly legacy. The cib_force_diff * portion is. However, PCMK__XA_CIB_UPDATE may be set by a sync operation * even in non-legacy mode, and manage_counters tells xml_create_patchset() * whether to update version/epoch info. */ if (pcmk__xe_attr_is_true(request, PCMK__XA_CIB_UPDATE)) { manage_counters = false; cib__set_call_options(call_options, "call", cib_force_diff); crm_trace("Global update detected"); CRM_LOG_ASSERT(pcmk__str_any_of(op, PCMK__CIB_REQUEST_APPLY_PATCH, PCMK__CIB_REQUEST_REPLACE, NULL)); } ping_modified_since = TRUE; // result_cib must not be modified after cib_perform_op() returns rc = cib_perform_op(NULL, op, call_options, op_function, false, section, request, input, manage_counters, &config_changed, &the_cib, &result_cib, cib_diff, &output); /* Always write to disk for successful ops with the flag set. This also * negates the need to detect ordering changes. */ if ((rc == pcmk_ok) && pcmk_is_set(operation->flags, cib__op_attr_writes_through)) { config_changed = true; } if ((rc == pcmk_ok) && !pcmk_any_flags_set(call_options, cib_dryrun|cib_transaction)) { if (result_cib != the_cib) { if (pcmk_is_set(operation->flags, cib__op_attr_writes_through)) { config_changed = true; } crm_trace("Activating %s->%s%s", crm_element_value(the_cib, PCMK_XA_NUM_UPDATES), crm_element_value(result_cib, PCMK_XA_NUM_UPDATES), (config_changed? " changed" : "")); rc = activateCibXml(result_cib, config_changed, op); if (rc != pcmk_ok) { crm_err("Failed to activate new CIB: %s", pcmk_strerror(rc)); } } if ((rc == pcmk_ok) && contains_config_change(*cib_diff)) { cib_read_config(config_hash, result_cib); } /* @COMPAT Nodes older than feature set 3.19.0 don't support * transactions. In a mixed-version cluster with nodes <3.19.0, we must * sync the updated CIB, so that the older nodes receive the changes. * Any node that has already applied the transaction will ignore the * synced CIB. * * To ensure the updated CIB is synced from only one node, we sync it * from the originator. */ if ((operation->type == cib__op_commit_transact) && pcmk__str_eq(originator, OUR_NODENAME, pcmk__str_casei) && compare_version(crm_element_value(the_cib, PCMK_XA_CRM_FEATURE_SET), "3.19.0") < 0) { sync_our_cib(request, TRUE); } mainloop_timer_stop(digest_timer); mainloop_timer_start(digest_timer); } else if (rc == -pcmk_err_schema_validation) { pcmk__assert(result_cib != the_cib); if (output != NULL) { crm_log_xml_info(output, "cib:output"); pcmk__xml_free(output); } output = result_cib; } else { crm_trace("Not activating %d %d %s", rc, pcmk_is_set(call_options, cib_dryrun), crm_element_value(result_cib, PCMK_XA_NUM_UPDATES)); if (result_cib != the_cib) { pcmk__xml_free(result_cib); } } if (!pcmk_any_flags_set(call_options, cib_dryrun|cib_inhibit_notify|cib_transaction)) { crm_trace("Sending notifications %d", pcmk_is_set(call_options, cib_dryrun)); cib_diff_notify(op, rc, call_id, client_id, client_name, originator, input, *cib_diff); } pcmk__log_xml_patchset(LOG_TRACE, *cib_diff); done: if (!pcmk_is_set(call_options, cib_discard_reply)) { *reply = create_cib_reply(op, call_id, client_id, call_options, rc, output); } if (output != the_cib) { pcmk__xml_free(output); } crm_trace("done"); return rc; } void cib_peer_callback(xmlNode * msg, void *private_data) { const char *reason = NULL; const char *originator = crm_element_value(msg, PCMK__XA_SRC); if (pcmk__peer_cache == NULL) { reason = "membership not established"; goto bail; } if (crm_element_value(msg, PCMK__XA_CIB_CLIENTNAME) == NULL) { crm_xml_add(msg, PCMK__XA_CIB_CLIENTNAME, originator); } /* crm_log_xml_trace(msg, "Peer[inbound]"); */ cib_process_request(msg, TRUE, NULL); return; bail: if (reason) { const char *op = crm_element_value(msg, PCMK__XA_CIB_OP); crm_warn("Discarding %s message from %s: %s", op, originator, reason); } } static gboolean cib_force_exit(gpointer data) { crm_notice("Exiting immediately after %s without shutdown acknowledgment", pcmk__readable_interval(EXIT_ESCALATION_MS)); terminate_cib(CRM_EX_ERROR); return FALSE; } static void disconnect_remote_client(gpointer key, gpointer value, gpointer user_data) { pcmk__client_t *a_client = value; crm_err("Can't disconnect client %s: Not implemented", pcmk__client_name(a_client)); } static void initiate_exit(void) { int active = 0; xmlNode *leaving = NULL; active = pcmk__cluster_num_active_nodes(); if (active < 2) { // This is the last active node crm_info("Exiting without sending shutdown request (no active peers)"); terminate_cib(CRM_EX_OK); return; } crm_info("Sending shutdown request to %d peers", active); leaving = pcmk__xe_create(NULL, PCMK__XE_EXIT_NOTIFICATION); crm_xml_add(leaving, PCMK__XA_T, PCMK__VALUE_CIB); crm_xml_add(leaving, PCMK__XA_CIB_OP, PCMK__CIB_REQUEST_SHUTDOWN); pcmk__cluster_send_message(NULL, pcmk_ipc_based, leaving); pcmk__xml_free(leaving); pcmk__create_timer(EXIT_ESCALATION_MS, cib_force_exit, NULL); } void cib_shutdown(int nsig) { struct qb_ipcs_stats srv_stats; if (cib_shutdown_flag == FALSE) { int disconnects = 0; qb_ipcs_connection_t *c = NULL; cib_shutdown_flag = TRUE; c = qb_ipcs_connection_first_get(ipcs_rw); while (c != NULL) { qb_ipcs_connection_t *last = c; c = qb_ipcs_connection_next_get(ipcs_rw, last); crm_debug("Disconnecting r/w client %p...", last); qb_ipcs_disconnect(last); qb_ipcs_connection_unref(last); disconnects++; } c = qb_ipcs_connection_first_get(ipcs_ro); while (c != NULL) { qb_ipcs_connection_t *last = c; c = qb_ipcs_connection_next_get(ipcs_ro, last); crm_debug("Disconnecting r/o client %p...", last); qb_ipcs_disconnect(last); qb_ipcs_connection_unref(last); disconnects++; } c = qb_ipcs_connection_first_get(ipcs_shm); while (c != NULL) { qb_ipcs_connection_t *last = c; c = qb_ipcs_connection_next_get(ipcs_shm, last); crm_debug("Disconnecting non-blocking r/w client %p...", last); qb_ipcs_disconnect(last); qb_ipcs_connection_unref(last); disconnects++; } disconnects += pcmk__ipc_client_count(); crm_debug("Disconnecting %d remote clients", pcmk__ipc_client_count()); pcmk__foreach_ipc_client(disconnect_remote_client, NULL); crm_info("Disconnected %d clients", disconnects); } qb_ipcs_stats_get(ipcs_rw, &srv_stats, QB_FALSE); if (pcmk__ipc_client_count() == 0) { crm_info("All clients disconnected (%d)", srv_stats.active_connections); initiate_exit(); } else { crm_info("Waiting on %d clients to disconnect (%d)", pcmk__ipc_client_count(), srv_stats.active_connections); } } extern int remote_fd; extern int remote_tls_fd; /*! * \internal * \brief Close remote sockets, free the global CIB and quit * * \param[in] exit_status What exit status to use (if -1, use CRM_EX_OK, but * skip disconnecting from the cluster layer) */ void terminate_cib(int exit_status) { if (remote_fd > 0) { close(remote_fd); remote_fd = 0; } if (remote_tls_fd > 0) { close(remote_tls_fd); remote_tls_fd = 0; } uninitializeCib(); // Exit immediately on error if (exit_status > CRM_EX_OK) { pcmk__stop_based_ipc(ipcs_ro, ipcs_rw, ipcs_shm); crm_exit(exit_status); return; } if ((mainloop != NULL) && g_main_loop_is_running(mainloop)) { /* Quit via returning from the main loop. If exit_status has the special * value -1, we skip the disconnect here, and it will be done when the * main loop returns (this allows the peer status callback to avoid * messing with the peer caches). */ if (exit_status == CRM_EX_OK) { pcmk_cluster_disconnect(crm_cluster); } g_main_loop_quit(mainloop); return; } /* Exit cleanly. Even the peer status callback can disconnect here, because * we're not returning control to the caller. */ pcmk_cluster_disconnect(crm_cluster); pcmk__stop_based_ipc(ipcs_ro, ipcs_rw, ipcs_shm); crm_exit(CRM_EX_OK); } diff --git a/daemons/execd/cts-exec-helper.c b/daemons/execd/cts-exec-helper.c index 47ab5bbe38..9a03287136 100644 --- a/daemons/execd/cts-exec-helper.c +++ b/daemons/execd/cts-exec-helper.c @@ -1,626 +1,623 @@ /* - * Copyright 2012-2024 the Pacemaker project contributors + * Copyright 2012-2025 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 #include #include #include #include #include #include #include #include #include #include #include #define SUMMARY "cts-exec-helper - inject commands into the Pacemaker executor and watch for events" static int exec_call_id = 0; static gboolean start_test(gpointer user_data); static void try_connect(void); static char *key = NULL; static char *val = NULL; static struct { int verbose; int quiet; guint interval_ms; int timeout; int start_delay; int cancel_call_id; gboolean no_wait; gboolean is_running; gboolean no_connect; int exec_call_opts; const char *api_call; const char *rsc_id; const char *provider; const char *class; const char *type; const char *action; const char *listen; gboolean use_tls; lrmd_key_value_t *params; } options; static gboolean interval_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { return pcmk_parse_interval_spec(optarg, &options.interval_ms) == pcmk_rc_ok; } static gboolean notify_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "--notify-orig", "-n", NULL)) { options.exec_call_opts = lrmd_opt_notify_orig_only; } else if (pcmk__str_any_of(option_name, "--notify-changes", "-o", NULL)) { options.exec_call_opts = lrmd_opt_notify_changes_only; } return TRUE; } static gboolean param_key_val_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "--param-key", "-k", NULL)) { pcmk__str_update(&key, optarg); } else if (pcmk__str_any_of(option_name, "--param-val", "-v", NULL)) { pcmk__str_update(&val, optarg); } if (key != NULL && val != NULL) { options.params = lrmd_key_value_add(options.params, key, val); pcmk__str_update(&key, NULL); pcmk__str_update(&val, NULL); } return TRUE; } static GOptionEntry basic_entries[] = { { "api-call", 'c', 0, G_OPTION_ARG_STRING, &options.api_call, "Directly relates to executor API functions", NULL }, { "is-running", 'R', 0, G_OPTION_ARG_NONE, &options.is_running, "Determine if a resource is registered and running", NULL }, { "listen", 'l', 0, G_OPTION_ARG_STRING, &options.listen, "Listen for a specific event string", NULL }, { "no-wait", 'w', 0, G_OPTION_ARG_NONE, &options.no_wait, "Make api call and do not wait for result", NULL }, { "notify-changes", 'o', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, notify_cb, "Only notify client changes to recurring operations", NULL }, { "notify-orig", 'n', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, notify_cb, "Only notify this client of the results of an API action", NULL }, { "tls", 'S', 0, G_OPTION_ARG_NONE, &options.use_tls, "Use TLS backend for local connection", NULL }, { NULL } }; static GOptionEntry api_call_entries[] = { { "action", 'a', 0, G_OPTION_ARG_STRING, &options.action, NULL, NULL }, { "cancel-call-id", 'x', 0, G_OPTION_ARG_INT, &options.cancel_call_id, NULL, NULL }, { "class", 'C', 0, G_OPTION_ARG_STRING, &options.class, NULL, NULL }, { "interval", 'i', 0, G_OPTION_ARG_CALLBACK, interval_cb, NULL, NULL }, { "param-key", 'k', 0, G_OPTION_ARG_CALLBACK, param_key_val_cb, NULL, NULL }, { "param-val", 'v', 0, G_OPTION_ARG_CALLBACK, param_key_val_cb, NULL, NULL }, { "provider", 'P', 0, G_OPTION_ARG_STRING, &options.provider, NULL, NULL }, { "rsc-id", 'r', 0, G_OPTION_ARG_STRING, &options.rsc_id, NULL, NULL }, { "start-delay", 's', 0, G_OPTION_ARG_INT, &options.start_delay, NULL, NULL }, { "timeout", 't', 0, G_OPTION_ARG_INT, &options.timeout, NULL, NULL }, { "type", 'T', 0, G_OPTION_ARG_STRING, &options.type, NULL, NULL }, { NULL } }; static GMainLoop *mainloop = NULL; static lrmd_t *lrmd_conn = NULL; -static char event_buf_v0[1024]; - static crm_exit_t test_exit(crm_exit_t exit_code) { lrmd_api_delete(lrmd_conn); return crm_exit(exit_code); } #define print_result(fmt, args...) \ if (!options.quiet) { \ printf(fmt "\n" , ##args); \ } -#define report_event(event) \ - snprintf(event_buf_v0, sizeof(event_buf_v0), "NEW_EVENT event_type:%s rsc_id:%s action:%s rc:%s op_status:%s", \ - lrmd_event_type2str(event->type), \ - event->rsc_id, \ - event->op_type ? event->op_type : "none", \ - crm_exit_str((crm_exit_t) event->rc), \ - pcmk_exec_status_str(event->op_status)); \ - crm_info("%s", event_buf_v0); - static void test_shutdown(int nsig) { lrmd_api_delete(lrmd_conn); lrmd_conn = NULL; } static void read_events(lrmd_event_data_t * event) { - report_event(event); - if (options.listen) { - if (pcmk__str_eq(options.listen, event_buf_v0, pcmk__str_casei)) { - print_result("LISTEN EVENT SUCCESSFUL"); - test_exit(CRM_EX_OK); - } + char buf[1024] = { '\0', }; + + pcmk__assert(snprintf(buf, sizeof(buf), + "NEW_EVENT event_type:%s rsc_id:%s action:%s rc:%s " + "op_status:%s", + lrmd_event_type2str(event->type), event->rsc_id, + pcmk__s(event->op_type, "none"), + crm_exit_str((crm_exit_t) event->rc), + pcmk_exec_status_str(event->op_status)) >= 0); + crm_info("%s", buf); + + if (options.listen && pcmk__str_eq(options.listen, buf, pcmk__str_casei)) { + print_result("LISTEN EVENT SUCCESSFUL"); + test_exit(CRM_EX_OK); } if (exec_call_id && (event->call_id == exec_call_id)) { if (event->op_status == 0 && event->rc == 0) { print_result("API-CALL SUCCESSFUL for 'exec'"); } else { print_result("API-CALL FAILURE for 'exec', rc:%d lrmd_op_status:%s", event->rc, pcmk_exec_status_str(event->op_status)); test_exit(CRM_EX_ERROR); } if (!options.listen) { test_exit(CRM_EX_OK); } } } static gboolean timeout_err(gpointer data) { print_result("LISTEN EVENT FAILURE - timeout occurred, never found"); test_exit(CRM_EX_TIMEOUT); return FALSE; } static void connection_events(lrmd_event_data_t * event) { int rc = event->connection_rc; if (event->type != lrmd_event_connect) { /* ignore */ return; } if (!rc) { crm_info("Executor client connection established"); start_test(NULL); return; } else { sleep(1); try_connect(); crm_notice("Executor client connection failed"); } } static void try_connect(void) { int tries = 10; static int num_tries = 0; int rc = 0; lrmd_conn->cmds->set_callback(lrmd_conn, connection_events); for (; num_tries < tries; num_tries++) { rc = lrmd_conn->cmds->connect_async(lrmd_conn, crm_system_name, 3000); if (!rc) { return; /* we'll hear back in async callback */ } sleep(1); } print_result("API CONNECTION FAILURE"); test_exit(CRM_EX_ERROR); } static gboolean start_test(gpointer user_data) { int rc = 0; if (!options.no_connect) { if (!lrmd_conn->cmds->is_connected(lrmd_conn)) { try_connect(); /* async connect -- this function will get called back into */ return 0; } } lrmd_conn->cmds->set_callback(lrmd_conn, read_events); if (options.timeout) { pcmk__create_timer(options.timeout, timeout_err, NULL); } if (!options.api_call) { return 0; } if (pcmk__str_eq(options.api_call, "exec", pcmk__str_casei)) { rc = lrmd_conn->cmds->exec(lrmd_conn, options.rsc_id, options.action, NULL, options.interval_ms, options.timeout, options.start_delay, options.exec_call_opts, options.params); if (rc > 0) { exec_call_id = rc; print_result("API-CALL 'exec' action pending, waiting on response"); } } else if (pcmk__str_eq(options.api_call, "register_rsc", pcmk__str_casei)) { rc = lrmd_conn->cmds->register_rsc(lrmd_conn, options.rsc_id, options.class, options.provider, options.type, 0); } else if (pcmk__str_eq(options.api_call, "get_rsc_info", pcmk__str_casei)) { lrmd_rsc_info_t *rsc_info; rsc_info = lrmd_conn->cmds->get_rsc_info(lrmd_conn, options.rsc_id, 0); if (rsc_info) { print_result("RSC_INFO: id:%s class:%s provider:%s type:%s", rsc_info->id, rsc_info->standard, (rsc_info->provider? rsc_info->provider : ""), rsc_info->type); lrmd_free_rsc_info(rsc_info); rc = pcmk_ok; } else { rc = -1; } } else if (pcmk__str_eq(options.api_call, "unregister_rsc", pcmk__str_casei)) { rc = lrmd_conn->cmds->unregister_rsc(lrmd_conn, options.rsc_id, 0); } else if (pcmk__str_eq(options.api_call, "cancel", pcmk__str_casei)) { rc = lrmd_conn->cmds->cancel(lrmd_conn, options.rsc_id, options.action, options.interval_ms); } else if (pcmk__str_eq(options.api_call, "metadata", pcmk__str_casei)) { char *output = NULL; rc = lrmd_conn->cmds->get_metadata(lrmd_conn, options.class, options.provider, options.type, &output, 0); if (rc == pcmk_ok) { print_result("%s", output); free(output); } } else if (pcmk__str_eq(options.api_call, "list_agents", pcmk__str_casei)) { lrmd_list_t *list = NULL; lrmd_list_t *iter = NULL; rc = lrmd_conn->cmds->list_agents(lrmd_conn, &list, options.class, options.provider); if (rc > 0) { print_result("%d agents found", rc); for (iter = list; iter != NULL; iter = iter->next) { print_result("%s", iter->val); } lrmd_list_freeall(list); rc = 0; } else { print_result("API_CALL FAILURE - no agents found"); rc = -1; } } else if (pcmk__str_eq(options.api_call, "list_ocf_providers", pcmk__str_casei)) { lrmd_list_t *list = NULL; lrmd_list_t *iter = NULL; rc = lrmd_conn->cmds->list_ocf_providers(lrmd_conn, options.type, &list); if (rc > 0) { print_result("%d providers found", rc); for (iter = list; iter != NULL; iter = iter->next) { print_result("%s", iter->val); } lrmd_list_freeall(list); rc = 0; } else { print_result("API_CALL FAILURE - no providers found"); rc = -1; } } else if (pcmk__str_eq(options.api_call, "list_standards", pcmk__str_casei)) { lrmd_list_t *list = NULL; lrmd_list_t *iter = NULL; rc = lrmd_conn->cmds->list_standards(lrmd_conn, &list); if (rc > 0) { print_result("%d standards found", rc); for (iter = list; iter != NULL; iter = iter->next) { print_result("%s", iter->val); } lrmd_list_freeall(list); rc = 0; } else { print_result("API_CALL FAILURE - no providers found"); rc = -1; } } else if (pcmk__str_eq(options.api_call, "get_recurring_ops", pcmk__str_casei)) { GList *op_list = NULL; GList *op_item = NULL; rc = lrmd_conn->cmds->get_recurring_ops(lrmd_conn, options.rsc_id, 0, 0, &op_list); for (op_item = op_list; op_item != NULL; op_item = op_item->next) { lrmd_op_info_t *op_info = op_item->data; print_result("RECURRING_OP: %s_%s_%s timeout=%sms", op_info->rsc_id, op_info->action, op_info->interval_ms_s, op_info->timeout_ms_s); lrmd_free_op_info(op_info); } g_list_free(op_list); } else if (options.api_call) { print_result("API-CALL FAILURE unknown action '%s'", options.action); test_exit(CRM_EX_ERROR); } if (rc < 0) { print_result("API-CALL FAILURE for '%s' api_rc:%d", options.api_call, rc); test_exit(CRM_EX_ERROR); } if (options.api_call && rc == pcmk_ok) { print_result("API-CALL SUCCESSFUL for '%s'", options.api_call); if (!options.listen) { test_exit(CRM_EX_OK); } } if (options.no_wait) { /* just make the call and exit regardless of anything else. */ test_exit(CRM_EX_OK); } return 0; } /*! * \internal * \brief Generate resource parameters from CIB if none explicitly given * * \return Standard Pacemaker return code */ static int generate_params(void) { int rc = pcmk_rc_ok; pcmk_scheduler_t *scheduler = NULL; xmlNode *cib_xml_copy = NULL; pcmk_resource_t *rsc = NULL; GHashTable *params = NULL; GHashTable *meta = NULL; GHashTableIter iter; char *key = NULL; char *value = NULL; if (options.params != NULL) { return pcmk_rc_ok; // User specified parameters explicitly } // Retrieve and update CIB rc = cib__signon_query(NULL, NULL, &cib_xml_copy); if (rc != pcmk_rc_ok) { return rc; } rc = pcmk__update_configured_schema(&cib_xml_copy, false); if (rc != pcmk_rc_ok) { return rc; } // Calculate cluster status scheduler = pcmk_new_scheduler(); if (scheduler == NULL) { crm_crit("Could not allocate scheduler data"); return ENOMEM; } pcmk__set_scheduler_flags(scheduler, pcmk__sched_no_counts); scheduler->input = cib_xml_copy; scheduler->priv->now = crm_time_new(NULL); cluster_status(scheduler); // Find resource in CIB rsc = pe_find_resource_with_flags(scheduler->priv->resources, options.rsc_id, pcmk_rsc_match_history |pcmk_rsc_match_basename); if (rsc == NULL) { crm_err("Resource does not exist in config"); pcmk_free_scheduler(scheduler); return EINVAL; } // Add resource instance parameters to options.params params = pe_rsc_params(rsc, NULL, scheduler); if (params != NULL) { g_hash_table_iter_init(&iter, params); while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &value)) { options.params = lrmd_key_value_add(options.params, key, value); } } // Add resource meta-attributes to options.params meta = pcmk__strkey_table(free, free); get_meta_attributes(meta, rsc, NULL, scheduler); 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); options.params = lrmd_key_value_add(options.params, crm_name, value); free(crm_name); } g_hash_table_destroy(meta); pcmk_free_scheduler(scheduler); return rc; } static GOptionContext * build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) { GOptionContext *context = NULL; context = pcmk__build_arg_context(args, NULL, group, NULL); pcmk__add_main_args(context, basic_entries); pcmk__add_arg_group(context, "api-call", "API Call Options:", "Parameters for api-call option", api_call_entries); return context; } int main(int argc, char **argv) { GError *error = NULL; crm_exit_t exit_code = CRM_EX_OK; crm_trigger_t *trig = NULL; pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); /* Typically we'd pass all the single character options that take an argument * as the second parameter here (and there's a bunch of those in this tool). * However, we control how this program is called so we can just not call it * in a way where the preprocessing ever matters. */ gchar **processed_args = pcmk__cmdline_preproc(argv, NULL); GOptionContext *context = build_arg_context(args, NULL); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } /* We have to use crm_log_init here to set up the logging because there's * different handling for daemons vs. command line programs, and * pcmk__cli_init_logging is set up to only handle the latter. */ crm_log_init(NULL, LOG_INFO, TRUE, (args->verbosity? TRUE : FALSE), argc, argv, FALSE); for (int i = 0; i < args->verbosity; i++) { crm_bump_log_level(argc, argv); } if (!options.listen && pcmk__strcase_any_of(options.api_call, "metadata", "list_agents", "list_standards", "list_ocf_providers", NULL)) { options.no_connect = TRUE; } if (options.is_running) { int rc = pcmk_rc_ok; if (options.rsc_id == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "--is-running requires --rsc-id"); goto done; } options.interval_ms = 0; if (options.timeout == 0) { options.timeout = 30000; } rc = generate_params(); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Can not determine resource status: " "unable to get parameters from CIB"); goto done; } options.api_call = "exec"; options.action = PCMK_ACTION_MONITOR; options.exec_call_opts = lrmd_opt_notify_orig_only; } if (!options.api_call && !options.listen) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must specify at least one of --api-call, --listen, " "or --is-running"); goto done; } if (options.use_tls) { lrmd_conn = lrmd_remote_api_new(NULL, "localhost", 0); } else { lrmd_conn = lrmd_api_new(); } trig = mainloop_add_trigger(G_PRIORITY_HIGH, start_test, NULL); mainloop_set_trigger(trig); mainloop_add_signal(SIGTERM, test_shutdown); crm_info("Starting"); mainloop = g_main_loop_new(NULL, FALSE); g_main_loop_run(mainloop); done: g_strfreev(processed_args); pcmk__free_arg_context(context); free(key); free(val); pcmk__output_and_clear_error(&error, NULL); return test_exit(exit_code); } diff --git a/daemons/execd/remoted_tls.c b/daemons/execd/remoted_tls.c index eede13edae..13c867f3e6 100644 --- a/daemons/execd/remoted_tls.c +++ b/daemons/execd/remoted_tls.c @@ -1,440 +1,439 @@ /* * Copyright 2012-2025 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include "pacemaker-execd.h" #include #define LRMD_REMOTE_AUTH_TIMEOUT 10000 static pcmk__tls_t *tls = NULL; static int ssock = -1; extern int lrmd_call_id; /*! * \internal * \brief Read (more) TLS handshake data from client * * \param[in,out] client IPC client doing handshake * * \return 0 on success or more data needed, -1 on error */ static int remoted__read_handshake_data(pcmk__client_t *client) { int rc = pcmk__read_handshake_data(client); if (rc == EAGAIN) { /* No more data is available at the moment. Just return for now; * we'll get invoked again once the client sends more. */ return 0; } else if (rc != pcmk_rc_ok) { return -1; } if (client->remote->auth_timeout) { g_source_remove(client->remote->auth_timeout); } client->remote->auth_timeout = 0; pcmk__set_client_flags(client, pcmk__client_tls_handshake_complete); crm_notice("Remote client connection accepted"); /* Now that the handshake is done, see if any client TLS certificate is * close to its expiration date and log if so. If a TLS certificate is not * in use, this function will just return so we don't need to check for the * session type here. */ pcmk__tls_check_cert_expiration(client->remote->tls_session); /* Only a client with access to the TLS key can connect, so we can treat * it as privileged. */ pcmk__set_client_flags(client, pcmk__client_privileged); // Alert other clients of the new connection notify_of_new_client(client); return 0; } static int lrmd_remote_client_msg(gpointer data) { int id = 0; int rc = pcmk_rc_ok; xmlNode *request = NULL; pcmk__client_t *client = data; if (!pcmk_is_set(client->flags, pcmk__client_tls_handshake_complete)) { return remoted__read_handshake_data(client); } rc = pcmk__remote_ready(client->remote, 0); switch (rc) { case pcmk_rc_ok: break; case ETIME: /* No message available to read */ return 0; default: /* Error */ crm_info("Error polling remote client: %s", pcmk_rc_str(rc)); return -1; } rc = pcmk__read_available_remote_data(client->remote); switch (rc) { case pcmk_rc_ok: break; case EAGAIN: /* We haven't read the whole message yet */ return 0; default: /* Error */ crm_info("Error reading from remote client: %s", pcmk_rc_str(rc)); return -1; } request = pcmk__remote_message_xml(client->remote); if (request == NULL) { return 0; } crm_element_value_int(request, PCMK__XA_LRMD_REMOTE_MSG_ID, &id); crm_trace("Processing remote client request %d", id); if (!client->name) { client->name = crm_element_value_copy(request, PCMK__XA_LRMD_CLIENTNAME); } lrmd_call_id++; if (lrmd_call_id < 1) { lrmd_call_id = 1; } crm_xml_add(request, PCMK__XA_LRMD_CLIENTID, client->id); crm_xml_add(request, PCMK__XA_LRMD_CLIENTNAME, client->name); crm_xml_add_int(request, PCMK__XA_LRMD_CALLID, lrmd_call_id); process_lrmd_message(client, id, request); pcmk__xml_free(request); return 0; } static void lrmd_remote_client_destroy(gpointer user_data) { pcmk__client_t *client = user_data; if (client == NULL) { return; } crm_notice("Cleaning up after remote client %s disconnected", pcmk__client_name(client)); ipc_proxy_remove_provider(client); /* if this is the last remote connection, stop recurring * operations */ if (pcmk__ipc_client_count() == 1) { client_disconnect_cleanup(NULL); } if (client->remote->tls_session) { int csock = pcmk__tls_get_client_sock(client->remote); gnutls_bye(client->remote->tls_session, GNUTLS_SHUT_RDWR); gnutls_deinit(client->remote->tls_session); client->remote->tls_session = NULL; close(csock); } lrmd_client_destroy(client); return; } static gboolean lrmd_auth_timeout_cb(gpointer data) { pcmk__client_t *client = data; client->remote->auth_timeout = 0; if (pcmk_is_set(client->flags, pcmk__client_tls_handshake_complete)) { return FALSE; } mainloop_del_fd(client->remote->source); client->remote->source = NULL; crm_err("Remote client authentication timed out"); return FALSE; } // Dispatch callback for remote server socket static int lrmd_remote_listen(gpointer data) { int csock = -1; gnutls_session_t session = NULL; pcmk__client_t *new_client = NULL; // For client socket static struct mainloop_fd_callbacks lrmd_remote_fd_cb = { .dispatch = lrmd_remote_client_msg, .destroy = lrmd_remote_client_destroy, }; CRM_CHECK(ssock >= 0, return TRUE); if (pcmk__accept_remote_connection(ssock, &csock) != pcmk_rc_ok) { return TRUE; } session = pcmk__new_tls_session(tls, csock); if (session == NULL) { close(csock); return TRUE; } new_client = pcmk__new_unauth_client(NULL); new_client->remote = pcmk__assert_alloc(1, sizeof(pcmk__remote_t)); pcmk__set_client_flags(new_client, pcmk__client_tls); new_client->remote->tls_session = session; // Require the client to authenticate within this time new_client->remote->auth_timeout = pcmk__create_timer(LRMD_REMOTE_AUTH_TIMEOUT, lrmd_auth_timeout_cb, new_client); crm_info("Remote client pending authentication " QB_XS " %p id: %s", new_client, new_client->id); new_client->remote->source = mainloop_add_fd("pacemaker-remote-client", G_PRIORITY_DEFAULT, csock, new_client, &lrmd_remote_fd_cb); return TRUE; } static void tls_server_dropped(gpointer user_data) { crm_notice("TLS server session ended"); return; } // \return 0 on success, -1 on error (gnutls_psk_server_credentials_function) static int lrmd_tls_server_key_cb(gnutls_session_t session, const char *username, gnutls_datum_t * key) { return (lrmd__init_remote_key(key) == pcmk_rc_ok)? 0 : -1; } static int bind_and_listen(struct addrinfo *addr) { int optval; int fd; int rc; char buffer[INET6_ADDRSTRLEN] = { 0, }; pcmk__sockaddr2str(addr->ai_addr, buffer); crm_trace("Attempting to bind to address %s", buffer); fd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol); if (fd < 0) { rc = errno; crm_err("Listener socket creation failed: %", pcmk_rc_str(rc)); return -rc; } /* reuse address */ optval = 1; rc = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); if (rc < 0) { rc = errno; crm_err("Local address reuse not allowed on %s: %s", buffer, pcmk_rc_str(rc)); close(fd); return -rc; } if (addr->ai_family == AF_INET6) { optval = 0; rc = setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &optval, sizeof(optval)); if (rc < 0) { rc = errno; crm_err("Couldn't disable IPV6-only on %s: %s", buffer, pcmk_rc_str(rc)); close(fd); return -rc; } } if (bind(fd, addr->ai_addr, addr->ai_addrlen) != 0) { rc = errno; crm_err("Cannot bind to %s: %s", buffer, pcmk_rc_str(rc)); close(fd); return -rc; } if (listen(fd, 10) == -1) { rc = errno; crm_err("Cannot listen on %s: %s", buffer, pcmk_rc_str(rc)); close(fd); return -rc; } return fd; } static int get_address_info(const char *bind_name, int port, struct addrinfo **res) { - int rc; - char port_str[6]; // at most "65535" + int rc = pcmk_rc_ok; + char *port_s = pcmk__itoa(port); struct addrinfo hints; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_flags = AI_PASSIVE; hints.ai_family = AF_UNSPEC; // IPv6 or IPv4 hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; - snprintf(port_str, sizeof(port_str), "%d", port); - rc = getaddrinfo(bind_name, port_str, &hints, res); + rc = getaddrinfo(bind_name, port_s, &hints, res); rc = pcmk__gaierror2rc(rc); if (rc != pcmk_rc_ok) { crm_err("Unable to get IP address(es) for %s: %s", (bind_name? bind_name : "local node"), pcmk_rc_str(rc)); - return rc; } - return pcmk_rc_ok; + free(port_s); + return rc; } int lrmd_init_remote_tls_server(void) { int rc = pcmk_rc_ok; int filter; int port = crm_default_remote_port(); struct addrinfo *res = NULL, *iter; const char *bind_name = pcmk__env_option(PCMK__ENV_REMOTE_ADDRESS); bool use_cert = pcmk__x509_enabled(); static struct mainloop_fd_callbacks remote_listen_fd_callbacks = { .dispatch = lrmd_remote_listen, .destroy = tls_server_dropped, }; CRM_CHECK(ssock == -1, return ssock); crm_debug("Starting TLS listener on %s port %d", (bind_name? bind_name : "all addresses on"), port); rc = pcmk__init_tls(&tls, true, use_cert ? GNUTLS_CRD_CERTIFICATE : GNUTLS_CRD_PSK); if (rc != pcmk_rc_ok) { return -1; } if (!use_cert) { gnutls_datum_t psk_key = { NULL, 0 }; pcmk__tls_add_psk_callback(tls, lrmd_tls_server_key_cb); /* The key callback won't get called until the first client connection * attempt. Do it once here, so we can warn the user at start-up if we can't * read the key. We don't error out, though, because it's fine if the key is * going to be added later. */ if (lrmd__init_remote_key(&psk_key) != pcmk_rc_ok) { crm_warn("A cluster connection will not be possible until the key is available"); } gnutls_free(psk_key.data); } if (get_address_info(bind_name, port, &res) != pcmk_rc_ok) { return -1; } /* Currently we listen on only one address from the resulting list (the * first IPv6 address we can bind to if possible, otherwise the first IPv4 * address we can bind to). When bind_name is NULL, this should be the * respective wildcard address. * * @TODO If there is demand for specifying more than one address, allow * bind_name to be a space-separated list, call getaddrinfo() for each, * and create a socket for each result (set IPV6_V6ONLY on IPv6 sockets * since IPv4 listeners will have their own sockets). */ iter = res; filter = AF_INET6; while (iter) { if (iter->ai_family == filter) { ssock = bind_and_listen(iter); } if (ssock >= 0) { break; } iter = iter->ai_next; if (iter == NULL && filter == AF_INET6) { iter = res; filter = AF_INET; } } if (ssock >= 0) { mainloop_add_fd("pacemaker-remote-server", G_PRIORITY_DEFAULT, ssock, NULL, &remote_listen_fd_callbacks); crm_debug("Started TLS listener on %s port %d", (bind_name? bind_name : "all addresses on"), port); } freeaddrinfo(res); return ssock; } void execd_stop_tls_server(void) { if (tls != NULL) { pcmk__free_tls(tls); tls = NULL; } if (ssock >= 0) { close(ssock); ssock = -1; } } diff --git a/daemons/fenced/cts-fence-helper.c b/daemons/fenced/cts-fence-helper.c index cc83fd32a3..42c1730324 100644 --- a/daemons/fenced/cts-fence-helper.c +++ b/daemons/fenced/cts-fence-helper.c @@ -1,664 +1,665 @@ /* * Copyright 2009-2025 the Pacemaker project contributors * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SUMMARY "cts-fence-helper - inject commands into the Pacemaker fencer and watch for events" static GMainLoop *mainloop = NULL; static crm_trigger_t *trig = NULL; static int mainloop_iter = 0; static pcmk__action_result_t result = PCMK__UNKNOWN_RESULT; typedef void (*mainloop_test_iteration_cb) (int check_event); #define MAINLOOP_DEFAULT_TIMEOUT 2 enum test_modes { test_standard = 0, // test using a specific developer environment test_api_sanity, // sanity-test stonith client API using fence_dummy test_api_mainloop, // sanity-test mainloop code with async responses }; struct { enum test_modes mode; } options = { .mode = test_standard }; static gboolean mode_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "--mainloop_api_test", "-m", NULL)) { options.mode = test_api_mainloop; } else if (pcmk__str_any_of(option_name, "--api_test", "-t", NULL)) { options.mode = test_api_sanity; } return TRUE; } static GOptionEntry entries[] = { { "mainloop_api_test", 'm', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, mode_cb, NULL, NULL, }, { "api_test", 't', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, mode_cb, NULL, NULL, }, { NULL } }; static stonith_t *st = NULL; static struct pollfd pollfd; static const int st_opts = st_opt_sync_call; static int expected_notifications = 0; static int verbose = 0; static void mainloop_test_done(const char *origin, bool pass) { if (pass) { crm_info("SUCCESS - %s", origin); mainloop_iter++; mainloop_set_trigger(trig); result.execution_status = PCMK_EXEC_DONE; result.exit_status = CRM_EX_OK; } else { crm_err("FAILURE - %s (%d: %s)", origin, result.exit_status, pcmk_exec_status_str(result.execution_status)); crm_exit(CRM_EX_ERROR); } } static void dispatch_helper(int timeout) { int rc; crm_debug("Looking for notification"); pollfd.events = POLLIN; while (true) { rc = poll(&pollfd, 1, timeout); /* wait 10 minutes, -1 forever */ if (rc > 0) { if (stonith__api_dispatch(st) != pcmk_rc_ok) { break; } } else { break; } } } static void st_callback(stonith_t * st, stonith_event_t * e) { char *desc = NULL; if (st->state == stonith_disconnected) { crm_exit(CRM_EX_DISCONNECT); } desc = stonith__event_description(e); crm_notice("%s", desc); free(desc); if (expected_notifications) { expected_notifications--; } } static void st_global_callback(stonith_t * stonith, stonith_callback_data_t * data) { crm_notice("Call %d exited %d: %s (%s)", data->call_id, stonith__exit_status(data), stonith__execution_status(data), pcmk__s(stonith__exit_reason(data), "unspecified reason")); } #define single_test(cmd, str, num_notifications, expected_rc) \ { \ int rc = 0; \ rc = cmd; \ expected_notifications = 0; \ if (num_notifications) { \ expected_notifications = num_notifications; \ dispatch_helper(500); \ } \ if (rc != expected_rc) { \ crm_err("FAILURE - expected rc %d != %d(%s) for cmd - %s", expected_rc, rc, pcmk_strerror(rc), str); \ crm_exit(CRM_EX_ERROR); \ } else if (expected_notifications) { \ crm_err("FAILURE - expected %d notifications, got only %d for cmd - %s", \ num_notifications, num_notifications - expected_notifications, str); \ crm_exit(CRM_EX_ERROR); \ } else { \ if (verbose) { \ crm_info("SUCCESS - %s: %d", str, rc); \ } else { \ crm_debug("SUCCESS - %s: %d", str, rc); \ } \ } \ }\ static void run_fence_failure_test(void) { stonith_key_value_t *params = NULL; params = stonith__key_value_add(params, PCMK_STONITH_HOST_MAP, "false_1_node1=1,2 false_1_node2=3,4"); params = stonith__key_value_add(params, "mode", "fail"); single_test(st-> cmds->register_device(st, st_opts, "test-id1", "stonith-ng", "fence_dummy", params), "Register device1 for failure test", 1, 0); single_test(st->cmds->fence(st, st_opts, "false_1_node2", PCMK_ACTION_OFF, 3, 0), "Fence failure results off", 1, -ENODATA); single_test(st->cmds->fence(st, st_opts, "false_1_node2", PCMK_ACTION_REBOOT, 3, 0), "Fence failure results reboot", 1, -ENODATA); single_test(st->cmds->remove_device(st, st_opts, "test-id1"), "Remove device1 for failure test", 1, 0); stonith__key_value_freeall(params, true, true); } static void run_fence_failure_rollover_test(void) { stonith_key_value_t *params = NULL; params = stonith__key_value_add(params, PCMK_STONITH_HOST_MAP, "false_1_node1=1,2 false_1_node2=3,4"); params = stonith__key_value_add(params, "mode", "fail"); single_test(st-> cmds->register_device(st, st_opts, "test-id1", "stonith-ng", "fence_dummy", params), "Register device1 for rollover test", 1, 0); stonith__key_value_freeall(params, true, true); params = NULL; params = stonith__key_value_add(params, PCMK_STONITH_HOST_MAP, "false_1_node1=1,2 false_1_node2=3,4"); params = stonith__key_value_add(params, "mode", "pass"); single_test(st-> cmds->register_device(st, st_opts, "test-id2", "stonith-ng", "fence_dummy", params), "Register device2 for rollover test", 1, 0); single_test(st->cmds->fence(st, st_opts, "false_1_node2", PCMK_ACTION_OFF, 3, 0), "Fence rollover results off", 1, 0); /* Expect -ENODEV because fence_dummy requires 'on' to be executed on target */ single_test(st->cmds->fence(st, st_opts, "false_1_node2", PCMK_ACTION_ON, 3, 0), "Fence rollover results on", 1, -ENODEV); single_test(st->cmds->remove_device(st, st_opts, "test-id1"), "Remove device1 for rollover tests", 1, 0); single_test(st->cmds->remove_device(st, st_opts, "test-id2"), "Remove device2 for rollover tests", 1, 0); stonith__key_value_freeall(params, true, true); } static void run_standard_test(void) { stonith_key_value_t *params = NULL; params = stonith__key_value_add(params, PCMK_STONITH_HOST_MAP, "false_1_node1=1,2 false_1_node2=3,4"); params = stonith__key_value_add(params, "mode", "pass"); params = stonith__key_value_add(params, "mock_dynamic_hosts", "false_1_node1 false_1_node2"); single_test(st-> cmds->register_device(st, st_opts, "test-id", "stonith-ng", "fence_dummy", params), "Register", 1, 0); stonith__key_value_freeall(params, true, true); params = NULL; single_test(st->cmds->list(st, st_opts, "test-id", NULL, 1), PCMK_ACTION_LIST, 0, 0); single_test(st->cmds->monitor(st, st_opts, "test-id", 1), "Monitor", 0, 0); single_test(st->cmds->status(st, st_opts, "test-id", "false_1_node2", 1), "Status false_1_node2", 0, 0); single_test(st->cmds->status(st, st_opts, "test-id", "false_1_node1", 1), "Status false_1_node1", 0, 0); single_test(st->cmds->fence(st, st_opts, "unknown-host", PCMK_ACTION_OFF, 1, 0), "Fence unknown-host (expected failure)", 0, -ENODEV); single_test(st->cmds->fence(st, st_opts, "false_1_node1", PCMK_ACTION_OFF, 1, 0), "Fence false_1_node1", 1, 0); /* Expect -ENODEV because fence_dummy requires 'on' to be executed on target */ single_test(st->cmds->fence(st, st_opts, "false_1_node1", PCMK_ACTION_ON, 1, 0), "Unfence false_1_node1", 1, -ENODEV); /* Confirm that an invalid level index is rejected */ single_test(st->cmds->register_level(st, st_opts, "node1", 999, params), "Attempt to register an invalid level index", 0, -EINVAL); single_test(st->cmds->remove_device(st, st_opts, "test-id"), "Remove test-id", 1, 0); stonith__key_value_freeall(params, true, true); } static void sanity_tests(void) { int rc = 0; rc = st->cmds->connect(st, crm_system_name, &pollfd.fd); if (rc != pcmk_ok) { stonith__api_free(st); crm_exit(CRM_EX_DISCONNECT); } st->cmds->register_notification(st, PCMK__VALUE_ST_NOTIFY_DISCONNECT, st_callback); st->cmds->register_notification(st, PCMK__VALUE_ST_NOTIFY_FENCE, st_callback); st->cmds->register_notification(st, STONITH_OP_DEVICE_ADD, st_callback); st->cmds->register_notification(st, STONITH_OP_DEVICE_DEL, st_callback); st->cmds->register_callback(st, 0, 120, st_opt_timeout_updates, NULL, "st_global_callback", st_global_callback); crm_info("Starting API Sanity Tests"); run_standard_test(); run_fence_failure_test(); run_fence_failure_rollover_test(); crm_info("Sanity Tests Passed"); } static void standard_dev_test(void) { int rc = 0; char *tmp = NULL; stonith_key_value_t *params = NULL; rc = st->cmds->connect(st, crm_system_name, &pollfd.fd); if (rc != pcmk_ok) { stonith__api_free(st); crm_exit(CRM_EX_DISCONNECT); } params = stonith__key_value_add(params, PCMK_STONITH_HOST_MAP, "some-host=pcmk-7 true_1_node1=3,4"); rc = st->cmds->register_device(st, st_opts, "test-id", "stonith-ng", "fence_xvm", params); crm_debug("Register: %d", rc); rc = st->cmds->list(st, st_opts, "test-id", &tmp, 10); crm_debug("List: %d output: %s", rc, tmp ? tmp : ""); rc = st->cmds->monitor(st, st_opts, "test-id", 10); crm_debug("Monitor: %d", rc); rc = st->cmds->status(st, st_opts, "test-id", "false_1_node2", 10); crm_debug("Status false_1_node2: %d", rc); rc = st->cmds->status(st, st_opts, "test-id", "false_1_node1", 10); crm_debug("Status false_1_node1: %d", rc); rc = st->cmds->fence(st, st_opts, "unknown-host", PCMK_ACTION_OFF, 60, 0); crm_debug("Fence unknown-host: %d", rc); rc = st->cmds->status(st, st_opts, "test-id", "false_1_node1", 10); crm_debug("Status false_1_node1: %d", rc); rc = st->cmds->fence(st, st_opts, "false_1_node1", PCMK_ACTION_OFF, 60, 0); crm_debug("Fence false_1_node1: %d", rc); rc = st->cmds->status(st, st_opts, "test-id", "false_1_node1", 10); crm_debug("Status false_1_node1: %d", rc); rc = st->cmds->fence(st, st_opts, "false_1_node1", PCMK_ACTION_ON, 10, 0); crm_debug("Unfence false_1_node1: %d", rc); rc = st->cmds->status(st, st_opts, "test-id", "false_1_node1", 10); crm_debug("Status false_1_node1: %d", rc); rc = st->cmds->fence(st, st_opts, "some-host", PCMK_ACTION_OFF, 10, 0); crm_debug("Fence alias: %d", rc); rc = st->cmds->status(st, st_opts, "test-id", "some-host", 10); crm_debug("Status alias: %d", rc); rc = st->cmds->fence(st, st_opts, "false_1_node1", PCMK_ACTION_ON, 10, 0); crm_debug("Unfence false_1_node1: %d", rc); rc = st->cmds->remove_device(st, st_opts, "test-id"); crm_debug("Remove test-id: %d", rc); stonith__key_value_freeall(params, true, true); } static void iterate_mainloop_tests(gboolean event_ready); static void mainloop_callback(stonith_t * stonith, stonith_callback_data_t * data) { pcmk__set_result(&result, stonith__exit_status(data), stonith__execution_status(data), stonith__exit_reason(data)); iterate_mainloop_tests(TRUE); } static int register_callback_helper(int callid) { return st->cmds->register_callback(st, callid, MAINLOOP_DEFAULT_TIMEOUT, st_opt_timeout_updates, NULL, "callback", mainloop_callback); } static void test_async_fence_pass(int check_event) { int rc = 0; if (check_event) { mainloop_test_done(__func__, (result.exit_status == CRM_EX_OK)); return; } rc = st->cmds->fence(st, 0, "true_1_node1", PCMK_ACTION_OFF, MAINLOOP_DEFAULT_TIMEOUT, 0); if (rc < 0) { crm_err("fence failed with rc %d", rc); mainloop_test_done(__func__, false); } register_callback_helper(rc); /* wait for event */ } #define CUSTOM_TIMEOUT_ADDITION 10 static void test_async_fence_custom_timeout(int check_event) { int rc = 0; static time_t begin = 0; if (check_event) { uint32_t diff = (time(NULL) - begin); if (result.execution_status != PCMK_EXEC_TIMEOUT) { mainloop_test_done(__func__, false); } else if (diff < CUSTOM_TIMEOUT_ADDITION + MAINLOOP_DEFAULT_TIMEOUT) { crm_err ("Custom timeout test failed, callback expiration should be updated to %d, actual timeout was %d", CUSTOM_TIMEOUT_ADDITION + MAINLOOP_DEFAULT_TIMEOUT, diff); mainloop_test_done(__func__, false); } else { mainloop_test_done(__func__, true); } return; } begin = time(NULL); rc = st->cmds->fence(st, 0, "custom_timeout_node1", PCMK_ACTION_OFF, MAINLOOP_DEFAULT_TIMEOUT, 0); if (rc < 0) { crm_err("fence failed with rc %d", rc); mainloop_test_done(__func__, false); } register_callback_helper(rc); /* wait for event */ } static void test_async_fence_timeout(int check_event) { int rc = 0; if (check_event) { mainloop_test_done(__func__, (result.execution_status == PCMK_EXEC_NO_FENCE_DEVICE)); return; } rc = st->cmds->fence(st, 0, "false_1_node2", PCMK_ACTION_OFF, MAINLOOP_DEFAULT_TIMEOUT, 0); if (rc < 0) { crm_err("fence failed with rc %d", rc); mainloop_test_done(__func__, false); } register_callback_helper(rc); /* wait for event */ } static void test_async_monitor(int check_event) { int rc = 0; if (check_event) { mainloop_test_done(__func__, (result.exit_status == CRM_EX_OK)); return; } rc = st->cmds->monitor(st, 0, "false_1", MAINLOOP_DEFAULT_TIMEOUT); if (rc < 0) { crm_err("monitor failed with rc %d", rc); mainloop_test_done(__func__, false); } register_callback_helper(rc); /* wait for event */ } static void test_register_async_devices(int check_event) { - char buf[16] = { 0, }; + char *off_timeout_s = pcmk__itoa(MAINLOOP_DEFAULT_TIMEOUT + + CUSTOM_TIMEOUT_ADDITION); stonith_key_value_t *params = NULL; params = stonith__key_value_add(params, PCMK_STONITH_HOST_MAP, "false_1_node1=1,2"); params = stonith__key_value_add(params, "mode", "fail"); st->cmds->register_device(st, st_opts, "false_1", "stonith-ng", "fence_dummy", params); stonith__key_value_freeall(params, true, true); params = NULL; params = stonith__key_value_add(params, PCMK_STONITH_HOST_MAP, "true_1_node1=1,2"); params = stonith__key_value_add(params, "mode", "pass"); st->cmds->register_device(st, st_opts, "true_1", "stonith-ng", "fence_dummy", params); stonith__key_value_freeall(params, true, true); params = NULL; params = stonith__key_value_add(params, PCMK_STONITH_HOST_MAP, "custom_timeout_node1=1,2"); params = stonith__key_value_add(params, "mode", "fail"); params = stonith__key_value_add(params, "delay", "1000"); - snprintf(buf, sizeof(buf) - 1, "%d", MAINLOOP_DEFAULT_TIMEOUT + CUSTOM_TIMEOUT_ADDITION); - params = stonith__key_value_add(params, "pcmk_off_timeout", buf); + params = stonith__key_value_add(params, "pcmk_off_timeout", off_timeout_s); st->cmds->register_device(st, st_opts, "false_custom_timeout", "stonith-ng", "fence_dummy", params); stonith__key_value_freeall(params, true, true); + free(off_timeout_s); mainloop_test_done(__func__, true); } static void try_mainloop_connect(int check_event) { int rc = stonith__api_connect_retry(st, crm_system_name, 10); if (rc == pcmk_rc_ok) { mainloop_test_done(__func__, true); return; } crm_err("API CONNECTION FAILURE"); mainloop_test_done(__func__, false); } static void iterate_mainloop_tests(gboolean event_ready) { static mainloop_test_iteration_cb callbacks[] = { try_mainloop_connect, test_register_async_devices, test_async_monitor, test_async_fence_pass, test_async_fence_timeout, test_async_fence_custom_timeout, }; if (mainloop_iter == (sizeof(callbacks) / sizeof(mainloop_test_iteration_cb))) { /* all tests ran, everything passed */ crm_info("ALL MAINLOOP TESTS PASSED!"); crm_exit(CRM_EX_OK); } callbacks[mainloop_iter] (event_ready); } static gboolean trigger_iterate_mainloop_tests(gpointer user_data) { iterate_mainloop_tests(FALSE); return TRUE; } static void test_shutdown(int nsig) { int rc = 0; if (st) { rc = st->cmds->disconnect(st); crm_info("Disconnect: %d", rc); crm_debug("Destroy"); stonith__api_free(st); } if (rc) { crm_exit(CRM_EX_ERROR); } } static void mainloop_tests(void) { trig = mainloop_add_trigger(G_PRIORITY_HIGH, trigger_iterate_mainloop_tests, NULL); mainloop_set_trigger(trig); mainloop_add_signal(SIGTERM, test_shutdown); crm_info("Starting"); mainloop = g_main_loop_new(NULL, FALSE); g_main_loop_run(mainloop); } static GOptionContext * build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) { GOptionContext *context = NULL; context = pcmk__build_arg_context(args, NULL, group, NULL); pcmk__add_main_args(context, entries); return context; } int main(int argc, char **argv) { GError *error = NULL; crm_exit_t exit_code = CRM_EX_OK; pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); gchar **processed_args = pcmk__cmdline_preproc(argv, NULL); GOptionContext *context = build_arg_context(args, NULL); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } /* We have to use crm_log_init here to set up the logging because there's * different handling for daemons vs. command line programs, and * pcmk__cli_init_logging is set up to only handle the latter. */ crm_log_init(NULL, LOG_INFO, TRUE, (verbose? TRUE : FALSE), argc, argv, FALSE); for (int i = 0; i < args->verbosity; i++) { crm_bump_log_level(argc, argv); } st = stonith__api_new(); if (st == NULL) { exit_code = CRM_EX_DISCONNECT; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to fencer: API memory allocation failed"); goto done; } switch (options.mode) { case test_standard: standard_dev_test(); break; case test_api_sanity: sanity_tests(); break; case test_api_mainloop: mainloop_tests(); break; } test_shutdown(0); done: g_strfreev(processed_args); pcmk__free_arg_context(context); pcmk__output_and_clear_error(&error, NULL); crm_exit(exit_code); } diff --git a/daemons/fenced/fenced_commands.c b/daemons/fenced/fenced_commands.c index 68c9a6df59..9934760ea5 100644 --- a/daemons/fenced/fenced_commands.c +++ b/daemons/fenced/fenced_commands.c @@ -1,3651 +1,3653 @@ /* * Copyright 2009-2025 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 #include #include // bool #include #include #include #include #include #include #include #include #include #include #include // xmlNode #include // xmlXPathObject, etc. #include #include #include #include #include #include #include #include #include static GHashTable *device_table = NULL; GHashTable *topology = NULL; static GList *cmd_list = NULL; static GHashTable *fenced_handlers = NULL; struct device_search_s { /* target of fence action */ char *host; /* requested fence action */ char *action; /* timeout to use if a device is queried dynamically for possible targets */ // @TODO This name is misleading now, it's the value of stonith-timeout int per_device_timeout; /* number of registered fencing devices at time of request */ int replies_needed; /* number of device replies received so far */ int replies_received; /* whether the target is eligible to perform requested action (or off) */ bool allow_self; /* private data to pass to search callback function */ void *user_data; /* function to call when all replies have been received */ void (*callback) (GList * devices, void *user_data); /* devices capable of performing requested action (or off if remapping) */ GList *capable; /* Whether to perform searches that support the action */ uint32_t support_action_only; }; static gboolean stonith_device_dispatch(gpointer user_data); static void st_child_done(int pid, const pcmk__action_result_t *result, void *user_data); static void search_devices_record_result(struct device_search_s *search, const char *device, gboolean can_fence); static int get_agent_metadata(const char *agent, xmlNode **metadata); static void read_action_metadata(fenced_device_t *device); static enum fenced_target_by unpack_level_kind(const xmlNode *level); typedef struct { int id; uint32_t options; int default_timeout; /* seconds */ int timeout; /* seconds */ int start_delay; // seconds (-1 means disable static/random fencing delays) int delay_id; char *op; char *origin; char *client; char *client_name; char *remote_op_id; char *target; char *action; char *device; //! Head of device list (used only for freeing list with command object) GList *device_list; //! Next item to process in \c device_list GList *next_device_iter; void *internal_user_data; void (*done_cb) (int pid, const pcmk__action_result_t *result, void *user_data); fenced_device_t *active_on; fenced_device_t *activating_on; } async_command_t; static xmlNode *construct_async_reply(const async_command_t *cmd, const pcmk__action_result_t *result); /*! * \internal * \brief Set a bad fencer API request error in a result object * * \param[out] result Result to set */ static inline void set_bad_request_result(pcmk__action_result_t *result) { pcmk__set_result(result, CRM_EX_PROTOCOL, PCMK_EXEC_INVALID, "Fencer API request missing required information (bug?)"); } /*! * \internal * \brief Check whether the fencer's device table contains a watchdog device * * \retval \c true If the device table contains a watchdog device * \retval \c false Otherwise */ bool fenced_has_watchdog_device(void) { return (device_table != NULL) && (g_hash_table_lookup(device_table, STONITH_WATCHDOG_ID) != NULL); } /*! * \internal * \brief Call a function for each known fence device * * \param[in] fn Function to call for each device * \param[in,out] user_data User data */ void fenced_foreach_device(GHFunc fn, gpointer user_data) { if (device_table != NULL) { g_hash_table_foreach(device_table, fn, user_data); } } /*! * \internal * \brief Remove each known fence device matching a given predicate * * \param[in] fn Function that returns \c TRUE to remove a fence device or * \c FALSE to keep it */ void fenced_foreach_device_remove(GHRFunc fn) { if (device_table != NULL) { g_hash_table_foreach_remove(device_table, fn, NULL); } } static gboolean is_action_required(const char *action, const fenced_device_t *device) { return (device != NULL) && pcmk_is_set(device->flags, fenced_df_auto_unfence) && pcmk__str_eq(action, PCMK_ACTION_ON, pcmk__str_none); } static int get_action_delay_max(const fenced_device_t *device, const char *action) { const char *value = NULL; guint delay_max = 0U; if (!pcmk__is_fencing_action(action)) { return 0; } value = g_hash_table_lookup(device->params, PCMK_STONITH_DELAY_MAX); if (value) { pcmk_parse_interval_spec(value, &delay_max); delay_max /= 1000; } return (int) delay_max; } static int get_action_delay_base(const fenced_device_t *device, const char *action, const char *target) { char *hash_value = NULL; guint delay_base = 0U; if (!pcmk__is_fencing_action(action)) { return 0; } hash_value = g_hash_table_lookup(device->params, PCMK_STONITH_DELAY_BASE); if (hash_value) { char *value = pcmk__str_copy(hash_value); char *valptr = value; if (target != NULL) { for (char *val = strtok(value, "; \t"); val != NULL; val = strtok(NULL, "; \t")) { char *mapval = strchr(val, ':'); if (mapval == NULL || mapval[1] == 0) { crm_err("pcmk_delay_base: empty value in mapping", val); continue; } if (mapval != val && strncasecmp(target, val, (size_t)(mapval - val)) == 0) { value = mapval + 1; crm_debug("pcmk_delay_base mapped to %s for %s", value, target); break; } } } if (strchr(value, ':') == 0) { pcmk_parse_interval_spec(value, &delay_base); delay_base /= 1000; } free(valptr); } return (int) delay_base; } /*! * \internal * \brief Override STONITH timeout with pcmk_*_timeout if available * * \param[in] device STONITH device to use * \param[in] action STONITH action name * \param[in] default_timeout Timeout to use if device does not have * a pcmk_*_timeout parameter for action * * \return Value of pcmk_(action)_timeout if available, otherwise default_timeout * \note For consistency, it would be nice if reboot/off/on timeouts could be * set the same way as start/stop/monitor timeouts, i.e. with an * entry in the fencing resource configuration. However that * is insufficient because fencing devices may be registered directly via * the fencer's register_device() API instead of going through the CIB * (e.g. stonith_admin uses it for its -R option, and the executor uses it * to ensure a device is registered when a command is issued). As device * properties, pcmk_*_timeout parameters can be grabbed by the fencer when * the device is registered, whether by CIB change or API call. */ static int get_action_timeout(const fenced_device_t *device, const char *action, int default_timeout) { if (action && device && device->params) { - char buffer[64] = { 0, }; + char *timeout_param = NULL; const char *value = NULL; /* If "reboot" was requested but the device does not support it, * we will remap to "off", so check timeout for "off" instead */ if (pcmk__str_eq(action, PCMK_ACTION_REBOOT, pcmk__str_none) && !pcmk_is_set(device->flags, fenced_df_supports_reboot)) { crm_trace("%s doesn't support reboot, using timeout for off instead", device->id); action = PCMK_ACTION_OFF; } /* If the device config specified an action-specific timeout, use it */ - snprintf(buffer, sizeof(buffer), "pcmk_%s_timeout", action); - value = g_hash_table_lookup(device->params, buffer); + timeout_param = crm_strdup_printf("pcmk_%s_timeout", action); + value = g_hash_table_lookup(device->params, timeout_param); + free(timeout_param); + if (value) { long long timeout_ms = crm_get_msec(value); return (int) QB_MIN(pcmk__timeout_ms2s(timeout_ms), INT_MAX); } } return default_timeout; } /*! * \internal * \brief Get the currently executing device for a fencing operation * * \param[in] cmd Fencing operation to check * * \return Currently executing device for \p cmd if any, otherwise NULL */ static fenced_device_t * cmd_device(const async_command_t *cmd) { if ((cmd == NULL) || (cmd->device == NULL) || (device_table == NULL)) { return NULL; } return g_hash_table_lookup(device_table, cmd->device); } /*! * \internal * \brief Return the configured reboot action for a given device * * \param[in] device_id Device ID * * \return Configured reboot action for \p device_id */ const char * fenced_device_reboot_action(const char *device_id) { const char *action = NULL; if ((device_table != NULL) && (device_id != NULL)) { fenced_device_t *device = g_hash_table_lookup(device_table, device_id); if ((device != NULL) && (device->params != NULL)) { action = g_hash_table_lookup(device->params, "pcmk_reboot_action"); } } return pcmk__s(action, PCMK_ACTION_REBOOT); } /*! * \internal * \brief Check whether a given device supports the "on" action * * \param[in] device_id Device ID * * \return true if \p device_id supports "on", otherwise false */ bool fenced_device_supports_on(const char *device_id) { if ((device_table != NULL) && (device_id != NULL)) { fenced_device_t *device = g_hash_table_lookup(device_table, device_id); if (device != NULL) { return pcmk_is_set(device->flags, fenced_df_supports_on); } } return false; } static void free_async_command(async_command_t * cmd) { if (!cmd) { return; } if (cmd->delay_id) { g_source_remove(cmd->delay_id); } cmd_list = g_list_remove(cmd_list, cmd); g_list_free_full(cmd->device_list, free); free(cmd->device); free(cmd->action); free(cmd->target); free(cmd->remote_op_id); free(cmd->client); free(cmd->client_name); free(cmd->origin); free(cmd->op); free(cmd); } /*! * \internal * \brief Create a new asynchronous fencing operation from request XML * * \param[in] msg Fencing request XML (from IPC or CPG) * * \return Newly allocated fencing operation on success, otherwise NULL * * \note This asserts on memory errors, so a NULL return indicates an * unparseable message. */ static async_command_t * create_async_command(xmlNode *msg) { xmlNode *op = NULL; async_command_t *cmd = NULL; int rc = pcmk_rc_ok; if (msg == NULL) { return NULL; } op = pcmk__xpath_find_one(msg->doc, "//*[@" PCMK__XA_ST_DEVICE_ACTION "]", LOG_ERR); if (op == NULL) { return NULL; } cmd = pcmk__assert_alloc(1, sizeof(async_command_t)); // All messages must include these cmd->action = crm_element_value_copy(op, PCMK__XA_ST_DEVICE_ACTION); cmd->op = crm_element_value_copy(msg, PCMK__XA_ST_OP); cmd->client = crm_element_value_copy(msg, PCMK__XA_ST_CLIENTID); if ((cmd->action == NULL) || (cmd->op == NULL) || (cmd->client == NULL)) { free_async_command(cmd); return NULL; } crm_element_value_int(msg, PCMK__XA_ST_CALLID, &(cmd->id)); crm_element_value_int(msg, PCMK__XA_ST_DELAY, &(cmd->start_delay)); crm_element_value_int(msg, PCMK__XA_ST_TIMEOUT, &(cmd->default_timeout)); cmd->timeout = cmd->default_timeout; rc = pcmk__xe_get_flags(msg, PCMK__XA_ST_CALLOPT, &(cmd->options), st_opt_none); if (rc != pcmk_rc_ok) { crm_warn("Couldn't parse options from request: %s", pcmk_rc_str(rc)); } cmd->origin = crm_element_value_copy(msg, PCMK__XA_SRC); cmd->remote_op_id = crm_element_value_copy(msg, PCMK__XA_ST_REMOTE_OP); cmd->client_name = crm_element_value_copy(msg, PCMK__XA_ST_CLIENTNAME); cmd->target = crm_element_value_copy(op, PCMK__XA_ST_TARGET); cmd->device = crm_element_value_copy(op, PCMK__XA_ST_DEVICE_ID); cmd->done_cb = st_child_done; // Track in global command list cmd_list = g_list_append(cmd_list, cmd); return cmd; } static int get_action_limit(fenced_device_t *device) { const char *value = NULL; int action_limit = 1; value = g_hash_table_lookup(device->params, PCMK_STONITH_ACTION_LIMIT); if ((value == NULL) || (pcmk__scan_min_int(value, &action_limit, INT_MIN) != pcmk_rc_ok) || (action_limit == 0)) { action_limit = 1; } return action_limit; } static int get_active_cmds(fenced_device_t *device) { int counter = 0; GList *gIter = NULL; GList *gIterNext = NULL; CRM_CHECK(device != NULL, return 0); for (gIter = cmd_list; gIter != NULL; gIter = gIterNext) { async_command_t *cmd = gIter->data; gIterNext = gIter->next; if (cmd->active_on == device) { counter++; } } return counter; } static void fork_cb(int pid, void *user_data) { async_command_t *cmd = (async_command_t *) user_data; fenced_device_t *device = cmd->activating_on; if (device == NULL) { /* In case of a retry, we've done the move from activating_on to * active_on already */ device = cmd->active_on; } pcmk__assert(device != NULL); crm_debug("Operation '%s' [%d]%s%s using %s now running with %ds timeout", cmd->action, pid, ((cmd->target == NULL)? "" : " targeting "), pcmk__s(cmd->target, ""), device->id, cmd->timeout); cmd->active_on = device; cmd->activating_on = NULL; } static int get_agent_metadata_cb(gpointer data) { fenced_device_t *device = data; guint period_ms; switch (get_agent_metadata(device->agent, &device->agent_metadata)) { case pcmk_rc_ok: if (device->agent_metadata) { read_action_metadata(device); device->default_host_arg = stonith__default_host_arg(device->agent_metadata); } return G_SOURCE_REMOVE; case EAGAIN: period_ms = pcmk__mainloop_timer_get_period(device->timer); if (period_ms < 160 * 1000) { mainloop_timer_set_period(device->timer, 2 * period_ms); } return G_SOURCE_CONTINUE; default: return G_SOURCE_REMOVE; } } /*! * \internal * \brief Call a command's action callback for an internal (not library) result * * \param[in,out] cmd Command to report result for * \param[in] execution_status Execution status to use for result * \param[in] exit_status Exit status to use for result * \param[in] exit_reason Exit reason to use for result */ static void report_internal_result(async_command_t *cmd, int exit_status, int execution_status, const char *exit_reason) { pcmk__action_result_t result = PCMK__UNKNOWN_RESULT; pcmk__set_result(&result, exit_status, execution_status, exit_reason); cmd->done_cb(0, &result, cmd); pcmk__reset_result(&result); } static gboolean stonith_device_execute(fenced_device_t *device) { int exec_rc = 0; const char *action_str = NULL; async_command_t *cmd = NULL; stonith_action_t *action = NULL; int active_cmds = 0; int action_limit = 0; GList *gIter = NULL; GList *gIterNext = NULL; CRM_CHECK(device != NULL, return FALSE); active_cmds = get_active_cmds(device); action_limit = get_action_limit(device); if (action_limit > -1 && active_cmds >= action_limit) { crm_trace("%s is over its action limit of %d (%u active action%s)", device->id, action_limit, active_cmds, pcmk__plural_s(active_cmds)); return TRUE; } for (gIter = device->pending_ops; gIter != NULL; gIter = gIterNext) { async_command_t *pending_op = gIter->data; gIterNext = gIter->next; if (pending_op && pending_op->delay_id) { crm_trace("Operation '%s'%s%s using %s was asked to run too early, " "waiting for start delay of %ds", pending_op->action, ((pending_op->target == NULL)? "" : " targeting "), pcmk__s(pending_op->target, ""), device->id, pending_op->start_delay); continue; } device->pending_ops = g_list_remove_link(device->pending_ops, gIter); g_list_free_1(gIter); cmd = pending_op; break; } if (cmd == NULL) { crm_trace("No actions using %s are needed", device->id); return TRUE; } if (pcmk__str_any_of(device->agent, STONITH_WATCHDOG_AGENT, STONITH_WATCHDOG_AGENT_INTERNAL, NULL)) { if (pcmk__is_fencing_action(cmd->action)) { if (node_does_watchdog_fencing(fenced_get_local_node())) { pcmk__panic("Watchdog self-fencing required"); goto done; } } else { crm_info("Faking success for %s watchdog operation", cmd->action); report_internal_result(cmd, CRM_EX_OK, PCMK_EXEC_DONE, NULL); goto done; } } #if PCMK__ENABLE_CIBSECRETS exec_rc = pcmk__substitute_secrets(device->id, device->params); if (exec_rc != pcmk_rc_ok) { if (pcmk__str_eq(cmd->action, PCMK_ACTION_STOP, pcmk__str_none)) { crm_info("Proceeding with stop operation for %s " "despite being unable to load CIB secrets (%s)", device->id, pcmk_rc_str(exec_rc)); } else { crm_err("Considering %s unconfigured " "because unable to load CIB secrets: %s", device->id, pcmk_rc_str(exec_rc)); report_internal_result(cmd, CRM_EX_ERROR, PCMK_EXEC_NO_SECRETS, "Failed to get CIB secrets"); goto done; } } #endif action_str = cmd->action; if (pcmk__str_eq(cmd->action, PCMK_ACTION_REBOOT, pcmk__str_none) && !pcmk_is_set(device->flags, fenced_df_supports_reboot)) { crm_notice("Remapping 'reboot' action%s%s using %s to 'off' " "because agent '%s' does not support reboot", ((cmd->target == NULL)? "" : " targeting "), pcmk__s(cmd->target, ""), device->id, device->agent); action_str = PCMK_ACTION_OFF; } action = stonith__action_create(device->agent, action_str, cmd->target, cmd->timeout, device->params, device->aliases, device->default_host_arg); /* for async exec, exec_rc is negative for early error exit otherwise handling of success/errors is done via callbacks */ cmd->activating_on = device; exec_rc = stonith__execute_async(action, (void *)cmd, cmd->done_cb, fork_cb); if (exec_rc < 0) { cmd->activating_on = NULL; cmd->done_cb(0, stonith__action_result(action), cmd); stonith__destroy_action(action); } done: /* Device might get triggered to work by multiple fencing commands * simultaneously. Trigger the device again to make sure any * remaining concurrent commands get executed. */ if (device->pending_ops) { mainloop_set_trigger(device->work); } return TRUE; } static gboolean stonith_device_dispatch(gpointer user_data) { return stonith_device_execute(user_data); } static gboolean start_delay_helper(gpointer data) { async_command_t *cmd = data; fenced_device_t *device = cmd_device(cmd); cmd->delay_id = 0; if (device) { mainloop_set_trigger(device->work); } return FALSE; } static void schedule_stonith_command(async_command_t *cmd, fenced_device_t *device) { int delay_max = 0; int delay_base = 0; int requested_delay = cmd->start_delay; CRM_CHECK(cmd != NULL, return); CRM_CHECK(device != NULL, return); if (cmd->device) { free(cmd->device); } cmd->device = pcmk__str_copy(device->id); cmd->timeout = get_action_timeout(device, cmd->action, cmd->default_timeout); if (cmd->remote_op_id) { crm_debug("Scheduling '%s' action%s%s using %s for remote peer %s " "with op id %.8s and timeout %ds", cmd->action, (cmd->target == NULL)? "" : " targeting ", pcmk__s(cmd->target, ""), device->id, cmd->origin, cmd->remote_op_id, cmd->timeout); } else { crm_debug("Scheduling '%s' action%s%s using %s for %s with timeout %ds", cmd->action, (cmd->target == NULL)? "" : " targeting ", pcmk__s(cmd->target, ""), device->id, cmd->client, cmd->timeout); } device->pending_ops = g_list_append(device->pending_ops, cmd); mainloop_set_trigger(device->work); // Value -1 means disable any static/random fencing delays if (requested_delay < 0) { return; } delay_max = get_action_delay_max(device, cmd->action); delay_base = get_action_delay_base(device, cmd->action, cmd->target); if (delay_max == 0) { delay_max = delay_base; } if (delay_max < delay_base) { crm_warn(PCMK_STONITH_DELAY_BASE " (%ds) is larger than " PCMK_STONITH_DELAY_MAX " (%ds) for %s using %s " "(limiting to maximum delay)", delay_base, delay_max, cmd->action, device->id); delay_base = delay_max; } if (delay_max > 0) { cmd->start_delay += // coverity[dont_call] It doesn't matter here if rand() is predictable ((delay_max != delay_base)?(rand() % (delay_max - delay_base)):0) + delay_base; } if (cmd->start_delay > 0) { crm_notice("Delaying '%s' action%s%s using %s for %ds " QB_XS " timeout=%ds requested_delay=%ds base=%ds max=%ds", cmd->action, (cmd->target == NULL)? "" : " targeting ", pcmk__s(cmd->target, ""), device->id, cmd->start_delay, cmd->timeout, requested_delay, delay_base, delay_max); cmd->delay_id = pcmk__create_timer(cmd->start_delay * 1000, start_delay_helper, cmd); } } static void free_device(gpointer data) { GList *gIter = NULL; fenced_device_t *device = data; g_hash_table_destroy(device->params); g_hash_table_destroy(device->aliases); for (gIter = device->pending_ops; gIter != NULL; gIter = gIter->next) { async_command_t *cmd = gIter->data; crm_warn("Removal of device '%s' purged operation '%s'", device->id, cmd->action); report_internal_result(cmd, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE, "Device was removed before action could be executed"); } g_list_free(device->pending_ops); g_list_free_full(device->targets, free); if (device->timer) { mainloop_timer_stop(device->timer); mainloop_timer_del(device->timer); } mainloop_destroy_trigger(device->work); pcmk__xml_free(device->agent_metadata); free(device->namespace); if (device->on_target_actions != NULL) { g_string_free(device->on_target_actions, TRUE); } free(device->agent); free(device->id); free(device); } /*! * \internal * \brief Initialize the table of known fence devices */ void fenced_init_device_table(void) { if (device_table == NULL) { device_table = pcmk__strkey_table(NULL, free_device); } } /*! * \internal * \brief Free the table of known fence devices */ void fenced_free_device_table(void) { if (device_table != NULL) { g_hash_table_destroy(device_table); device_table = NULL; } } static GHashTable * build_port_aliases(const char *hostmap, GList ** targets) { char *name = NULL; int last = 0, lpc = 0, max = 0, added = 0; GHashTable *aliases = pcmk__strikey_table(free, free); if (hostmap == NULL) { return aliases; } max = strlen(hostmap); for (; lpc <= max; lpc++) { switch (hostmap[lpc]) { /* Skip escaped chars */ case '\\': lpc++; break; /* Assignment chars */ case '=': case ':': if (lpc > last) { free(name); name = pcmk__assert_alloc(1, 1 + lpc - last); memcpy(name, hostmap + last, lpc - last); } last = lpc + 1; break; /* Delimeter chars */ /* case ',': Potentially used to specify multiple ports */ case 0: case ';': case ' ': case '\t': if (name) { char *value = NULL; int k = 0; value = pcmk__assert_alloc(1, 1 + lpc - last); memcpy(value, hostmap + last, lpc - last); for (int i = 0; value[i] != '\0'; i++) { if (value[i] != '\\') { value[k++] = value[i]; } } value[k] = '\0'; crm_debug("Adding alias '%s'='%s'", name, value); g_hash_table_replace(aliases, name, value); if (targets) { *targets = g_list_append(*targets, pcmk__str_copy(value)); } value = NULL; name = NULL; added++; } else if (lpc > last) { crm_debug("Parse error at offset %d near '%s'", lpc - last, hostmap + last); } last = lpc + 1; break; } if (hostmap[lpc] == 0) { break; } } if (added == 0) { crm_info("No host mappings detected in '%s'", hostmap); } free(name); return aliases; } GHashTable *metadata_cache = NULL; void free_metadata_cache(void) { if (metadata_cache != NULL) { g_hash_table_destroy(metadata_cache); metadata_cache = NULL; } } static void init_metadata_cache(void) { if (metadata_cache == NULL) { metadata_cache = pcmk__strkey_table(free, free); } } int get_agent_metadata(const char *agent, xmlNode ** metadata) { char *buffer = NULL; if (metadata == NULL) { return EINVAL; } *metadata = NULL; if (pcmk__str_eq(agent, STONITH_WATCHDOG_AGENT_INTERNAL, pcmk__str_none)) { return pcmk_rc_ok; } init_metadata_cache(); buffer = g_hash_table_lookup(metadata_cache, agent); if (buffer == NULL) { stonith_t *st = stonith__api_new(); int rc; if (st == NULL) { crm_warn("Could not get agent meta-data: " "API memory allocation failed"); return EAGAIN; } rc = st->cmds->metadata(st, st_opt_sync_call, agent, NULL, &buffer, 10); stonith__api_free(st); if (rc || !buffer) { crm_err("Could not retrieve metadata for fencing agent %s", agent); return EAGAIN; } g_hash_table_replace(metadata_cache, pcmk__str_copy(agent), buffer); } *metadata = pcmk__xml_parse(buffer); return pcmk_rc_ok; } static void read_action_metadata(fenced_device_t *device) { xmlXPathObject *xpath = NULL; int max = 0; int lpc = 0; if (device->agent_metadata == NULL) { return; } xpath = pcmk__xpath_search(device->agent_metadata->doc, "//" PCMK_XE_ACTION); max = pcmk__xpath_num_results(xpath); if (max == 0) { xmlXPathFreeObject(xpath); return; } for (lpc = 0; lpc < max; lpc++) { const char *action = NULL; xmlNode *match = pcmk__xpath_result(xpath, lpc); CRM_LOG_ASSERT(match != NULL); if(match == NULL) { continue; }; action = crm_element_value(match, PCMK_XA_NAME); if (pcmk__str_eq(action, PCMK_ACTION_LIST, pcmk__str_none)) { fenced_device_set_flags(device, fenced_df_supports_list); } else if (pcmk__str_eq(action, PCMK_ACTION_STATUS, pcmk__str_none)) { fenced_device_set_flags(device, fenced_df_supports_status); } else if (pcmk__str_eq(action, PCMK_ACTION_REBOOT, pcmk__str_none)) { fenced_device_set_flags(device, fenced_df_supports_reboot); } else if (pcmk__str_eq(action, PCMK_ACTION_ON, pcmk__str_none)) { /* PCMK_XA_AUTOMATIC means the cluster will unfence a node when it * joins. * * @COMPAT PCMK__XA_REQUIRED is a deprecated synonym for * PCMK_XA_AUTOMATIC. */ if (pcmk__xe_attr_is_true(match, PCMK_XA_AUTOMATIC) || pcmk__xe_attr_is_true(match, PCMK__XA_REQUIRED)) { fenced_device_set_flags(device, fenced_df_auto_unfence); } fenced_device_set_flags(device, fenced_df_supports_on); } if ((action != NULL) && pcmk__xe_attr_is_true(match, PCMK_XA_ON_TARGET)) { pcmk__add_word(&(device->on_target_actions), 64, action); } } xmlXPathFreeObject(xpath); } static const char * target_list_type(fenced_device_t *dev) { const char *check_type = NULL; check_type = g_hash_table_lookup(dev->params, PCMK_STONITH_HOST_CHECK); if (check_type == NULL) { if (g_hash_table_lookup(dev->params, PCMK_STONITH_HOST_LIST)) { check_type = PCMK_VALUE_STATIC_LIST; } else if (g_hash_table_lookup(dev->params, PCMK_STONITH_HOST_MAP)) { check_type = PCMK_VALUE_STATIC_LIST; } else if (pcmk_is_set(dev->flags, fenced_df_supports_list)) { check_type = PCMK_VALUE_DYNAMIC_LIST; } else if (pcmk_is_set(dev->flags, fenced_df_supports_status)) { check_type = PCMK_VALUE_STATUS; } else { check_type = PCMK_VALUE_NONE; } } return check_type; } static fenced_device_t * build_device_from_xml(const xmlNode *dev) { const char *value; fenced_device_t *device = NULL; char *agent = crm_element_value_copy(dev, PCMK_XA_AGENT); CRM_CHECK(agent != NULL, return device); device = pcmk__assert_alloc(1, sizeof(fenced_device_t)); device->id = crm_element_value_copy(dev, PCMK_XA_ID); device->agent = agent; device->namespace = crm_element_value_copy(dev, PCMK__XA_NAMESPACE); device->params = xml2list(dev); value = g_hash_table_lookup(device->params, PCMK_STONITH_HOST_LIST); if (value) { device->targets = stonith__parse_targets(value); } value = g_hash_table_lookup(device->params, PCMK_STONITH_HOST_MAP); device->aliases = build_port_aliases(value, &(device->targets)); value = target_list_type(device); if (!pcmk__str_eq(value, PCMK_VALUE_STATIC_LIST, pcmk__str_casei) && (device->targets != NULL)) { // device->targets is necessary only with PCMK_VALUE_STATIC_LIST g_list_free_full(device->targets, free); device->targets = NULL; } switch (get_agent_metadata(device->agent, &device->agent_metadata)) { case pcmk_rc_ok: if (device->agent_metadata) { read_action_metadata(device); device->default_host_arg = stonith__default_host_arg(device->agent_metadata); } break; case EAGAIN: if (device->timer == NULL) { device->timer = mainloop_timer_add("get_agent_metadata", 10 * 1000, TRUE, get_agent_metadata_cb, device); } if (!mainloop_timer_running(device->timer)) { mainloop_timer_start(device->timer); } break; default: break; } value = crm_element_value(dev, PCMK__XA_RSC_PROVIDES); if (pcmk__str_eq(value, PCMK_VALUE_UNFENCING, pcmk__str_casei)) { fenced_device_set_flags(device, fenced_df_auto_unfence); } if (is_action_required(PCMK_ACTION_ON, device)) { crm_info("Fencing device '%s' requires unfencing", device->id); } if (device->on_target_actions != NULL) { crm_info("Fencing device '%s' requires actions (%s) to be executed " "on target", device->id, (const char *) device->on_target_actions->str); } device->work = mainloop_add_trigger(G_PRIORITY_HIGH, stonith_device_dispatch, device); return device; } static void schedule_internal_command(const char *origin, fenced_device_t *device, const char *action, const char *target, int timeout, void *internal_user_data, void (*done_cb) (int pid, const pcmk__action_result_t *result, void *user_data)) { async_command_t *cmd = NULL; cmd = pcmk__assert_alloc(1, sizeof(async_command_t)); cmd->id = -1; cmd->default_timeout = timeout ? timeout : 60; cmd->timeout = cmd->default_timeout; cmd->action = pcmk__str_copy(action); cmd->target = pcmk__str_copy(target); cmd->device = pcmk__str_copy(device->id); cmd->origin = pcmk__str_copy(origin); cmd->client = pcmk__str_copy(crm_system_name); cmd->client_name = pcmk__str_copy(crm_system_name); cmd->internal_user_data = internal_user_data; cmd->done_cb = done_cb; /* cmd, not internal_user_data, is passed to 'done_cb' as the userdata */ schedule_stonith_command(cmd, device); } // Fence agent status commands use custom exit status codes enum fence_status_code { fence_status_invalid = -1, fence_status_active = 0, fence_status_unknown = 1, fence_status_inactive = 2, }; static void status_search_cb(int pid, const pcmk__action_result_t *result, void *user_data) { async_command_t *cmd = user_data; struct device_search_s *search = cmd->internal_user_data; fenced_device_t *dev = cmd_device(cmd); gboolean can = FALSE; free_async_command(cmd); if (!dev) { search_devices_record_result(search, NULL, FALSE); return; } mainloop_set_trigger(dev->work); if (result->execution_status != PCMK_EXEC_DONE) { crm_warn("Assuming %s cannot fence %s " "because status could not be executed: %s%s%s%s", dev->id, search->host, pcmk_exec_status_str(result->execution_status), ((result->exit_reason == NULL)? "" : " ("), ((result->exit_reason == NULL)? "" : result->exit_reason), ((result->exit_reason == NULL)? "" : ")")); search_devices_record_result(search, dev->id, FALSE); return; } switch (result->exit_status) { case fence_status_unknown: crm_trace("%s reported it cannot fence %s", dev->id, search->host); break; case fence_status_active: case fence_status_inactive: crm_trace("%s reported it can fence %s", dev->id, search->host); can = TRUE; break; default: crm_warn("Assuming %s cannot fence %s " "(status returned unknown code %d)", dev->id, search->host, result->exit_status); break; } search_devices_record_result(search, dev->id, can); } static void dynamic_list_search_cb(int pid, const pcmk__action_result_t *result, void *user_data) { async_command_t *cmd = user_data; struct device_search_s *search = cmd->internal_user_data; fenced_device_t *dev = cmd_device(cmd); gboolean can_fence = FALSE; free_async_command(cmd); /* Host/alias must be in the list output to be eligible to be fenced * * Will cause problems if down'd nodes aren't listed or (for virtual nodes) * if the guest is still listed despite being moved to another machine */ if (!dev) { search_devices_record_result(search, NULL, FALSE); return; } mainloop_set_trigger(dev->work); if (pcmk__result_ok(result)) { crm_info("Refreshing target list for %s", dev->id); g_list_free_full(dev->targets, free); dev->targets = stonith__parse_targets(result->action_stdout); dev->targets_age = time(NULL); } else if (dev->targets != NULL) { if (result->execution_status == PCMK_EXEC_DONE) { crm_info("Reusing most recent target list for %s " "because list returned error code %d", dev->id, result->exit_status); } else { crm_info("Reusing most recent target list for %s " "because list could not be executed: %s%s%s%s", dev->id, pcmk_exec_status_str(result->execution_status), ((result->exit_reason == NULL)? "" : " ("), ((result->exit_reason == NULL)? "" : result->exit_reason), ((result->exit_reason == NULL)? "" : ")")); } } else { // We have never successfully executed list if (result->execution_status == PCMK_EXEC_DONE) { crm_warn("Assuming %s cannot fence %s " "because list returned error code %d", dev->id, search->host, result->exit_status); } else { crm_warn("Assuming %s cannot fence %s " "because list could not be executed: %s%s%s%s", dev->id, search->host, pcmk_exec_status_str(result->execution_status), ((result->exit_reason == NULL)? "" : " ("), ((result->exit_reason == NULL)? "" : result->exit_reason), ((result->exit_reason == NULL)? "" : ")")); } /* Fall back to pcmk_host_check=PCMK_VALUE_STATUS if the user didn't * explicitly specify PCMK_VALUE_DYNAMIC_LIST */ if (g_hash_table_lookup(dev->params, PCMK_STONITH_HOST_CHECK) == NULL) { crm_notice("Switching to pcmk_host_check='status' for %s", dev->id); pcmk__insert_dup(dev->params, PCMK_STONITH_HOST_CHECK, PCMK_VALUE_STATUS); } } if (dev->targets) { const char *alias = g_hash_table_lookup(dev->aliases, search->host); if (!alias) { alias = search->host; } if (pcmk__str_in_list(alias, dev->targets, pcmk__str_casei)) { can_fence = TRUE; } } search_devices_record_result(search, dev->id, can_fence); } /*! * \internal * \brief Returns true if any key in first is not in second or second has a different value for key */ static int device_params_diff(GHashTable *first, GHashTable *second) { char *key = NULL; char *value = NULL; GHashTableIter gIter; g_hash_table_iter_init(&gIter, first); while (g_hash_table_iter_next(&gIter, (void **)&key, (void **)&value)) { if(strstr(key, "CRM_meta") == key) { continue; } else if (strcmp(key, PCMK_XA_CRM_FEATURE_SET) == 0) { continue; } else { char *other_value = g_hash_table_lookup(second, key); if (!other_value || !pcmk__str_eq(other_value, value, pcmk__str_casei)) { crm_trace("Different value for %s: %s != %s", key, other_value, value); return 1; } } } return 0; } /*! * \internal * \brief Checks to see if an identical device already exists in the table */ static fenced_device_t * device_has_duplicate(const fenced_device_t *device) { fenced_device_t *dup = g_hash_table_lookup(device_table, device->id); if (!dup) { crm_trace("No match for %s", device->id); return NULL; } else if (!pcmk__str_eq(dup->agent, device->agent, pcmk__str_casei)) { crm_trace("Different agent: %s != %s", dup->agent, device->agent); return NULL; } // Find a way to share logic with pcmk__digest_op_params() here? if (device_params_diff(device->params, dup->params) || device_params_diff(dup->params, device->params)) { return NULL; } crm_trace("Match"); return dup; } int fenced_device_register(const xmlNode *dev, bool from_cib) { const char *local_node_name = fenced_get_local_node(); fenced_device_t *dup = NULL; fenced_device_t *device = build_device_from_xml(dev); int rc = pcmk_rc_ok; CRM_CHECK(device != NULL, return ENOMEM); /* do we have a watchdog-device? */ if (pcmk__str_eq(device->id, STONITH_WATCHDOG_ID, pcmk__str_none) || pcmk__str_any_of(device->agent, STONITH_WATCHDOG_AGENT, STONITH_WATCHDOG_AGENT_INTERNAL, NULL)) { if (stonith_watchdog_timeout_ms <= 0) { crm_err("Ignoring watchdog fence device without " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " set"); rc = ENODEV; goto done; } if (!pcmk__str_any_of(device->agent, STONITH_WATCHDOG_AGENT, STONITH_WATCHDOG_AGENT_INTERNAL, NULL)) { crm_err("Ignoring watchdog fence device with unknown agent '%s' " "rather than '" STONITH_WATCHDOG_AGENT "'", pcmk__s(device->agent, "")); rc = ENODEV; goto done; } if (!pcmk__str_eq(device->id, STONITH_WATCHDOG_ID, pcmk__str_none)) { crm_err("Ignoring watchdog fence device named '%s' rather than " "'" STONITH_WATCHDOG_ID "'", pcmk__s(device->id, "")); rc = ENODEV; goto done; } if (pcmk__str_eq(device->agent, STONITH_WATCHDOG_AGENT, pcmk__str_none)) { /* This has either an empty list or the targets configured for * watchdog fencing */ g_list_free_full(stonith_watchdog_targets, free); stonith_watchdog_targets = device->targets; device->targets = NULL; } if (!node_does_watchdog_fencing(local_node_name)) { crm_debug("Skip registration of watchdog fence device on node not " "in host list"); device->targets = NULL; stonith_device_remove(device->id, from_cib); goto done; } // Proceed as with any other fencing device g_list_free_full(device->targets, free); device->targets = stonith__parse_targets(local_node_name); pcmk__insert_dup(device->params, PCMK_STONITH_HOST_LIST, local_node_name); } dup = device_has_duplicate(device); if (dup != NULL) { guint ndevices = g_hash_table_size(device_table); crm_debug("Device '%s' already in device list (%d active device%s)", device->id, ndevices, pcmk__plural_s(ndevices)); free_device(device); device = dup; fenced_device_clear_flags(device, fenced_df_dirty); } else { guint ndevices = 0; fenced_device_t *old = g_hash_table_lookup(device_table, device->id); if (from_cib && (old != NULL) && pcmk_is_set(old->flags, fenced_df_api_registered)) { /* If the CIB is writing over an entry that is shared with a stonith * client, copy any pending ops that currently exist on the old * entry to the new one. Otherwise the pending ops will be reported * as failures. */ crm_info("Overwriting existing entry for %s from CIB", device->id); device->pending_ops = old->pending_ops; fenced_device_set_flags(device, fenced_df_api_registered); old->pending_ops = NULL; if (device->pending_ops != NULL) { mainloop_set_trigger(device->work); } } g_hash_table_replace(device_table, device->id, device); ndevices = g_hash_table_size(device_table); crm_notice("Added '%s' to device list (%d active device%s)", device->id, ndevices, pcmk__plural_s(ndevices)); } if (from_cib) { fenced_device_set_flags(device, fenced_df_cib_registered); } else { fenced_device_set_flags(device, fenced_df_api_registered); } done: if (rc != pcmk_rc_ok) { free_device(device); } return rc; } void stonith_device_remove(const char *id, bool from_cib) { fenced_device_t *device = g_hash_table_lookup(device_table, id); guint ndevices = 0; if (device == NULL) { ndevices = g_hash_table_size(device_table); crm_info("Device '%s' not found (%u active device%s)", id, ndevices, pcmk__plural_s(ndevices)); return; } if (from_cib) { fenced_device_clear_flags(device, fenced_df_cib_registered); } else { fenced_device_clear_flags(device, fenced_df_api_registered|fenced_df_verified); } if (!pcmk_any_flags_set(device->flags, fenced_df_api_registered |fenced_df_cib_registered)) { g_hash_table_remove(device_table, id); ndevices = g_hash_table_size(device_table); crm_info("Removed '%s' from device list (%u active device%s)", id, ndevices, pcmk__plural_s(ndevices)); } else { // Exactly one is true at this point const bool cib_registered = pcmk_is_set(device->flags, fenced_df_cib_registered); crm_trace("Not removing '%s' from device list (%u active) because " "still registered via %s", id, g_hash_table_size(device_table), (cib_registered? "CIB" : "API")); } } /*! * \internal * \brief Return the number of stonith levels registered for a node * * \param[in] tp Node's topology table entry * * \return Number of non-NULL levels in topology entry * \note This function is used only for log messages. */ static int count_active_levels(const stonith_topology_t *tp) { int lpc = 0; int count = 0; for (lpc = 0; lpc < ST__LEVEL_COUNT; lpc++) { if (tp->levels[lpc] != NULL) { count++; } } return count; } static void free_topology_entry(gpointer data) { stonith_topology_t *tp = data; int lpc = 0; for (lpc = 0; lpc < ST__LEVEL_COUNT; lpc++) { if (tp->levels[lpc] != NULL) { g_list_free_full(tp->levels[lpc], free); } } free(tp->target); free(tp->target_value); free(tp->target_pattern); free(tp->target_attribute); free(tp); } void free_topology_list(void) { if (topology != NULL) { g_hash_table_destroy(topology); topology = NULL; } } void init_topology_list(void) { if (topology == NULL) { topology = pcmk__strkey_table(NULL, free_topology_entry); } } char * stonith_level_key(const xmlNode *level, enum fenced_target_by mode) { if (mode == fenced_target_by_unknown) { mode = unpack_level_kind(level); } switch (mode) { case fenced_target_by_name: return crm_element_value_copy(level, PCMK_XA_TARGET); case fenced_target_by_pattern: return crm_element_value_copy(level, PCMK_XA_TARGET_PATTERN); case fenced_target_by_attribute: return crm_strdup_printf("%s=%s", crm_element_value(level, PCMK_XA_TARGET_ATTRIBUTE), crm_element_value(level, PCMK_XA_TARGET_VALUE)); default: return crm_strdup_printf("unknown-%s", pcmk__xe_id(level)); } } /*! * \internal * \brief Parse target identification from topology level XML * * \param[in] level Topology level XML to parse * * \return How to identify target of \p level */ static enum fenced_target_by unpack_level_kind(const xmlNode *level) { if (crm_element_value(level, PCMK_XA_TARGET) != NULL) { return fenced_target_by_name; } if (crm_element_value(level, PCMK_XA_TARGET_PATTERN) != NULL) { return fenced_target_by_pattern; } if ((crm_element_value(level, PCMK_XA_TARGET_ATTRIBUTE) != NULL) && (crm_element_value(level, PCMK_XA_TARGET_VALUE) != NULL)) { return fenced_target_by_attribute; } return fenced_target_by_unknown; } /*! * \internal * \brief Unpack essential information from topology request XML * * \param[in] xml Request XML to search * \param[out] mode If not NULL, where to store level kind * \param[out] target If not NULL, where to store representation of target * \param[out] id If not NULL, where to store level number * * \return Topology level XML from within \p xml, or NULL if not found * \note The caller is responsible for freeing \p *target if set. */ static xmlNode * unpack_level_request(xmlNode *xml, enum fenced_target_by *mode, char **target, int *id) { enum fenced_target_by local_mode = fenced_target_by_unknown; char *local_target = NULL; int local_id = 0; /* The level element can be the top element or lower. If top level, don't * search by xpath, because it might give multiple hits if the XML is the * CIB. */ if ((xml != NULL) && !pcmk__xe_is(xml, PCMK_XE_FENCING_LEVEL)) { xml = pcmk__xpath_find_one(xml->doc, "//" PCMK_XE_FENCING_LEVEL, LOG_WARNING); } if (xml != NULL) { local_mode = unpack_level_kind(xml); local_target = stonith_level_key(xml, local_mode); crm_element_value_int(xml, PCMK_XA_INDEX, &local_id); } if (mode != NULL) { *mode = local_mode; } if (id != NULL) { *id = local_id; } if (target != NULL) { *target = local_target; } else { free(local_target); } return xml; } /*! * \internal * \brief Register a fencing topology level for a target * * Given an XML request specifying the target name, level index, and device IDs * for the level, this will create an entry for the target in the global topology * table if one does not already exist, then append the specified device IDs to * the entry's device list for the specified level. * * \param[in] msg XML request for STONITH level registration * \param[out] result Where to set result of registration (can be \c NULL) */ void fenced_register_level(xmlNode *msg, pcmk__action_result_t *result) { int id = 0; xmlNode *level; enum fenced_target_by mode; char *target; stonith_topology_t *tp; const char *value = NULL; CRM_CHECK(msg != NULL, return); level = unpack_level_request(msg, &mode, &target, &id); if (level == NULL) { set_bad_request_result(result); return; } // Ensure an ID was given (even the client API adds an ID) if (pcmk__str_empty(pcmk__xe_id(level))) { crm_warn("Ignoring registration for topology level without ID"); free(target); crm_log_xml_trace(level, "Bad level"); pcmk__format_result(result, CRM_EX_INVALID_PARAM, PCMK_EXEC_INVALID, "Topology level is invalid without ID"); return; } // Ensure a valid target was specified if (mode == fenced_target_by_unknown) { crm_warn("Ignoring registration for topology level '%s' " "without valid target", pcmk__xe_id(level)); free(target); crm_log_xml_trace(level, "Bad level"); pcmk__format_result(result, CRM_EX_INVALID_PARAM, PCMK_EXEC_INVALID, "Invalid target for topology level '%s'", pcmk__xe_id(level)); return; } // Ensure level ID is in allowed range if ((id < ST__LEVEL_MIN) || (id > ST__LEVEL_MAX)) { crm_warn("Ignoring topology registration for %s with invalid level %d", target, id); free(target); crm_log_xml_trace(level, "Bad level"); pcmk__format_result(result, CRM_EX_INVALID_PARAM, PCMK_EXEC_INVALID, "Invalid level number '%s' for topology level '%s'", pcmk__s(crm_element_value(level, PCMK_XA_INDEX), ""), pcmk__xe_id(level)); return; } /* Find or create topology table entry */ tp = g_hash_table_lookup(topology, target); if (tp == NULL) { tp = pcmk__assert_alloc(1, sizeof(stonith_topology_t)); tp->kind = mode; tp->target = target; tp->target_value = crm_element_value_copy(level, PCMK_XA_TARGET_VALUE); tp->target_pattern = crm_element_value_copy(level, PCMK_XA_TARGET_PATTERN); tp->target_attribute = crm_element_value_copy(level, PCMK_XA_TARGET_ATTRIBUTE); g_hash_table_replace(topology, tp->target, tp); crm_trace("Added %s (%d) to the topology (%d active entries)", target, (int) mode, g_hash_table_size(topology)); } else { free(target); } if (tp->levels[id] != NULL) { crm_info("Adding to the existing %s[%d] topology entry", tp->target, id); } value = crm_element_value(level, PCMK_XA_DEVICES); if (value != NULL) { /* Empty string and whitespace are not possible with schema validation * enabled. Don't bother handling them specially here. */ gchar **devices = g_strsplit(value, ",", 0); for (char **dev = devices; (dev != NULL) && (*dev != NULL); dev++) { crm_trace("Adding device '%s' for %s[%d]", *dev, tp->target, id); tp->levels[id] = g_list_append(tp->levels[id], pcmk__str_copy(*dev)); } g_strfreev(devices); } { int nlevels = count_active_levels(tp); crm_info("Target %s has %d active fencing level%s", tp->target, nlevels, pcmk__plural_s(nlevels)); } pcmk__set_result(result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); } /*! * \internal * \brief Unregister a fencing topology level for a target * * Given an XML request specifying the target name and level index (or 0 for all * levels), this will remove any corresponding entry for the target from the * global topology table. * * \param[in] msg XML request for STONITH level registration * \param[out] result Where to set result of unregistration (can be \c NULL) */ void fenced_unregister_level(xmlNode *msg, pcmk__action_result_t *result) { int id = -1; stonith_topology_t *tp; char *target; xmlNode *level = NULL; level = unpack_level_request(msg, NULL, &target, &id); if (level == NULL) { set_bad_request_result(result); return; } // Ensure level ID is in allowed range if ((id < 0) || (id >= ST__LEVEL_COUNT)) { crm_warn("Ignoring topology unregistration for %s with invalid level %d", target, id); free(target); crm_log_xml_trace(level, "Bad level"); pcmk__format_result(result, CRM_EX_INVALID_PARAM, PCMK_EXEC_INVALID, "Invalid level number '%s' for topology level %s", pcmk__s(crm_element_value(level, PCMK_XA_INDEX), ""), // Client API doesn't add ID to unregistration XML pcmk__s(pcmk__xe_id(level), "")); return; } tp = g_hash_table_lookup(topology, target); if (tp == NULL) { guint nentries = g_hash_table_size(topology); crm_info("No fencing topology found for %s (%d active %s)", target, nentries, pcmk__plural_alt(nentries, "entry", "entries")); } else if (id == 0 && g_hash_table_remove(topology, target)) { guint nentries = g_hash_table_size(topology); crm_info("Removed all fencing topology entries related to %s " "(%d active %s remaining)", target, nentries, pcmk__plural_alt(nentries, "entry", "entries")); } else if (tp->levels[id] != NULL) { guint nlevels; g_list_free_full(tp->levels[id], free); tp->levels[id] = NULL; nlevels = count_active_levels(tp); crm_info("Removed level %d from fencing topology for %s " "(%d active level%s remaining)", id, target, nlevels, pcmk__plural_s(nlevels)); } free(target); pcmk__set_result(result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); } static char * list_to_string(GList *list, const char *delim, gboolean terminate_with_delim) { int max = g_list_length(list); size_t delim_len = delim?strlen(delim):0; size_t alloc_size = 1 + (max?((max-1+(terminate_with_delim?1:0))*delim_len):0); char *rv; GList *gIter; char *pos = NULL; const char *lead_delim = ""; for (gIter = list; gIter != NULL; gIter = gIter->next) { const char *value = (const char *) gIter->data; alloc_size += strlen(value); } rv = pcmk__assert_alloc(alloc_size, sizeof(char)); pos = rv; for (gIter = list; gIter != NULL; gIter = gIter->next) { const char *value = (const char *) gIter->data; pos = &pos[sprintf(pos, "%s%s", lead_delim, value)]; lead_delim = delim; } if (max && terminate_with_delim) { sprintf(pos, "%s", delim); } return rv; } /*! * \internal * \brief Execute a fence agent action directly (and asynchronously) * * Handle a STONITH_OP_EXEC API message by scheduling a requested agent action * directly on a specified device. Only list, monitor, and status actions are * expected to use this call, though it should work with any agent command. * * \param[in] msg Request XML specifying action * \param[out] result Where to store result of action * * \note If the action is monitor, the device must be registered via the API * (CIB registration is not sufficient), because monitor should not be * possible unless the device is "started" (API registered). */ static void execute_agent_action(xmlNode *msg, pcmk__action_result_t *result) { xmlNode *dev = pcmk__xpath_find_one(msg->doc, "//" PCMK__XE_ST_DEVICE_ID, LOG_ERR); xmlNode *op = pcmk__xpath_find_one(msg->doc, "//*[@" PCMK__XA_ST_DEVICE_ACTION "]", LOG_ERR); const char *id = crm_element_value(dev, PCMK__XA_ST_DEVICE_ID); const char *action = crm_element_value(op, PCMK__XA_ST_DEVICE_ACTION); async_command_t *cmd = NULL; fenced_device_t *device = NULL; if ((id == NULL) || (action == NULL)) { crm_info("Malformed API action request: device %s, action %s", (id? id : "not specified"), (action? action : "not specified")); set_bad_request_result(result); return; } if (pcmk__str_eq(id, STONITH_WATCHDOG_ID, pcmk__str_none)) { // Watchdog agent actions are implemented internally if (stonith_watchdog_timeout_ms <= 0) { pcmk__set_result(result, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE, "Watchdog fence device not configured"); return; } else if (pcmk__str_eq(action, PCMK_ACTION_LIST, pcmk__str_none)) { pcmk__set_result(result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); pcmk__set_result_output(result, list_to_string(stonith_watchdog_targets, "\n", TRUE), NULL); return; } else if (pcmk__str_eq(action, PCMK_ACTION_MONITOR, pcmk__str_none)) { pcmk__set_result(result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); return; } } device = g_hash_table_lookup(device_table, id); if (device == NULL) { crm_info("Ignoring API '%s' action request because device %s not found", action, id); pcmk__format_result(result, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE, "'%s' not found", id); return; } else if (!pcmk_is_set(device->flags, fenced_df_api_registered) && (strcmp(action, PCMK_ACTION_MONITOR) == 0)) { // Monitors may run only on "started" (API-registered) devices crm_info("Ignoring API '%s' action request because device %s not active", action, id); pcmk__format_result(result, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE, "'%s' not active", id); return; } cmd = create_async_command(msg); if (cmd == NULL) { crm_log_xml_warn(msg, "invalid"); set_bad_request_result(result); return; } schedule_stonith_command(cmd, device); pcmk__set_result(result, CRM_EX_OK, PCMK_EXEC_PENDING, NULL); } static void search_devices_record_result(struct device_search_s *search, const char *device, gboolean can_fence) { search->replies_received++; if (can_fence && device) { if (search->support_action_only != fenced_df_none) { fenced_device_t *dev = g_hash_table_lookup(device_table, device); if (dev && !pcmk_is_set(dev->flags, search->support_action_only)) { return; } } search->capable = g_list_append(search->capable, pcmk__str_copy(device)); } if (search->replies_needed == search->replies_received) { guint ndevices = g_list_length(search->capable); crm_debug("Search found %d device%s that can perform '%s' targeting %s", ndevices, pcmk__plural_s(ndevices), (search->action? search->action : "unknown action"), (search->host? search->host : "any node")); search->callback(search->capable, search->user_data); free(search->host); free(search->action); free(search); } } /*! * \internal * \brief Check whether the local host is allowed to execute a fencing action * * \param[in] device Fence device to check * \param[in] action Fence action to check * \param[in] target Hostname of fence target * \param[in] allow_self Whether self-fencing is allowed for this operation * * \return TRUE if local host is allowed to execute action, FALSE otherwise */ static gboolean localhost_is_eligible(const fenced_device_t *device, const char *action, const char *target, gboolean allow_self) { gboolean localhost_is_target = pcmk__str_eq(target, fenced_get_local_node(), pcmk__str_casei); if ((device != NULL) && (action != NULL) && (device->on_target_actions != NULL) && (strstr((const char*) device->on_target_actions->str, action) != NULL)) { if (!localhost_is_target) { crm_trace("Operation '%s' using %s can only be executed for local " "host, not %s", action, device->id, target); return FALSE; } } else if (localhost_is_target && !allow_self) { crm_trace("'%s' operation does not support self-fencing", action); return FALSE; } return TRUE; } /*! * \internal * \brief Check if local node is allowed to execute (possibly remapped) action * * \param[in] device Fence device to check * \param[in] action Fence action to check * \param[in] target Node name of fence target * \param[in] allow_self Whether self-fencing is allowed for this operation * * \return true if local node is allowed to execute \p action or any actions it * might be remapped to, otherwise false */ static bool localhost_is_eligible_with_remap(const fenced_device_t *device, const char *action, const char *target, gboolean allow_self) { // Check exact action if (localhost_is_eligible(device, action, target, allow_self)) { return true; } // Check potential remaps if (pcmk__str_eq(action, PCMK_ACTION_REBOOT, pcmk__str_none)) { /* "reboot" might get remapped to "off" then "on", so even if reboot is * disallowed, return true if either of those is allowed. We'll report * the disallowed actions with the results. We never allow self-fencing * for remapped "on" actions because the target is off at that point. */ if (localhost_is_eligible(device, PCMK_ACTION_OFF, target, allow_self) || localhost_is_eligible(device, PCMK_ACTION_ON, target, FALSE)) { return true; } } return false; } /*! * \internal * \brief Check whether we can use a device's cached target list * * \param[in] dev Fencing device to check * * \return \c true if \p dev cached its targets less than a minute ago, * otherwise \c false */ static inline bool can_use_target_cache(const fenced_device_t *dev) { return (dev->targets != NULL) && (time(NULL) < (dev->targets_age + 60)); } static void can_fence_host_with_device(fenced_device_t *dev, struct device_search_s *search) { gboolean can = FALSE; const char *check_type = "Internal bug"; const char *target = NULL; const char *alias = NULL; const char *dev_id = "Unspecified device"; const char *action = (search == NULL)? NULL : search->action; CRM_CHECK((dev != NULL) && (action != NULL), goto search_report_results); if (dev->id != NULL) { dev_id = dev->id; } target = search->host; if (target == NULL) { can = TRUE; check_type = "No target"; goto search_report_results; } /* Answer immediately if the device does not support the action * or the local node is not allowed to perform it */ if (pcmk__str_eq(action, PCMK_ACTION_ON, pcmk__str_none) && !pcmk_is_set(dev->flags, fenced_df_supports_on)) { check_type = "Agent does not support 'on'"; goto search_report_results; } else if (!localhost_is_eligible_with_remap(dev, action, target, search->allow_self)) { check_type = "This node is not allowed to execute action"; goto search_report_results; } // Check eligibility as specified by pcmk_host_check check_type = target_list_type(dev); alias = g_hash_table_lookup(dev->aliases, target); if (pcmk__str_eq(check_type, PCMK_VALUE_NONE, pcmk__str_casei)) { can = TRUE; } else if (pcmk__str_eq(check_type, PCMK_VALUE_STATIC_LIST, pcmk__str_casei)) { if (pcmk__str_in_list(target, dev->targets, pcmk__str_casei)) { can = TRUE; } else if (g_hash_table_lookup(dev->params, PCMK_STONITH_HOST_MAP) && g_hash_table_lookup(dev->aliases, target)) { can = TRUE; } } else if (pcmk__str_eq(check_type, PCMK_VALUE_DYNAMIC_LIST, pcmk__str_casei)) { if (!can_use_target_cache(dev)) { int device_timeout = get_action_timeout(dev, PCMK_ACTION_LIST, search->per_device_timeout); if (device_timeout > search->per_device_timeout) { crm_notice("Since the pcmk_list_timeout (%ds) parameter of %s " "is larger than " PCMK_OPT_STONITH_TIMEOUT " (%ds), timeout may occur", device_timeout, dev_id, search->per_device_timeout); } crm_trace("Running '%s' to check whether %s is eligible to fence %s (%s)", check_type, dev_id, target, action); schedule_internal_command(__func__, dev, PCMK_ACTION_LIST, NULL, search->per_device_timeout, search, dynamic_list_search_cb); /* we'll respond to this search request async in the cb */ return; } if (pcmk__str_in_list(((alias == NULL)? target : alias), dev->targets, pcmk__str_casei)) { can = TRUE; } } else if (pcmk__str_eq(check_type, PCMK_VALUE_STATUS, pcmk__str_casei)) { int device_timeout = get_action_timeout(dev, check_type, search->per_device_timeout); if (device_timeout > search->per_device_timeout) { crm_notice("Since the pcmk_status_timeout (%ds) parameter of %s is " "larger than " PCMK_OPT_STONITH_TIMEOUT " (%ds), " "timeout may occur", device_timeout, dev_id, search->per_device_timeout); } crm_trace("Running '%s' to check whether %s is eligible to fence %s (%s)", check_type, dev_id, target, action); schedule_internal_command(__func__, dev, PCMK_ACTION_STATUS, target, search->per_device_timeout, search, status_search_cb); /* we'll respond to this search request async in the cb */ return; } else { crm_err("Invalid value for " PCMK_STONITH_HOST_CHECK ": %s", check_type); check_type = "Invalid " PCMK_STONITH_HOST_CHECK; } search_report_results: crm_info("%s is%s eligible to fence (%s) %s%s%s%s: %s", dev_id, (can? "" : " not"), pcmk__s(action, "unspecified action"), pcmk__s(target, "unspecified target"), (alias == NULL)? "" : " (as '", pcmk__s(alias, ""), (alias == NULL)? "" : "')", check_type); search_devices_record_result(search, ((dev == NULL)? NULL : dev_id), can); } static void search_devices(gpointer key, gpointer value, gpointer user_data) { fenced_device_t *dev = value; struct device_search_s *search = user_data; can_fence_host_with_device(dev, search); } #define DEFAULT_QUERY_TIMEOUT 20 static void get_capable_devices(const char *host, const char *action, int timeout, bool allow_self, void *user_data, void (*callback) (GList * devices, void *user_data), uint32_t support_action_only) { struct device_search_s *search; guint ndevices = g_hash_table_size(device_table); if (ndevices == 0) { callback(NULL, user_data); return; } search = pcmk__assert_alloc(1, sizeof(struct device_search_s)); search->host = pcmk__str_copy(host); search->action = pcmk__str_copy(action); search->per_device_timeout = timeout; search->allow_self = allow_self; search->callback = callback; search->user_data = user_data; search->support_action_only = support_action_only; /* We are guaranteed this many replies, even if a device is * unregistered while the search is in progress. */ search->replies_needed = ndevices; crm_debug("Searching %d device%s to see which can execute '%s' targeting %s", ndevices, pcmk__plural_s(ndevices), (search->action? search->action : "unknown action"), (search->host? search->host : "any node")); fenced_foreach_device(search_devices, search); } struct st_query_data { xmlNode *reply; char *remote_peer; char *client_id; char *target; char *action; int call_options; }; /*! * \internal * \brief Add action-specific attributes to query reply XML * * \param[in,out] xml XML to add attributes to * \param[in] action Fence action * \param[in] device Fence device * \param[in] target Fence target */ static void add_action_specific_attributes(xmlNode *xml, const char *action, const fenced_device_t *device, const char *target) { int action_specific_timeout; int delay_max; int delay_base; CRM_CHECK(xml && action && device, return); // PCMK__XA_ST_REQUIRED is currently used only for unfencing if (is_action_required(action, device)) { crm_trace("Action '%s' is required using %s", action, device->id); crm_xml_add_int(xml, PCMK__XA_ST_REQUIRED, 1); } // pcmk__timeout if configured action_specific_timeout = get_action_timeout(device, action, 0); if (action_specific_timeout) { crm_trace("Action '%s' has timeout %ds using %s", action, action_specific_timeout, device->id); crm_xml_add_int(xml, PCMK__XA_ST_ACTION_TIMEOUT, action_specific_timeout); } delay_max = get_action_delay_max(device, action); if (delay_max > 0) { crm_trace("Action '%s' has maximum random delay %ds using %s", action, delay_max, device->id); crm_xml_add_int(xml, PCMK__XA_ST_DELAY_MAX, delay_max); } delay_base = get_action_delay_base(device, action, target); if (delay_base > 0) { crm_xml_add_int(xml, PCMK__XA_ST_DELAY_BASE, delay_base); } if ((delay_max > 0) && (delay_base == 0)) { crm_trace("Action '%s' has maximum random delay %ds using %s", action, delay_max, device->id); } else if ((delay_max == 0) && (delay_base > 0)) { crm_trace("Action '%s' has a static delay of %ds using %s", action, delay_base, device->id); } else if ((delay_max > 0) && (delay_base > 0)) { crm_trace("Action '%s' has a minimum delay of %ds and a randomly chosen " "maximum delay of %ds using %s", action, delay_base, delay_max, device->id); } } /*! * \internal * \brief Add "disallowed" attribute to query reply XML if appropriate * * \param[in,out] xml XML to add attribute to * \param[in] action Fence action * \param[in] device Fence device * \param[in] target Fence target * \param[in] allow_self Whether self-fencing is allowed */ static void add_disallowed(xmlNode *xml, const char *action, const fenced_device_t *device, const char *target, gboolean allow_self) { if (!localhost_is_eligible(device, action, target, allow_self)) { crm_trace("Action '%s' using %s is disallowed for local host", action, device->id); pcmk__xe_set_bool_attr(xml, PCMK__XA_ST_ACTION_DISALLOWED, true); } } /*! * \internal * \brief Add child element with action-specific values to query reply XML * * \param[in,out] xml XML to add attribute to * \param[in] action Fence action * \param[in] device Fence device * \param[in] target Fence target * \param[in] allow_self Whether self-fencing is allowed */ static void add_action_reply(xmlNode *xml, const char *action, const fenced_device_t *device, const char *target, gboolean allow_self) { xmlNode *child = pcmk__xe_create(xml, PCMK__XE_ST_DEVICE_ACTION); crm_xml_add(child, PCMK_XA_ID, action); add_action_specific_attributes(child, action, device, target); add_disallowed(child, action, device, target, allow_self); } /*! * \internal * \brief Send a reply to a CPG peer or IPC client * * \param[in] reply XML reply to send * \param[in] call_options Send synchronously if st_opt_sync_call is set * \param[in] remote_peer If not NULL, name of peer node to send CPG reply * \param[in,out] client If not NULL, client to send IPC reply */ static void stonith_send_reply(const xmlNode *reply, int call_options, const char *remote_peer, pcmk__client_t *client) { CRM_CHECK((reply != NULL) && ((remote_peer != NULL) || (client != NULL)), return); if (remote_peer == NULL) { do_local_reply(reply, client, call_options); } else { const pcmk__node_status_t *node = pcmk__get_node(0, remote_peer, NULL, pcmk__node_search_cluster_member); pcmk__cluster_send_message(node, pcmk_ipc_fenced, reply); } } static void stonith_query_capable_device_cb(GList * devices, void *user_data) { struct st_query_data *query = user_data; int available_devices = 0; xmlNode *wrapper = NULL; xmlNode *list = NULL; GList *lpc = NULL; pcmk__client_t *client = NULL; if (query->client_id != NULL) { client = pcmk__find_client_by_id(query->client_id); if ((client == NULL) && (query->remote_peer == NULL)) { crm_trace("Skipping reply to %s: no longer a client", query->client_id); goto done; } } // Pack the results into XML wrapper = pcmk__xe_create(query->reply, PCMK__XE_ST_CALLDATA); list = pcmk__xe_create(wrapper, __func__); crm_xml_add(list, PCMK__XA_ST_TARGET, query->target); for (lpc = devices; lpc != NULL; lpc = lpc->next) { fenced_device_t *device = g_hash_table_lookup(device_table, lpc->data); const char *action = query->action; xmlNode *dev = NULL; if (!device) { /* It is possible the device got unregistered while * determining who can fence the target */ continue; } available_devices++; dev = pcmk__xe_create(list, PCMK__XE_ST_DEVICE_ID); crm_xml_add(dev, PCMK_XA_ID, device->id); crm_xml_add(dev, PCMK__XA_NAMESPACE, device->namespace); crm_xml_add(dev, PCMK_XA_AGENT, device->agent); // Has had successful monitor, list, or status on this node crm_xml_add_int(dev, PCMK__XA_ST_MONITOR_VERIFIED, pcmk_is_set(device->flags, fenced_df_verified)); crm_xml_add_int(dev, PCMK__XA_ST_DEVICE_SUPPORT_FLAGS, device->flags); /* If the originating fencer wants to reboot the node, and we have a * capable device that doesn't support "reboot", remap to "off" instead. */ if (!pcmk_is_set(device->flags, fenced_df_supports_reboot) && pcmk__str_eq(query->action, PCMK_ACTION_REBOOT, pcmk__str_none)) { crm_trace("%s doesn't support reboot, using values for off instead", device->id); action = PCMK_ACTION_OFF; } /* Add action-specific values if available */ add_action_specific_attributes(dev, action, device, query->target); if (pcmk__str_eq(query->action, PCMK_ACTION_REBOOT, pcmk__str_none)) { /* A "reboot" *might* get remapped to "off" then "on", so after * sending the "reboot"-specific values in the main element, we add * sub-elements for "off" and "on" values. * * We short-circuited earlier if "reboot", "off" and "on" are all * disallowed for the local host. However if only one or two are * disallowed, we send back the results and mark which ones are * disallowed. If "reboot" is disallowed, this might cause problems * with older fencer versions, which won't check for it. Older * versions will ignore "off" and "on", so they are not a problem. */ add_disallowed(dev, action, device, query->target, pcmk_is_set(query->call_options, st_opt_allow_self_fencing)); add_action_reply(dev, PCMK_ACTION_OFF, device, query->target, pcmk_is_set(query->call_options, st_opt_allow_self_fencing)); add_action_reply(dev, PCMK_ACTION_ON, device, query->target, FALSE); } /* A query without a target wants device parameters */ if (query->target == NULL) { xmlNode *attrs = pcmk__xe_create(dev, PCMK__XE_ATTRIBUTES); g_hash_table_foreach(device->params, hash2field, attrs); } } crm_xml_add_int(list, PCMK__XA_ST_AVAILABLE_DEVICES, available_devices); if (query->target) { crm_debug("Found %d matching device%s for target '%s'", available_devices, pcmk__plural_s(available_devices), query->target); } else { crm_debug("%d device%s installed", available_devices, pcmk__plural_s(available_devices)); } crm_log_xml_trace(list, "query-result"); stonith_send_reply(query->reply, query->call_options, query->remote_peer, client); done: pcmk__xml_free(query->reply); free(query->remote_peer); free(query->client_id); free(query->target); free(query->action); free(query); g_list_free_full(devices, free); } /*! * \internal * \brief Log the result of an asynchronous command * * \param[in] cmd Command the result is for * \param[in] result Result of command * \param[in] pid Process ID of command, if available * \param[in] next Alternate device that will be tried if command failed * \param[in] op_merged Whether this command was merged with an earlier one */ static void log_async_result(const async_command_t *cmd, const pcmk__action_result_t *result, int pid, const char *next, bool op_merged) { int log_level = LOG_ERR; int output_log_level = LOG_NEVER; guint devices_remaining = g_list_length(cmd->next_device_iter); GString *msg = g_string_sized_new(80); // Reasonable starting size // Choose log levels appropriately if we have a result if (pcmk__result_ok(result)) { log_level = (cmd->target == NULL)? LOG_DEBUG : LOG_NOTICE; if ((result->action_stdout != NULL) && !pcmk__str_eq(cmd->action, PCMK_ACTION_METADATA, pcmk__str_none)) { output_log_level = LOG_DEBUG; } next = NULL; } else { log_level = (cmd->target == NULL)? LOG_NOTICE : LOG_ERR; if ((result->action_stdout != NULL) && !pcmk__str_eq(cmd->action, PCMK_ACTION_METADATA, pcmk__str_none)) { output_log_level = LOG_WARNING; } } // Build the log message piece by piece pcmk__g_strcat(msg, "Operation '", cmd->action, "' ", NULL); if (pid != 0) { g_string_append_printf(msg, "[%d] ", pid); } if (cmd->target != NULL) { pcmk__g_strcat(msg, "targeting ", cmd->target, " ", NULL); } if (cmd->device != NULL) { pcmk__g_strcat(msg, "using ", cmd->device, " ", NULL); } // Add exit status or execution status as appropriate if (result->execution_status == PCMK_EXEC_DONE) { g_string_append_printf(msg, "returned %d", result->exit_status); } else { pcmk__g_strcat(msg, "could not be executed: ", pcmk_exec_status_str(result->execution_status), NULL); } // Add exit reason and next device if appropriate if (result->exit_reason != NULL) { pcmk__g_strcat(msg, " (", result->exit_reason, ")", NULL); } if (next != NULL) { pcmk__g_strcat(msg, ", retrying with ", next, NULL); } if (devices_remaining > 0) { g_string_append_printf(msg, " (%u device%s remaining)", (unsigned int) devices_remaining, pcmk__plural_s(devices_remaining)); } g_string_append_printf(msg, " " QB_XS " %scall %d from %s", (op_merged? "merged " : ""), cmd->id, cmd->client_name); // Log the result do_crm_log(log_level, "%s", msg->str); g_string_free(msg, TRUE); // Log the output (which may have multiple lines), if appropriate if (output_log_level != LOG_NEVER) { char *prefix = crm_strdup_printf("%s[%d]", cmd->device, pid); crm_log_output(output_log_level, prefix, result->action_stdout); free(prefix); } } /*! * \internal * \brief Reply to requester after asynchronous command completion * * \param[in] cmd Command that completed * \param[in] result Result of command * \param[in] pid Process ID of command, if available * \param[in] merged If true, command was merged with another, not executed */ static void send_async_reply(const async_command_t *cmd, const pcmk__action_result_t *result, int pid, bool merged) { xmlNode *reply = NULL; pcmk__client_t *client = NULL; CRM_CHECK((cmd != NULL) && (result != NULL), return); log_async_result(cmd, result, pid, NULL, merged); if (cmd->client != NULL) { client = pcmk__find_client_by_id(cmd->client); if ((client == NULL) && (cmd->origin == NULL)) { crm_trace("Skipping reply to %s: no longer a client", cmd->client); return; } } reply = construct_async_reply(cmd, result); if (merged) { pcmk__xe_set_bool_attr(reply, PCMK__XA_ST_OP_MERGED, true); } if (pcmk__is_fencing_action(cmd->action) && pcmk__str_eq(cmd->origin, cmd->target, pcmk__str_casei)) { /* The target was also the originator, so broadcast the result on its * behalf (since it will be unable to). */ crm_trace("Broadcast '%s' result for %s (target was also originator)", cmd->action, cmd->target); crm_xml_add(reply, PCMK__XA_SUBT, PCMK__VALUE_BROADCAST); crm_xml_add(reply, PCMK__XA_ST_OP, STONITH_OP_NOTIFY); pcmk__cluster_send_message(NULL, pcmk_ipc_fenced, reply); } else { // Reply only to the originator stonith_send_reply(reply, cmd->options, cmd->origin, client); } crm_log_xml_trace(reply, "Reply"); pcmk__xml_free(reply); } static void cancel_stonith_command(async_command_t * cmd) { fenced_device_t *device = cmd_device(cmd); if (device) { crm_trace("Cancel scheduled '%s' action using %s", cmd->action, device->id); device->pending_ops = g_list_remove(device->pending_ops, cmd); } } /*! * \internal * \brief Cancel and reply to any duplicates of a just-completed operation * * Check whether any fencing operations are scheduled to do the same thing as * one that just succeeded. If so, rather than performing the same operation * twice, return the result of this operation for all matching pending commands. * * \param[in,out] cmd Fencing operation that just succeeded * \param[in] result Result of \p cmd * \param[in] pid If nonzero, process ID of agent invocation (for logs) * * \note Duplicate merging will do the right thing for either type of remapped * reboot. If the executing fencer remapped an unsupported reboot to off, * then cmd->action will be "reboot" and will be merged with any other * reboot requests. If the originating fencer remapped a topology reboot * to off then on, we will get here once with cmd->action "off" and once * with "on", and they will be merged separately with similar requests. */ static void reply_to_duplicates(async_command_t *cmd, const pcmk__action_result_t *result, int pid) { GList *next = NULL; for (GList *iter = cmd_list; iter != NULL; iter = next) { async_command_t *cmd_other = iter->data; next = iter->next; // We might delete this entry, so grab next now if (cmd == cmd_other) { continue; } /* A pending operation matches if: * 1. The client connections are different. * 2. The target is the same. * 3. The fencing action is the same. * 4. The device scheduled to execute the action is the same. */ if (pcmk__str_eq(cmd->client, cmd_other->client, pcmk__str_casei) || !pcmk__str_eq(cmd->target, cmd_other->target, pcmk__str_casei) || !pcmk__str_eq(cmd->action, cmd_other->action, pcmk__str_none) || !pcmk__str_eq(cmd->device, cmd_other->device, pcmk__str_casei)) { continue; } crm_notice("Merging fencing action '%s'%s%s originating from " "client %s with identical fencing request from client %s", cmd_other->action, (cmd_other->target == NULL)? "" : " targeting ", pcmk__s(cmd_other->target, ""), cmd_other->client_name, cmd->client_name); // Stop tracking the duplicate, send its result, and cancel it cmd_list = g_list_remove_link(cmd_list, iter); send_async_reply(cmd_other, result, pid, true); cancel_stonith_command(cmd_other); free_async_command(cmd_other); g_list_free_1(iter); } } /*! * \internal * \brief Return the next required device (if any) for an operation * * \param[in,out] cmd Fencing operation that just succeeded * * \return Next device required for action if any, otherwise NULL */ static fenced_device_t * next_required_device(async_command_t *cmd) { for (GList *iter = cmd->next_device_iter; iter != NULL; iter = iter->next) { fenced_device_t *next_device = g_hash_table_lookup(device_table, iter->data); if (is_action_required(cmd->action, next_device)) { /* This is only called for successful actions, so it's OK to skip * non-required devices. */ cmd->next_device_iter = iter->next; return next_device; } } return NULL; } static void st_child_done(int pid, const pcmk__action_result_t *result, void *user_data) { async_command_t *cmd = user_data; fenced_device_t *device = NULL; fenced_device_t *next_device = NULL; CRM_CHECK(cmd != NULL, return); device = cmd_device(cmd); cmd->active_on = NULL; /* The device is ready to do something else now */ if (device) { if (!pcmk_is_set(device->flags, fenced_df_verified) && pcmk__result_ok(result) && pcmk__strcase_any_of(cmd->action, PCMK_ACTION_LIST, PCMK_ACTION_MONITOR, PCMK_ACTION_STATUS, NULL)) { fenced_device_set_flags(device, fenced_df_verified); } mainloop_set_trigger(device->work); } if (pcmk__result_ok(result)) { next_device = next_required_device(cmd); } else if ((cmd->next_device_iter != NULL) && !is_action_required(cmd->action, device)) { /* if this device didn't work out, see if there are any others we can try. * if the failed device was 'required', we can't pick another device. */ next_device = g_hash_table_lookup(device_table, cmd->next_device_iter->data); cmd->next_device_iter = cmd->next_device_iter->next; } if (next_device == NULL) { send_async_reply(cmd, result, pid, false); if (pcmk__result_ok(result)) { reply_to_duplicates(cmd, result, pid); } free_async_command(cmd); } else { // This operation requires more fencing log_async_result(cmd, result, pid, next_device->id, false); schedule_stonith_command(cmd, next_device); } } static void stonith_fence_get_devices_cb(GList * devices, void *user_data) { async_command_t *cmd = user_data; fenced_device_t *device = NULL; guint ndevices = g_list_length(devices); crm_info("Found %d matching device%s for target '%s'", ndevices, pcmk__plural_s(ndevices), cmd->target); if (devices != NULL) { device = g_hash_table_lookup(device_table, devices->data); } if (device == NULL) { // No device found pcmk__action_result_t result = PCMK__UNKNOWN_RESULT; pcmk__format_result(&result, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE, "No device configured for target '%s'", cmd->target); send_async_reply(cmd, &result, 0, false); pcmk__reset_result(&result); free_async_command(cmd); g_list_free_full(devices, free); } else { /* Device found. Schedule a fencing command for it. * * Assign devices to device_list so that it will be freed with cmd. */ cmd->device_list = devices; cmd->next_device_iter = devices->next; schedule_stonith_command(cmd, device); } } /*! * \internal * \brief Execute a fence action via the local node * * \param[in] msg Fencing request * \param[out] result Where to store result of fence action */ static void fence_locally(xmlNode *msg, pcmk__action_result_t *result) { const char *device_id = NULL; fenced_device_t *device = NULL; async_command_t *cmd = NULL; xmlNode *dev = NULL; CRM_CHECK((msg != NULL) && (result != NULL), return); dev = pcmk__xpath_find_one(msg->doc, "//*[@" PCMK__XA_ST_TARGET "]", LOG_ERR); cmd = create_async_command(msg); if (cmd == NULL) { crm_log_xml_warn(msg, "invalid"); set_bad_request_result(result); return; } device_id = crm_element_value(dev, PCMK__XA_ST_DEVICE_ID); if (device_id != NULL) { device = g_hash_table_lookup(device_table, device_id); if (device == NULL) { crm_err("Requested device '%s' is not available", device_id); pcmk__format_result(result, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE, "Requested device '%s' not found", device_id); return; } schedule_stonith_command(cmd, device); } else { const char *host = crm_element_value(dev, PCMK__XA_ST_TARGET); if (pcmk_is_set(cmd->options, st_opt_cs_nodeid)) { int nodeid = 0; pcmk__node_status_t *node = NULL; pcmk__scan_min_int(host, &nodeid, 0); node = pcmk__search_node_caches(nodeid, NULL, NULL, pcmk__node_search_any |pcmk__node_search_cluster_cib); if (node != NULL) { host = node->name; } } /* If we get to here, then self-fencing is implicitly allowed */ get_capable_devices(host, cmd->action, cmd->default_timeout, TRUE, cmd, stonith_fence_get_devices_cb, fenced_support_flag(cmd->action)); } pcmk__set_result(result, CRM_EX_OK, PCMK_EXEC_PENDING, NULL); } /*! * \internal * \brief Build an XML reply for a fencing operation * * \param[in] request Request that reply is for * \param[in] data If not NULL, add to reply as call data * \param[in] result Full result of fencing operation * * \return Newly created XML reply * \note The caller is responsible for freeing the result. * \note This has some overlap with construct_async_reply(), but that copies * values from an async_command_t, whereas this one copies them from the * request. */ xmlNode * fenced_construct_reply(const xmlNode *request, xmlNode *data, const pcmk__action_result_t *result) { xmlNode *reply = NULL; reply = pcmk__xe_create(NULL, PCMK__XE_ST_REPLY); crm_xml_add(reply, PCMK__XA_ST_ORIGIN, __func__); crm_xml_add(reply, PCMK__XA_T, PCMK__VALUE_STONITH_NG); stonith__xe_set_result(reply, result); if (request == NULL) { /* Most likely, this is the result of a stonith operation that was * initiated before we came up. Unfortunately that means we lack enough * information to provide clients with a full result. * * @TODO Maybe synchronize this information at start-up? */ crm_warn("Missing request information for client notifications for " "operation with result '%s' (initiated before we came up?)", pcmk_exec_status_str(result->execution_status)); } else { const char *name = NULL; const char *value = NULL; // Attributes to copy from request to reply const char *names[] = { PCMK__XA_ST_OP, PCMK__XA_ST_CALLID, PCMK__XA_ST_CLIENTID, PCMK__XA_ST_CLIENTNAME, PCMK__XA_ST_REMOTE_OP, PCMK__XA_ST_CALLOPT, }; for (int lpc = 0; lpc < PCMK__NELEM(names); lpc++) { name = names[lpc]; value = crm_element_value(request, name); crm_xml_add(reply, name, value); } if (data != NULL) { xmlNode *wrapper = pcmk__xe_create(reply, PCMK__XE_ST_CALLDATA); pcmk__xml_copy(wrapper, data); } } return reply; } /*! * \internal * \brief Build an XML reply to an asynchronous fencing command * * \param[in] cmd Fencing command that reply is for * \param[in] result Command result */ static xmlNode * construct_async_reply(const async_command_t *cmd, const pcmk__action_result_t *result) { xmlNode *reply = pcmk__xe_create(NULL, PCMK__XE_ST_REPLY); crm_xml_add(reply, PCMK__XA_ST_ORIGIN, __func__); crm_xml_add(reply, PCMK__XA_T, PCMK__VALUE_STONITH_NG); crm_xml_add(reply, PCMK__XA_ST_OP, cmd->op); crm_xml_add(reply, PCMK__XA_ST_DEVICE_ID, cmd->device); crm_xml_add(reply, PCMK__XA_ST_REMOTE_OP, cmd->remote_op_id); crm_xml_add(reply, PCMK__XA_ST_CLIENTID, cmd->client); crm_xml_add(reply, PCMK__XA_ST_CLIENTNAME, cmd->client_name); crm_xml_add(reply, PCMK__XA_ST_TARGET, cmd->target); crm_xml_add(reply, PCMK__XA_ST_DEVICE_ACTION, cmd->op); crm_xml_add(reply, PCMK__XA_ST_ORIGIN, cmd->origin); crm_xml_add_int(reply, PCMK__XA_ST_CALLID, cmd->id); crm_xml_add_int(reply, PCMK__XA_ST_CALLOPT, cmd->options); stonith__xe_set_result(reply, result); return reply; } bool fencing_peer_active(pcmk__node_status_t *peer) { return (peer != NULL) && (peer->name != NULL) && pcmk_is_set(peer->processes, crm_get_cluster_proc()); } void set_fencing_completed(remote_fencing_op_t *op) { struct timespec tv; qb_util_timespec_from_epoch_get(&tv); op->completed = tv.tv_sec; op->completed_nsec = tv.tv_nsec; } /*! * \internal * \brief Look for alternate node needed if local node shouldn't fence target * * \param[in] target Node that must be fenced * * \return Name of an alternate node that should fence \p target if any, * or NULL otherwise */ static const char * check_alternate_host(const char *target) { if (pcmk__str_eq(target, fenced_get_local_node(), pcmk__str_casei)) { GHashTableIter gIter; pcmk__node_status_t *entry = NULL; g_hash_table_iter_init(&gIter, pcmk__peer_cache); while (g_hash_table_iter_next(&gIter, NULL, (void **)&entry)) { if (fencing_peer_active(entry) && !pcmk__str_eq(entry->name, target, pcmk__str_casei)) { crm_notice("Forwarding self-fencing request to %s", entry->name); return entry->name; } } crm_warn("Will handle own fencing because no peer can"); } return NULL; } static void remove_relay_op(xmlNode * request) { xmlNode *dev = pcmk__xpath_find_one(request->doc, "//*[@" PCMK__XA_ST_DEVICE_ACTION "]", LOG_TRACE); const char *relay_op_id = NULL; const char *op_id = NULL; const char *client_name = NULL; const char *target = NULL; remote_fencing_op_t *relay_op = NULL; if (dev) { target = crm_element_value(dev, PCMK__XA_ST_TARGET); } relay_op_id = crm_element_value(request, PCMK__XA_ST_REMOTE_OP_RELAY); op_id = crm_element_value(request, PCMK__XA_ST_REMOTE_OP); client_name = crm_element_value(request, PCMK__XA_ST_CLIENTNAME); /* Delete RELAY operation. */ if ((relay_op_id != NULL) && (target != NULL) && pcmk__str_eq(target, fenced_get_local_node(), pcmk__str_casei)) { relay_op = g_hash_table_lookup(stonith_remote_op_list, relay_op_id); if (relay_op) { GHashTableIter iter; remote_fencing_op_t *list_op = NULL; g_hash_table_iter_init(&iter, stonith_remote_op_list); /* If the operation to be deleted is registered as a duplicate, delete the registration. */ while (g_hash_table_iter_next(&iter, NULL, (void **)&list_op)) { GList *dup_iter = NULL; if (list_op != relay_op) { for (dup_iter = list_op->duplicates; dup_iter != NULL; dup_iter = dup_iter->next) { remote_fencing_op_t *other = dup_iter->data; if (other == relay_op) { other->duplicates = g_list_remove(other->duplicates, relay_op); break; } } } } crm_debug("Deleting relay op %s ('%s'%s%s for %s), " "replaced by op %s ('%s'%s%s for %s)", relay_op->id, relay_op->action, (relay_op->target == NULL)? "" : " targeting ", pcmk__s(relay_op->target, ""), relay_op->client_name, op_id, relay_op->action, (target == NULL)? "" : " targeting ", pcmk__s(target, ""), client_name); g_hash_table_remove(stonith_remote_op_list, relay_op_id); } } } /*! * \internal * \brief Check whether an API request was sent by a privileged user * * API commands related to fencing configuration may be done only by privileged * IPC users (i.e. root or hacluster), because all other users should go through * the CIB to have ACLs applied. If no client was given, this is a peer request, * which is always allowed. * * \param[in] c IPC client that sent request (or NULL if sent by CPG peer) * \param[in] op Requested API operation (for logging only) * * \return true if sender is peer or privileged client, otherwise false */ static inline bool is_privileged(const pcmk__client_t *c, const char *op) { if ((c == NULL) || pcmk_is_set(c->flags, pcmk__client_privileged)) { return true; } else { crm_warn("Rejecting IPC request '%s' from unprivileged client %s", pcmk__s(op, ""), pcmk__client_name(c)); return false; } } // CRM_OP_REGISTER static xmlNode * handle_register_request(pcmk__request_t *request) { xmlNode *reply = pcmk__xe_create(NULL, "reply"); pcmk__assert(request->ipc_client != NULL); crm_xml_add(reply, PCMK__XA_ST_OP, CRM_OP_REGISTER); crm_xml_add(reply, PCMK__XA_ST_CLIENTID, request->ipc_client->id); pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); pcmk__set_request_flags(request, pcmk__request_reuse_options); return reply; } // STONITH_OP_EXEC static xmlNode * handle_agent_request(pcmk__request_t *request) { execute_agent_action(request->xml, &request->result); if (request->result.execution_status == PCMK_EXEC_PENDING) { return NULL; } return fenced_construct_reply(request->xml, NULL, &request->result); } // STONITH_OP_TIMEOUT_UPDATE static xmlNode * handle_update_timeout_request(pcmk__request_t *request) { const char *call_id = crm_element_value(request->xml, PCMK__XA_ST_CALLID); const char *client_id = crm_element_value(request->xml, PCMK__XA_ST_CLIENTID); int op_timeout = 0; crm_element_value_int(request->xml, PCMK__XA_ST_TIMEOUT, &op_timeout); do_stonith_async_timeout_update(client_id, call_id, op_timeout); pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); return NULL; } // STONITH_OP_QUERY static xmlNode * handle_query_request(pcmk__request_t *request) { int timeout = 0; xmlNode *dev = NULL; const char *action = NULL; const char *target = NULL; const char *client_id = crm_element_value(request->xml, PCMK__XA_ST_CLIENTID); struct st_query_data *query = NULL; if (request->peer != NULL) { // Record it for the future notification create_remote_stonith_op(client_id, request->xml, TRUE); } /* Delete the DC node RELAY operation. */ remove_relay_op(request->xml); pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); dev = pcmk__xpath_find_one(request->xml->doc, "//*[@" PCMK__XA_ST_DEVICE_ACTION "]", LOG_NEVER); if (dev != NULL) { const char *device = crm_element_value(dev, PCMK__XA_ST_DEVICE_ID); if (pcmk__str_eq(device, "manual_ack", pcmk__str_casei)) { return NULL; // No query or reply necessary } target = crm_element_value(dev, PCMK__XA_ST_TARGET); action = crm_element_value(dev, PCMK__XA_ST_DEVICE_ACTION); } crm_log_xml_trace(request->xml, "Query"); query = pcmk__assert_alloc(1, sizeof(struct st_query_data)); query->reply = fenced_construct_reply(request->xml, NULL, &request->result); query->remote_peer = pcmk__str_copy(request->peer); query->client_id = pcmk__str_copy(client_id); query->target = pcmk__str_copy(target); query->action = pcmk__str_copy(action); query->call_options = request->call_options; crm_element_value_int(request->xml, PCMK__XA_ST_TIMEOUT, &timeout); get_capable_devices(target, action, timeout, pcmk_is_set(query->call_options, st_opt_allow_self_fencing), query, stonith_query_capable_device_cb, fenced_df_none); return NULL; } // STONITH_OP_NOTIFY static xmlNode * handle_notify_request(pcmk__request_t *request) { const char *flag_name = NULL; pcmk__assert(request->ipc_client != NULL); flag_name = crm_element_value(request->xml, PCMK__XA_ST_NOTIFY_ACTIVATE); if (flag_name != NULL) { crm_debug("Enabling %s callbacks for client %s", flag_name, pcmk__request_origin(request)); pcmk__set_client_flags(request->ipc_client, fenced_parse_notify_flag(flag_name)); } flag_name = crm_element_value(request->xml, PCMK__XA_ST_NOTIFY_DEACTIVATE); if (flag_name != NULL) { crm_debug("Disabling %s callbacks for client %s", flag_name, pcmk__request_origin(request)); pcmk__clear_client_flags(request->ipc_client, fenced_parse_notify_flag(flag_name)); } pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); pcmk__set_request_flags(request, pcmk__request_reuse_options); return pcmk__ipc_create_ack(request->ipc_flags, PCMK__XE_ACK, NULL, CRM_EX_OK); } // STONITH_OP_RELAY static xmlNode * handle_relay_request(pcmk__request_t *request) { xmlNode *dev = pcmk__xpath_find_one(request->xml->doc, "//*[@" PCMK__XA_ST_TARGET "]", LOG_TRACE); crm_notice("Received forwarded fencing request from " "%s %s to fence (%s) peer %s", pcmk__request_origin_type(request), pcmk__request_origin(request), crm_element_value(dev, PCMK__XA_ST_DEVICE_ACTION), crm_element_value(dev, PCMK__XA_ST_TARGET)); if (initiate_remote_stonith_op(NULL, request->xml, FALSE) == NULL) { set_bad_request_result(&request->result); return fenced_construct_reply(request->xml, NULL, &request->result); } pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_PENDING, NULL); return NULL; } // STONITH_OP_FENCE static xmlNode * handle_fence_request(pcmk__request_t *request) { if (request->peer != NULL) { fence_locally(request->xml, &request->result); } else if (pcmk_is_set(request->call_options, st_opt_manual_ack)) { switch (fenced_handle_manual_confirmation(request->ipc_client, request->xml)) { case pcmk_rc_ok: pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); break; case EINPROGRESS: pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_PENDING, NULL); break; default: set_bad_request_result(&request->result); break; } } else { const char *alternate_host = NULL; xmlNode *dev = pcmk__xpath_find_one(request->xml->doc, "//*[@" PCMK__XA_ST_TARGET "]", LOG_TRACE); const char *target = crm_element_value(dev, PCMK__XA_ST_TARGET); const char *action = crm_element_value(dev, PCMK__XA_ST_DEVICE_ACTION); const char *device = crm_element_value(dev, PCMK__XA_ST_DEVICE_ID); if (request->ipc_client != NULL) { int tolerance = 0; crm_notice("Client %s wants to fence (%s) %s using %s", pcmk__request_origin(request), action, target, (device? device : "any device")); crm_element_value_int(dev, PCMK__XA_ST_TOLERANCE, &tolerance); if (stonith_check_fence_tolerance(tolerance, target, action)) { pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); return fenced_construct_reply(request->xml, NULL, &request->result); } alternate_host = check_alternate_host(target); } else { crm_notice("Peer %s wants to fence (%s) '%s' with device '%s'", request->peer, action, target, (device == NULL)? "(any)" : device); } if (alternate_host != NULL) { const char *client_id = NULL; remote_fencing_op_t *op = NULL; pcmk__node_status_t *node = pcmk__get_node(0, alternate_host, NULL, pcmk__node_search_cluster_member); if (request->ipc_client->id == 0) { client_id = crm_element_value(request->xml, PCMK__XA_ST_CLIENTID); } else { client_id = request->ipc_client->id; } /* Create a duplicate fencing operation to relay with the client ID. * When a query response is received, this operation should be * deleted to avoid keeping the duplicate around. */ op = create_remote_stonith_op(client_id, request->xml, FALSE); crm_xml_add(request->xml, PCMK__XA_ST_OP, STONITH_OP_RELAY); crm_xml_add(request->xml, PCMK__XA_ST_CLIENTID, request->ipc_client->id); crm_xml_add(request->xml, PCMK__XA_ST_REMOTE_OP, op->id); // @TODO On failure, fail request immediately, or maybe panic pcmk__cluster_send_message(node, pcmk_ipc_fenced, request->xml); pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_PENDING, NULL); } else if (initiate_remote_stonith_op(request->ipc_client, request->xml, FALSE) == NULL) { set_bad_request_result(&request->result); } else { pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_PENDING, NULL); } } if (request->result.execution_status == PCMK_EXEC_PENDING) { return NULL; } return fenced_construct_reply(request->xml, NULL, &request->result); } // STONITH_OP_FENCE_HISTORY static xmlNode * handle_history_request(pcmk__request_t *request) { xmlNode *reply = NULL; xmlNode *data = NULL; stonith_fence_history(request->xml, &data, request->peer, request->call_options); pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); if (!pcmk_is_set(request->call_options, st_opt_discard_reply)) { /* When the local node broadcasts its history, it sets * st_opt_discard_reply and doesn't need a reply. */ reply = fenced_construct_reply(request->xml, data, &request->result); } pcmk__xml_free(data); return reply; } // STONITH_OP_DEVICE_ADD static xmlNode * handle_device_add_request(pcmk__request_t *request) { const char *op = crm_element_value(request->xml, PCMK__XA_ST_OP); xmlNode *dev = pcmk__xpath_find_one(request->xml->doc, "//" PCMK__XE_ST_DEVICE_ID, LOG_ERR); if (is_privileged(request->ipc_client, op)) { int rc = fenced_device_register(dev, false); rc = pcmk_rc2legacy(rc); pcmk__set_result(&request->result, ((rc == pcmk_ok)? CRM_EX_OK : CRM_EX_ERROR), stonith__legacy2status(rc), ((rc == pcmk_ok)? NULL : pcmk_strerror(rc))); } else { pcmk__set_result(&request->result, CRM_EX_INSUFFICIENT_PRIV, PCMK_EXEC_INVALID, "Unprivileged users must register device via CIB"); } fenced_send_config_notification(op, &request->result, (dev == NULL)? NULL : pcmk__xe_id(dev)); return fenced_construct_reply(request->xml, NULL, &request->result); } // STONITH_OP_DEVICE_DEL static xmlNode * handle_device_delete_request(pcmk__request_t *request) { xmlNode *dev = pcmk__xpath_find_one(request->xml->doc, "//" PCMK__XE_ST_DEVICE_ID, LOG_ERR); const char *device_id = crm_element_value(dev, PCMK_XA_ID); const char *op = crm_element_value(request->xml, PCMK__XA_ST_OP); if (is_privileged(request->ipc_client, op)) { stonith_device_remove(device_id, false); pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); } else { pcmk__set_result(&request->result, CRM_EX_INSUFFICIENT_PRIV, PCMK_EXEC_INVALID, "Unprivileged users must delete device via CIB"); } fenced_send_config_notification(op, &request->result, device_id); return fenced_construct_reply(request->xml, NULL, &request->result); } // STONITH_OP_LEVEL_ADD static xmlNode * handle_level_add_request(pcmk__request_t *request) { const char *op = crm_element_value(request->xml, PCMK__XA_ST_OP); if (is_privileged(request->ipc_client, op)) { fenced_register_level(request->xml, &request->result); } else { unpack_level_request(request->xml, NULL, NULL, NULL); pcmk__set_result(&request->result, CRM_EX_INSUFFICIENT_PRIV, PCMK_EXEC_INVALID, "Unprivileged users must add level via CIB"); } return fenced_construct_reply(request->xml, NULL, &request->result); } // STONITH_OP_LEVEL_DEL static xmlNode * handle_level_delete_request(pcmk__request_t *request) { const char *op = crm_element_value(request->xml, PCMK__XA_ST_OP); if (is_privileged(request->ipc_client, op)) { fenced_unregister_level(request->xml, &request->result); } else { unpack_level_request(request->xml, NULL, NULL, NULL); pcmk__set_result(&request->result, CRM_EX_INSUFFICIENT_PRIV, PCMK_EXEC_INVALID, "Unprivileged users must delete level via CIB"); } return fenced_construct_reply(request->xml, NULL, &request->result); } // CRM_OP_RM_NODE_CACHE static xmlNode * handle_cache_request(pcmk__request_t *request) { int node_id = 0; const char *name = NULL; crm_element_value_int(request->xml, PCMK_XA_ID, &node_id); name = crm_element_value(request->xml, PCMK_XA_UNAME); pcmk__cluster_forget_cluster_node(node_id, name); pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); return NULL; } static xmlNode * handle_unknown_request(pcmk__request_t *request) { crm_err("Unknown IPC request %s from %s %s", request->op, pcmk__request_origin_type(request), pcmk__request_origin(request)); pcmk__format_result(&request->result, CRM_EX_PROTOCOL, PCMK_EXEC_INVALID, "Unknown IPC request type '%s' (bug?)", request->op); return fenced_construct_reply(request->xml, NULL, &request->result); } static void fenced_register_handlers(void) { pcmk__server_command_t handlers[] = { { CRM_OP_REGISTER, handle_register_request }, { STONITH_OP_EXEC, handle_agent_request }, { STONITH_OP_TIMEOUT_UPDATE, handle_update_timeout_request }, { STONITH_OP_QUERY, handle_query_request }, { STONITH_OP_NOTIFY, handle_notify_request }, { STONITH_OP_RELAY, handle_relay_request }, { STONITH_OP_FENCE, handle_fence_request }, { STONITH_OP_FENCE_HISTORY, handle_history_request }, { STONITH_OP_DEVICE_ADD, handle_device_add_request }, { STONITH_OP_DEVICE_DEL, handle_device_delete_request }, { STONITH_OP_LEVEL_ADD, handle_level_add_request }, { STONITH_OP_LEVEL_DEL, handle_level_delete_request }, { CRM_OP_RM_NODE_CACHE, handle_cache_request }, { NULL, handle_unknown_request }, }; fenced_handlers = pcmk__register_handlers(handlers); } void fenced_unregister_handlers(void) { if (fenced_handlers != NULL) { g_hash_table_destroy(fenced_handlers); fenced_handlers = NULL; } } static void handle_request(pcmk__request_t *request) { xmlNode *reply = NULL; const char *reason = NULL; if (fenced_handlers == NULL) { fenced_register_handlers(); } reply = pcmk__process_request(request, fenced_handlers); if (reply != NULL) { if (pcmk_is_set(request->flags, pcmk__request_reuse_options) && (request->ipc_client != NULL)) { /* Certain IPC-only commands must reuse the call options from the * original request rather than the ones set by stonith_send_reply() * -> do_local_reply(). */ pcmk__ipc_send_xml(request->ipc_client, request->ipc_id, reply, request->ipc_flags); request->ipc_client->request_id = 0; } else { stonith_send_reply(reply, request->call_options, request->peer, request->ipc_client); } pcmk__xml_free(reply); } reason = request->result.exit_reason; crm_debug("Processed %s request from %s %s: %s%s%s%s", request->op, pcmk__request_origin_type(request), pcmk__request_origin(request), pcmk_exec_status_str(request->result.execution_status), (reason == NULL)? "" : " (", (reason == NULL)? "" : reason, (reason == NULL)? "" : ")"); } static void handle_reply(pcmk__client_t *client, xmlNode *request, const char *remote_peer) { // Copy, because request might be freed before we want to log this char *op = crm_element_value_copy(request, PCMK__XA_ST_OP); if (pcmk__str_eq(op, STONITH_OP_QUERY, pcmk__str_none)) { process_remote_stonith_query(request); } else if (pcmk__str_any_of(op, STONITH_OP_NOTIFY, STONITH_OP_FENCE, NULL)) { fenced_process_fencing_reply(request); } else { crm_err("Ignoring unknown %s reply from %s %s", pcmk__s(op, "untyped"), ((client == NULL)? "peer" : "client"), ((client == NULL)? remote_peer : pcmk__client_name(client))); crm_log_xml_warn(request, "UnknownOp"); free(op); return; } crm_debug("Processed %s reply from %s %s", op, ((client == NULL)? "peer" : "client"), ((client == NULL)? remote_peer : pcmk__client_name(client))); free(op); } /*! * \internal * \brief Handle a message from an IPC client or CPG peer * * \param[in,out] client If not NULL, IPC client that sent message * \param[in] id If from IPC client, IPC message ID * \param[in] flags Message flags * \param[in,out] message Message XML * \param[in] remote_peer If not NULL, CPG peer that sent message */ void stonith_command(pcmk__client_t *client, uint32_t id, uint32_t flags, xmlNode *message, const char *remote_peer) { uint32_t call_options = st_opt_none; int rc = pcmk_rc_ok; bool is_reply = false; CRM_CHECK(message != NULL, return); if (pcmk__xpath_find_one(message->doc, "//" PCMK__XE_ST_REPLY, LOG_NEVER) != NULL) { is_reply = true; } rc = pcmk__xe_get_flags(message, PCMK__XA_ST_CALLOPT, &call_options, st_opt_none); if (rc != pcmk_rc_ok) { crm_warn("Couldn't parse options from message: %s", pcmk_rc_str(rc)); } crm_debug("Processing %ssynchronous %s %s %u from %s %s", pcmk_is_set(call_options, st_opt_sync_call)? "" : "a", crm_element_value(message, PCMK__XA_ST_OP), (is_reply? "reply" : "request"), id, ((client == NULL)? "peer" : "client"), ((client == NULL)? remote_peer : pcmk__client_name(client))); if (pcmk_is_set(call_options, st_opt_sync_call)) { pcmk__assert((client == NULL) || (client->request_id == id)); } if (is_reply) { handle_reply(client, message, remote_peer); } else { pcmk__request_t request = { .ipc_client = client, .ipc_id = id, .ipc_flags = flags, .peer = remote_peer, .xml = message, .call_options = call_options, .result = PCMK__UNKNOWN_RESULT, }; request.op = crm_element_value_copy(request.xml, PCMK__XA_ST_OP); CRM_CHECK(request.op != NULL, return); if (pcmk_is_set(request.call_options, st_opt_sync_call)) { pcmk__set_request_flags(&request, pcmk__request_sync); } handle_request(&request); pcmk__reset_request(&request); } } diff --git a/daemons/pacemakerd/pcmkd_corosync.c b/daemons/pacemakerd/pcmkd_corosync.c index 7e54c42c4a..f0e68d749f 100644 --- a/daemons/pacemakerd/pcmkd_corosync.c +++ b/daemons/pacemakerd/pcmkd_corosync.c @@ -1,375 +1,377 @@ /* - * Copyright 2010-2024 the Pacemaker project contributors + * Copyright 2010-2025 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 #include "pacemakerd.h" #include "pcmkd_corosync.h" #include #include /* for calls to stat() */ #include /* For basename() and dirname() */ #include #include /* For getpwname() */ #include #include #include #include #include #include /* for crm_ipc_is_authentic_process */ #include #include /* PCMK__SPECIAL_PID* */ static corosync_cfg_handle_t cfg_handle = 0; static mainloop_timer_t *reconnect_timer = NULL; /* =::=::=::= CFG - Shutdown stuff =::=::=::= */ static void cfg_shutdown_callback(corosync_cfg_handle_t h, corosync_cfg_shutdown_flags_t flags) { crm_info("Corosync wants to shut down: %s", (flags == COROSYNC_CFG_SHUTDOWN_FLAG_IMMEDIATE) ? "immediate" : (flags == COROSYNC_CFG_SHUTDOWN_FLAG_REGARDLESS) ? "forced" : "optional"); /* Never allow corosync to shut down while we're running */ corosync_cfg_replyto_shutdown(h, COROSYNC_CFG_SHUTDOWN_FLAG_NO); } static corosync_cfg_callbacks_t cfg_callbacks = { .corosync_cfg_shutdown_callback = cfg_shutdown_callback, }; static int pcmk_cfg_dispatch(gpointer user_data) { corosync_cfg_handle_t *handle = (corosync_cfg_handle_t *) user_data; cs_error_t rc = corosync_cfg_dispatch(*handle, CS_DISPATCH_ALL); if (rc != CS_OK) { return -1; } return 0; } static void close_cfg(void) { if (cfg_handle != 0) { #ifdef HAVE_COROSYNC_CFG_TRACKSTART /* Ideally, we would call corosync_cfg_trackstop(cfg_handle) here, but a * bug in corosync 3.1.1 and 3.1.2 makes it hang forever. Thankfully, * it's not necessary since we exit immediately after this. */ #endif corosync_cfg_finalize(cfg_handle); cfg_handle = 0; } } static gboolean cluster_reconnect_cb(gpointer data) { if (cluster_connect_cfg()) { mainloop_timer_del(reconnect_timer); reconnect_timer = NULL; crm_notice("Cluster reconnect succeeded"); pacemakerd_read_config(); restart_cluster_subdaemons(); return G_SOURCE_REMOVE; } else { crm_info("Cluster reconnect failed " "(connection will be reattempted once per second)"); } /* * In theory this will continue forever. In practice the CIB connection from * attrd will timeout and shut down Pacemaker when it gets bored. */ return G_SOURCE_CONTINUE; } static void cfg_connection_destroy(gpointer user_data) { crm_warn("Lost connection to cluster layer " "(connection will be reattempted once per second)"); corosync_cfg_finalize(cfg_handle); cfg_handle = 0; reconnect_timer = mainloop_timer_add("corosync reconnect", 1000, TRUE, cluster_reconnect_cb, NULL); mainloop_timer_start(reconnect_timer); } void cluster_disconnect_cfg(void) { close_cfg(); if (reconnect_timer != NULL) { /* The mainloop should be gone by this point, so this isn't necessary, * but cleaning up memory should make valgrind happier. */ mainloop_timer_del(reconnect_timer); reconnect_timer = NULL; } } #define cs_repeat(counter, max, code) do { \ code; \ if(rc == CS_ERR_TRY_AGAIN || rc == CS_ERR_QUEUE_FULL) { \ counter++; \ crm_debug("Retrying Corosync operation after %ds", counter); \ sleep(counter); \ } else { \ break; \ } \ } while(counter < max) gboolean cluster_connect_cfg(void) { cs_error_t rc; int fd = -1, retries = 0, rv; uid_t found_uid = 0; gid_t found_gid = 0; pid_t found_pid = 0; uint32_t nodeid; static struct mainloop_fd_callbacks cfg_fd_callbacks = { .dispatch = pcmk_cfg_dispatch, .destroy = cfg_connection_destroy, }; cs_repeat(retries, 30, rc = corosync_cfg_initialize(&cfg_handle, &cfg_callbacks)); if (rc != CS_OK) { crm_crit("Could not connect to Corosync CFG: %s " QB_XS " rc=%d", cs_strerror(rc), rc); return FALSE; } rc = corosync_cfg_fd_get(cfg_handle, &fd); if (rc != CS_OK) { crm_crit("Could not get Corosync CFG descriptor: %s " QB_XS " rc=%d", cs_strerror(rc), rc); goto bail; } /* CFG provider run as root (in given user namespace, anyway)? */ if (!(rv = crm_ipc_is_authentic_process(fd, (uid_t) 0,(gid_t) 0, &found_pid, &found_uid, &found_gid))) { crm_crit("Rejecting Corosync CFG provider because process %lld " "is running as uid %lld gid %lld, not root", (long long) PCMK__SPECIAL_PID_AS_0(found_pid), (long long) found_uid, (long long) found_gid); goto bail; } else if (rv < 0) { crm_crit("Could not authenticate Corosync CFG provider: %s " QB_XS " rc=%d", strerror(-rv), -rv); goto bail; } retries = 0; cs_repeat(retries, 30, rc = corosync_cfg_local_get(cfg_handle, &nodeid)); if (rc != CS_OK) { crm_crit("Could not get local node ID from Corosync: %s " QB_XS " rc=%d", cs_strerror(rc), rc); goto bail; } crm_debug("Corosync reports local node ID is %lu", (unsigned long) nodeid); #ifdef HAVE_COROSYNC_CFG_TRACKSTART retries = 0; cs_repeat(retries, 30, rc = corosync_cfg_trackstart(cfg_handle, 0)); if (rc != CS_OK) { crm_crit("Could not enable Corosync CFG shutdown tracker: %s " QB_XS " rc=%d", cs_strerror(rc), rc); goto bail; } #endif mainloop_add_fd("corosync-cfg", G_PRIORITY_DEFAULT, fd, &cfg_handle, &cfg_fd_callbacks); return TRUE; bail: corosync_cfg_finalize(cfg_handle); return FALSE; } void pcmkd_shutdown_corosync(void) { cs_error_t rc; if (cfg_handle == 0) { crm_warn("Unable to shut down Corosync: No connection"); return; } crm_info("Asking Corosync to shut down"); rc = corosync_cfg_try_shutdown(cfg_handle, COROSYNC_CFG_SHUTDOWN_FLAG_IMMEDIATE); if (rc == CS_OK) { close_cfg(); } else { crm_warn("Corosync shutdown failed: %s " QB_XS " rc=%d", cs_strerror(rc), rc); } } bool pcmkd_corosync_connected(void) { cpg_handle_t local_handle = 0; cpg_model_v1_data_t cpg_model_info = {CPG_MODEL_V1, NULL, NULL, NULL, 0}; int fd = -1; if (cpg_model_initialize(&local_handle, CPG_MODEL_V1, (cpg_model_data_t *) &cpg_model_info, NULL) != CS_OK) { return false; } if (cpg_fd_get(local_handle, &fd) != CS_OK) { return false; } cpg_finalize(local_handle); return true; } /* =::=::=::= Configuration =::=::=::= */ static int get_config_opt(uint64_t unused, cmap_handle_t object_handle, const char *key, char **value, const char *fallback) { int rc = 0, retries = 0; cs_repeat(retries, 5, rc = cmap_get_string(object_handle, key, value)); if (rc != CS_OK) { crm_trace("Search for %s failed %d, defaulting to %s", key, rc, fallback); pcmk__str_update(value, fallback); } crm_trace("%s: %s", key, *value); return rc; } gboolean pacemakerd_read_config(void) { cs_error_t rc = CS_OK; int retries = 0; cmap_handle_t local_handle; uint64_t config = 0; int fd = -1; uid_t found_uid = 0; gid_t found_gid = 0; pid_t found_pid = 0; int rv; enum pcmk_cluster_layer cluster_layer = pcmk_cluster_layer_unknown; const char *cluster_layer_s = NULL; // There can be only one possibility do { rc = pcmk__init_cmap(&local_handle); if (rc != CS_OK) { retries++; crm_info("Could not connect to Corosync CMAP: %s (retrying in %ds) " QB_XS " rc=%d", cs_strerror(rc), retries, rc); sleep(retries); } else { break; } } while (retries < 5); if (rc != CS_OK) { crm_crit("Could not connect to Corosync CMAP: %s " QB_XS " rc=%d", cs_strerror(rc), rc); return FALSE; } rc = cmap_fd_get(local_handle, &fd); if (rc != CS_OK) { crm_crit("Could not get Corosync CMAP descriptor: %s " QB_XS " rc=%d", cs_strerror(rc), rc); cmap_finalize(local_handle); return FALSE; } /* CMAP provider run as root (in given user namespace, anyway)? */ if (!(rv = crm_ipc_is_authentic_process(fd, (uid_t) 0,(gid_t) 0, &found_pid, &found_uid, &found_gid))) { crm_crit("Rejecting Corosync CMAP provider because process %lld " "is running as uid %lld gid %lld, not root", (long long) PCMK__SPECIAL_PID_AS_0(found_pid), (long long) found_uid, (long long) found_gid); cmap_finalize(local_handle); return FALSE; } else if (rv < 0) { crm_crit("Could not authenticate Corosync CMAP provider: %s " QB_XS " rc=%d", strerror(-rv), -rv); cmap_finalize(local_handle); return FALSE; } cluster_layer = pcmk_get_cluster_layer(); cluster_layer_s = pcmk_cluster_layer_text(cluster_layer); if (cluster_layer != pcmk_cluster_layer_corosync) { crm_crit("Expected Corosync cluster layer but detected %s " QB_XS " cluster_layer=%d", cluster_layer_s, cluster_layer); return FALSE; } crm_info("Reading configuration for %s cluster layer", cluster_layer_s); pcmk__set_env_option(PCMK__ENV_CLUSTER_TYPE, PCMK_VALUE_COROSYNC, true); // If debug logging is not configured, check whether corosync has it if (pcmk__env_option(PCMK__ENV_DEBUG) == NULL) { char *debug_enabled = NULL; get_config_opt(config, local_handle, "logging.debug", &debug_enabled, PCMK_VALUE_OFF); if (crm_is_true(debug_enabled)) { pcmk__set_env_option(PCMK__ENV_DEBUG, "1", true); if (get_crm_log_level() < LOG_DEBUG) { set_crm_log_level(LOG_DEBUG); } } else { pcmk__set_env_option(PCMK__ENV_DEBUG, "0", true); } free(debug_enabled); } if(local_handle){ gid_t gid = 0; if (pcmk_daemon_user(NULL, &gid) < 0) { crm_warn("Could not authorize group with Corosync " QB_XS " No group found for user %s", CRM_DAEMON_USER); } else { - char key[PATH_MAX]; - snprintf(key, PATH_MAX, "uidgid.gid.%u", gid); + char *key = crm_strdup_printf("uidgid.gid.%lld", (long long) gid); + rc = cmap_set_uint8(local_handle, key, 1); + free(key); + if (rc != CS_OK) { crm_warn("Could not authorize group with Corosync: %s " QB_XS " group=%u rc=%d", pcmk__cs_err_str(rc), gid, rc); } } } cmap_finalize(local_handle); return TRUE; } diff --git a/doc/sphinx/Pacemaker_Explained/alerts.rst b/doc/sphinx/Pacemaker_Explained/alerts.rst index 27000ed941..b573bcf2d4 100644 --- a/doc/sphinx/Pacemaker_Explained/alerts.rst +++ b/doc/sphinx/Pacemaker_Explained/alerts.rst @@ -1,284 +1,297 @@ .. _alerts: .. index:: single: alert single: resource; alert single: node; alert single: fencing; alert pair: XML element; alert pair: XML element; alerts Alerts ------ *Alerts* may be configured to take some external action when a cluster event occurs (node failure, resource starting or stopping, etc.). .. index:: pair: alert; agent Alert Agents ############ As with resource agents, the cluster calls an external program (an *alert agent*) to handle alerts. The cluster passes information about the event to the agent via environment variables. Agents can do anything desired with this information (send an e-mail, log to a file, update a monitoring system, etc.). .. topic:: Simple alert configuration .. code-block:: xml In the example above, the cluster will call ``my-script.sh`` for each event. Multiple alert agents may be configured; the cluster will call all of them for each event. Alert agents will be called only on cluster nodes. They will be called for events involving Pacemaker Remote nodes, but they will never be called *on* those nodes. For more information about sample alert agents provided by Pacemaker and about developing custom alert agents, see the *Pacemaker Administration* document. .. index:: single: alert; recipient pair: XML element; recipient Alert Recipients ################ Usually, alerts are directed towards a recipient. Thus, each alert may be additionally configured with one or more recipients. The cluster will call the agent separately for each recipient. .. topic:: Alert configuration with recipient .. code-block:: xml In the above example, the cluster will call ``my-script.sh`` for each event, passing the recipient ``some-address`` as an environment variable. The recipient may be anything the alert agent can recognize -- an IP address, an e-mail address, a file name, whatever the particular agent supports. .. index:: single: alert; meta-attributes single: meta-attribute; alert meta-attributes Alert Meta-Attributes ##################### As with resources, meta-attributes can be configured for alerts to change whether and how Pacemaker calls them. .. table:: **Meta-Attributes of an Alert or Recipient** :class: longtable :widths: 1 1 3 +------------------+---------------+-----------------------------------------------------+ | Meta-Attribute | Default | Description | +==================+===============+=====================================================+ | description | | .. index:: | | | | single: acl_permission; description (attribute) | | | | single: description; acl_permission attribute | | | | single: attribute; description (acl_permission) | | | | | | | | Arbitrary text for user's use (ignored by Pacemaker)| +------------------+---------------+-----------------------------------------------------+ | enabled | true | .. index:: | | | | single: alert; meta-attribute, enabled | | | | single: meta-attribute; enabled (alert) | | | | single: enabled; alert meta-attribute | | | | | | | | If false for an alert, the alert will not be used. | | | | If true for an alert and false for a particular | | | | recipient of that alert, that recipient will not be | | | | used. *(since 2.1.6)* | +------------------+---------------+-----------------------------------------------------+ - | timestamp-format | %H:%M:%S.%06N | .. index:: | + | timestamp-format | %H:%M:%S.%6N | .. index:: | | | | single: alert; meta-attribute, timestamp-format | | | | single: meta-attribute; timestamp-format (alert) | | | | single: timestamp-format; alert meta-attribute | | | | | | | | Format the cluster will use when sending the | | | | event's timestamp to the agent. This is a string as | - | | | used with the ``date(1)`` command. | + | | | used with the ``date(1)`` command, with the | + | | | following extension. ``"%xN"``, where ``x`` is a | + | | | number with ``1 <= x <= 6``, prints the fractional | + | | | seconds component of the timestamp at ``10^(-x)`` | + | | | resolution, without a decimal point (``'.'``). | + | | | Values are truncated toward zero, not rounded. | + | | | | + | | | Note: This is implemented using ``strftime()`` with | + | | | a 128-character buffer. If any format specifier's | + | | | expansion requires more than 128 characters, or if | + | | | any specifier expands to an empty string, then the | + | | | timestamp is discarded. (Expanding to an empty | + | | | string is not an error, but there is no way to | + | | | distinguish this from a too-small buffer.) | +------------------+---------------+-----------------------------------------------------+ | timeout | 30s | .. index:: | | | | single: alert; meta-attribute, timeout | | | | single: meta-attribute; timeout (alert) | | | | single: timeout; alert meta-attribute | | | | | | | | If the alert agent does not complete within this | | | | amount of time, it will be terminated. | +------------------+---------------+-----------------------------------------------------+ Meta-attributes can be configured per alert and/or per recipient. .. topic:: Alert configuration with meta-attributes .. code-block:: xml In the above example, the ``my-script.sh`` will get called twice for each event, with each call using a 15-second timeout. One call will be passed the recipient ``someuser@example.com`` and a timestamp in the format ``%D %H:%M``, while the other call will be passed the recipient ``otheruser@example.com`` and a timestamp in the format ``%c``. .. index:: single: alert; instance attributes single: instance attribute; alert instance attributes Alert Instance Attributes ######################### As with resource agents, agent-specific configuration values may be configured as instance attributes. These will be passed to the agent as additional environment variables. The number, names and allowed values of these instance attributes are completely up to the particular agent. .. topic:: Alert configuration with instance attributes .. code-block:: xml .. index:: single: alert; filters pair: XML element; select pair: XML element; select_nodes pair: XML element; select_fencing pair: XML element; select_resources pair: XML element; select_attributes pair: XML element; attribute Alert Filters ############# By default, an alert agent will be called for node events, fencing events, and resource events. An agent may choose to ignore certain types of events, but there is still the overhead of calling it for those events. To eliminate that overhead, you may select which types of events the agent should receive. Alert filters are configured within a ``select`` element inside an ``alert`` element. .. list-table:: **Possible alert filters** :class: longtable :widths: 1 3 :header-rows: 1 * - Name - Events alerted * - select_nodes - A node joins or leaves the cluster (whether at the cluster layer for cluster nodes, or via a remote connection for Pacemaker Remote nodes). * - select_fencing - Fencing or unfencing of a node completes (whether successfully or not). * - select_resources - A resource action other than meta-data completes (whether successfully or not). * - select_attributes - A transient attribute value update is sent to the CIB. .. topic:: Alert configuration to receive only node events and fencing events .. code-block:: xml With ```` (the only event type not enabled by default), the agent will receive alerts when a node attribute changes. If you wish the agent to be called only when certain attributes change, you can configure that as well. .. topic:: Alert configuration to be called when certain node attributes change .. code-block:: xml Node attribute alerts are currently considered experimental. Alerts may be limited to attributes set via ``attrd_updater``, and agents may be called multiple times with the same attribute value. diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h index ceb831c41d..c333787d4a 100644 --- a/include/crm/common/internal.h +++ b/include/crm/common/internal.h @@ -1,348 +1,347 @@ /* * Copyright 2015-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_INTERNAL__H #define PCMK__CRM_COMMON_INTERNAL__H #include // pid_t, getpid() #include // bool #include // uint8_t, uint64_t #include // PRIu64 #include // guint, GList, GHashTable #include // xmlNode #include // do_crm_log_unlikely(), etc. #include // mainloop_io_t, struct ipc_client_callbacks #include // crm_strdup_printf() #include #include #include #include #include #include #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif /* This says whether the current application is a Pacemaker daemon or not, * and is used to change default logging settings such as whether to log to * stderr, etc., as well as a few other details such as whether blackbox signal * handling is enabled. * * It is set when logging is initialized, and does not need to be set directly. */ extern bool pcmk__is_daemon; // Number of elements in a statically defined array #define PCMK__NELEM(a) ((int) (sizeof(a)/sizeof(a[0])) ) #if PCMK__ENABLE_CIBSECRETS /* internal CIB utilities (from cib_secrets.c) */ int pcmk__substitute_secrets(const char *rsc_id, GHashTable *params); #endif /* internal main loop utilities (from mainloop.c) */ int pcmk__add_mainloop_ipc(crm_ipc_t *ipc, int priority, void *userdata, const struct ipc_client_callbacks *callbacks, mainloop_io_t **source); guint pcmk__mainloop_timer_get_period(const mainloop_timer_t *timer); /* internal name/value utilities (from nvpair.c) */ int pcmk__scan_nvpair(const gchar *input, gchar **name, gchar **value); char *pcmk__format_nvpair(const char *name, const char *value, const char *units); /* internal procfs utilities (from procfs.c) */ pid_t pcmk__procfs_pid_of(const char *name); unsigned int pcmk__procfs_num_cores(void); -int pcmk__procfs_pid2path(pid_t pid, char path[], size_t path_size); +int pcmk__procfs_pid2path(pid_t pid, char **path); bool pcmk__procfs_has_pids(void); DIR *pcmk__procfs_fd_dir(void); void pcmk__sysrq_trigger(char t); bool pcmk__throttle_cib_load(const char *server, float *load); bool pcmk__throttle_load_avg(float *load); /* internal functions related to process IDs (from pid.c) */ /*! * \internal * \brief Check whether process exists (by PID and optionally executable path) * * \param[in] pid PID of process to check * \param[in] daemon If not NULL, path component to match with procfs entry * * \return Standard Pacemaker return code * \note Particular return codes of interest include pcmk_rc_ok for alive, * ESRCH for process is not alive (verified by kill and/or executable path * match), EACCES for caller unable or not allowed to check. A result of * "alive" is less reliable when \p daemon is not provided or procfs is * not available, since there is no guarantee that the PID has not been * recycled for another process. * \note This function cannot be used to verify \e authenticity of the process. */ int pcmk__pid_active(pid_t pid, const char *daemon); -int pcmk__read_pidfile(const char *filename, pid_t *pid); int pcmk__pidfile_matches(const char *filename, pid_t expected_pid, const char *expected_name, pid_t *pid); int pcmk__lock_pidfile(const char *filename, const char *name); // bitwise arithmetic utilities /*! * \internal * \brief Set specified flags in a flag group * * \param[in] function Function name of caller * \param[in] line Line number of caller * \param[in] log_level Log a message at this level * \param[in] flag_type Label describing this flag group (for logging) * \param[in] target Name of object whose flags these are (for logging) * \param[in] flag_group Flag group being manipulated * \param[in] flags Which flags in the group should be set * \param[in] flags_str Readable equivalent of \p flags (for logging) * * \return Possibly modified flag group */ static inline uint64_t pcmk__set_flags_as(const char *function, int line, uint8_t log_level, const char *flag_type, const char *target, uint64_t flag_group, uint64_t flags, const char *flags_str) { uint64_t result = flag_group | flags; if (result != flag_group) { do_crm_log_unlikely(log_level, "%s flags %#.8" PRIx64 " (%s) for %s set by %s:%d", pcmk__s(flag_type, "Group of"), flags, pcmk__s(flags_str, "flags"), pcmk__s(target, "target"), function, line); } return result; } /*! * \internal * \brief Clear specified flags in a flag group * * \param[in] function Function name of caller * \param[in] line Line number of caller * \param[in] log_level Log a message at this level * \param[in] flag_type Label describing this flag group (for logging) * \param[in] target Name of object whose flags these are (for logging) * \param[in] flag_group Flag group being manipulated * \param[in] flags Which flags in the group should be cleared * \param[in] flags_str Readable equivalent of \p flags (for logging) * * \return Possibly modified flag group */ static inline uint64_t pcmk__clear_flags_as(const char *function, int line, uint8_t log_level, const char *flag_type, const char *target, uint64_t flag_group, uint64_t flags, const char *flags_str) { uint64_t result = flag_group & ~flags; if (result != flag_group) { do_crm_log_unlikely(log_level, "%s flags %#.8" PRIx64 " (%s) for %s cleared by %s:%d", pcmk__s(flag_type, "Group of"), flags, pcmk__s(flags_str, "flags"), pcmk__s(target, "target"), function, line); } return result; } /*! * \internal * \brief Get readable string for whether specified flags are set * * \param[in] flag_group Group of flags to check * \param[in] flags Which flags in \p flag_group should be checked * * \return "true" if all \p flags are set in \p flag_group, otherwise "false" */ static inline const char * pcmk__flag_text(uint64_t flag_group, uint64_t flags) { return pcmk__btoa(pcmk_all_flags_set(flag_group, flags)); } // miscellaneous utilities (from utils.c) void pcmk__daemonize(const char *name, const char *pidfile); void pcmk__panic(const char *reason); pid_t pcmk__locate_sbd(void); void pcmk__sleep_ms(unsigned int ms); guint pcmk__create_timer(guint interval_ms, GSourceFunc fn, gpointer data); guint pcmk__timeout_ms2s(guint timeout_ms); extern int pcmk__score_red; extern int pcmk__score_green; extern int pcmk__score_yellow; /*! * \internal * \brief Allocate new zero-initialized memory, asserting on failure * * \param[in] file File where \p function is located * \param[in] function Calling function * \param[in] line Line within \p file * \param[in] nmemb Number of elements to allocate memory for * \param[in] size Size of each element * * \return Newly allocated memory of of size nmemb * size (guaranteed * not to be \c NULL) * * \note The caller is responsible for freeing the return value using \c free(). */ static inline void * pcmk__assert_alloc_as(const char *file, const char *function, uint32_t line, size_t nmemb, size_t size) { void *ptr = calloc(nmemb, size); if (ptr == NULL) { crm_abort(file, function, line, "Out of memory", FALSE, TRUE); crm_exit(CRM_EX_OSERR); } return ptr; } /*! * \internal * \brief Allocate new zero-initialized memory, asserting on failure * * \param[in] nmemb Number of elements to allocate memory for * \param[in] size Size of each element * * \return Newly allocated memory of of size nmemb * size (guaranteed * not to be \c NULL) * * \note The caller is responsible for freeing the return value using \c free(). */ #define pcmk__assert_alloc(nmemb, size) \ pcmk__assert_alloc_as(__FILE__, __func__, __LINE__, nmemb, size) /*! * \internal * \brief Resize a dynamically allocated memory block * * \param[in] ptr Memory block to resize (or NULL to allocate new memory) * \param[in] size New size of memory block in bytes (must be > 0) * * \return Pointer to resized memory block * * \note This asserts on error, so the result is guaranteed to be non-NULL * (which is the main advantage of this over directly using realloc()). */ static inline void * pcmk__realloc(void *ptr, size_t size) { void *new_ptr; // realloc(p, 0) can replace free(p) but this wrapper can't pcmk__assert(size > 0); new_ptr = realloc(ptr, size); if (new_ptr == NULL) { free(ptr); abort(); } return new_ptr; } static inline char * pcmk__getpid_s(void) { return crm_strdup_printf("%lu", (unsigned long) getpid()); } // More efficient than g_list_length(list) == 1 static inline bool pcmk__list_of_1(GList *list) { return list && (list->next == NULL); } // More efficient than g_list_length(list) > 1 static inline bool pcmk__list_of_multiple(GList *list) { return list && (list->next != NULL); } /* convenience functions for failure-related node attributes */ #define PCMK__FAIL_COUNT_PREFIX "fail-count" #define PCMK__LAST_FAILURE_PREFIX "last-failure" /*! * \internal * \brief Generate a failure-related node attribute name for a resource * * \param[in] prefix Start of attribute name * \param[in] rsc_id Resource name * \param[in] op Operation name * \param[in] interval_ms Operation interval * * \return Newly allocated string with attribute name * * \note Failure attributes are named like PREFIX-RSC#OP_INTERVAL (for example, * "fail-count-myrsc#monitor_30000"). The '#' is used because it is not * a valid character in a resource ID, to reliably distinguish where the * operation name begins. The '_' is used simply to be more comparable to * action labels like "myrsc_monitor_30000". */ static inline char * pcmk__fail_attr_name(const char *prefix, const char *rsc_id, const char *op, guint interval_ms) { CRM_CHECK(prefix && rsc_id && op, return NULL); return crm_strdup_printf("%s-%s#%s_%u", prefix, rsc_id, op, interval_ms); } static inline char * pcmk__failcount_name(const char *rsc_id, const char *op, guint interval_ms) { return pcmk__fail_attr_name(PCMK__FAIL_COUNT_PREFIX, rsc_id, op, interval_ms); } static inline char * pcmk__lastfailure_name(const char *rsc_id, const char *op, guint interval_ms) { return pcmk__fail_attr_name(PCMK__LAST_FAILURE_PREFIX, rsc_id, op, interval_ms); } // internal resource agent functions (from agents.c) int pcmk__effective_rc(int rc); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_INTERNAL__H diff --git a/include/crm/common/iso8601_internal.h b/include/crm/common/iso8601_internal.h index 41afc60d0e..7226e8b166 100644 --- a/include/crm/common/iso8601_internal.h +++ b/include/crm/common/iso8601_internal.h @@ -1,51 +1,48 @@ /* - * Copyright 2015-2024 the Pacemaker project contributors + * Copyright 2015-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_ISO8601_INTERNAL__H #define PCMK__CRM_COMMON_ISO8601_INTERNAL__H #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif typedef struct pcmk__time_us pcmk__time_hr_t; -pcmk__time_hr_t *pcmk__time_hr_convert(pcmk__time_hr_t *target, - const crm_time_t *dt); -void pcmk__time_set_hr_dt(crm_time_t *target, const pcmk__time_hr_t *hr_dt); pcmk__time_hr_t *pcmk__time_hr_now(time_t *epoch); pcmk__time_hr_t *pcmk__time_hr_new(const char *date_time); void pcmk__time_hr_free(pcmk__time_hr_t *hr_dt); char *pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt); char *pcmk__epoch2str(const time_t *source, uint32_t flags); char *pcmk__timespec2str(const struct timespec *ts, uint32_t flags); const char *pcmk__readable_interval(guint interval_ms); crm_time_t *pcmk__copy_timet(time_t source); struct pcmk__time_us { int years; int months; /* Only for durations */ int days; int seconds; int offset; /* Seconds */ bool duration; int useconds; }; #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_ISO8601_INTERNAL__H diff --git a/lib/common/attrs.c b/lib/common/attrs.c index aa50831c8b..e3d832d57c 100644 --- a/lib/common/attrs.c +++ b/lib/common/attrs.c @@ -1,180 +1,182 @@ /* - * Copyright 2011-2024 the Pacemaker project contributors + * Copyright 2011-2025 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 #include #include #include #include #define OCF_RESKEY_PREFIX "OCF_RESKEY_" #define LRM_TARGET_ENV OCF_RESKEY_PREFIX CRM_META "_" PCMK__META_ON_NODE /*! * \internal * \brief Get the node name that should be used to set node attributes * * If given NULL, "auto", or "localhost" as an argument, check the environment * to detect the node name that should be used to set node attributes. (The * caller might not know the correct name, for example if the target is part of * a bundle with \c PCMK_META_CONTAINER_ATTRIBUTE_TARGET set to * \c PCMK_VALUE_HOST.) * * \param[in] name NULL, "auto" or "localhost" to check environment variables, * or anything else to return NULL * * \return Node name that should be used for node attributes based on the * environment if known, otherwise NULL */ const char * pcmk__node_attr_target(const char *name) { if (name == NULL || pcmk__strcase_any_of(name, "auto", "localhost", NULL)) { - char buf[128] = OCF_RESKEY_PREFIX; - size_t offset = sizeof(OCF_RESKEY_PREFIX) - 1; + char *buf = NULL; char *target_var = crm_meta_name(PCMK_META_CONTAINER_ATTRIBUTE_TARGET); char *phys_var = crm_meta_name(PCMK__META_PHYSICAL_HOST); const char *target = NULL; const char *host_physical = NULL; - snprintf(buf + offset, sizeof(buf) - offset, "%s", target_var); + buf = crm_strdup_printf(OCF_RESKEY_PREFIX "%s", target_var); target = getenv(buf); + free(buf); - snprintf(buf + offset, sizeof(buf) - offset, "%s", phys_var); + buf = crm_strdup_printf(OCF_RESKEY_PREFIX "%s", phys_var); host_physical = getenv(buf); + free(buf); + + free(target_var); + free(phys_var); // It is important to use the name by which the scheduler knows us if (host_physical && pcmk__str_eq(target, PCMK_VALUE_HOST, pcmk__str_casei)) { name = host_physical; } else { const char *host_pcmk = getenv(LRM_TARGET_ENV); if (host_pcmk) { name = host_pcmk; } } - free(target_var); - free(phys_var); // TODO? Call pcmk__cluster_local_node_name() if name == NULL // (currently would require linkage against libcrmcluster) return name; } else { return NULL; } } /*! * \brief Return the name of the node attribute used as a promotion score * * \param[in] rsc_id Resource ID that promotion score is for (or NULL to * check the OCF_RESOURCE_INSTANCE environment variable) * * \return Newly allocated string with the node attribute name (or NULL on * error, including no ID or environment variable specified) * \note It is the caller's responsibility to free() the result. */ char * pcmk_promotion_score_name(const char *rsc_id) { if (pcmk__str_empty(rsc_id)) { rsc_id = getenv("OCF_RESOURCE_INSTANCE"); if (pcmk__str_empty(rsc_id)) { return NULL; } } return crm_strdup_printf("master-%s", rsc_id); } /*! * \internal * \brief Get the value of a node attribute * * \param[in] node Node to get attribute for * \param[in] name Name of node attribute to get * \param[in] target If this is \c PCMK_VALUE_HOST and \p node is a guest * (bundle) node, get the value from the guest's host, * otherwise get the value from \p node itself * \param[in] node_type If getting the value from \p node's host, this * indicates whether to check the current or assigned host * * \return Value of \p name attribute for \p node */ const char * pcmk__node_attr(const pcmk_node_t *node, const char *name, const char *target, enum pcmk__rsc_node node_type) { // @TODO accept a group of enum pcmk__rsc_node flags as node_type const char *value = NULL; // Attribute value to return const char *node_type_s = NULL; // Readable equivalent of node_type const pcmk_node_t *host = NULL; const pcmk_resource_t *container = NULL; if ((node == NULL) || (name == NULL)) { return NULL; } /* Check the node's own attributes unless this is a guest (bundle) node with * the container host as the attribute target. */ if (!pcmk__is_guest_or_bundle_node(node) || !pcmk__str_eq(target, PCMK_VALUE_HOST, pcmk__str_casei)) { value = g_hash_table_lookup(node->priv->attrs, name); crm_trace("%s='%s' on %s", name, pcmk__s(value, ""), pcmk__node_name(node)); return value; } /* This resource needs attributes set for the container's host instead of * for the container itself (useful when the container uses the host's * storage). */ container = node->priv->remote->priv->launcher; switch (node_type) { case pcmk__rsc_node_assigned: host = container->priv->assigned_node; if (host == NULL) { crm_trace("Skipping %s lookup for %s because " "its container %s is unassigned", name, pcmk__node_name(node), container->id); return NULL; } node_type_s = "assigned"; break; case pcmk__rsc_node_current: if (container->priv->active_nodes != NULL) { host = container->priv->active_nodes->data; } if (host == NULL) { crm_trace("Skipping %s lookup for %s because " "its container %s is inactive", name, pcmk__node_name(node), container->id); return NULL; } node_type_s = "current"; break; default: // Add support for other enum pcmk__rsc_node values if needed pcmk__assert(false); break; } value = g_hash_table_lookup(host->priv->attrs, name); crm_trace("%s='%s' for %s on %s container host %s", name, pcmk__s(value, ""), pcmk__node_name(node), node_type_s, pcmk__node_name(host)); return value; } diff --git a/lib/common/iso8601.c b/lib/common/iso8601.c index b738c0b285..b3ffa49060 100644 --- a/lib/common/iso8601.c +++ b/lib/common/iso8601.c @@ -1,2255 +1,2354 @@ /* * Copyright 2005-2025 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. */ /* * References: * https://en.wikipedia.org/wiki/ISO_8601 * http://www.staff.science.uu.nl/~gent0113/calendar/isocalendar.htm */ #include #include #include #include #include #include // INT_MIN, INT_MAX #include #include #include #include #include "crmcommon_private.h" /* * Andrew's code was originally written for OSes whose "struct tm" contains: * long tm_gmtoff; :: Seconds east of UTC * const char *tm_zone; :: Timezone abbreviation * Some OSes lack these, instead having: * time_t (or long) timezone; :: "difference between UTC and local standard time" * char *tzname[2] = { "...", "..." }; * I (David Lee) confess to not understanding the details. So my attempted * generalisations for where their use is necessary may be flawed. * * 1. Does "difference between ..." subtract the same or opposite way? * 2. Should it use "altzone" instead of "timezone"? * 3. Should it use tzname[0] or tzname[1]? Interaction with timezone/altzone? */ #if defined(HAVE_STRUCT_TM_TM_GMTOFF) # define GMTOFF(tm) ((tm)->tm_gmtoff) #else /* Note: extern variable; macro argument not actually used. */ # define GMTOFF(tm) (-timezone+daylight) #endif #define HOUR_SECONDS (60 * 60) #define DAY_SECONDS (HOUR_SECONDS * 24) /*! * \internal * \brief Validate a seconds/microseconds tuple * * The microseconds value must be in the correct range, and if both are nonzero * they must have the same sign. * * \param[in] sec Seconds * \param[in] usec Microseconds * * \return true if the seconds/microseconds tuple is valid, or false otherwise */ #define valid_sec_usec(sec, usec) \ ((QB_ABS(usec) < QB_TIME_US_IN_SEC) \ && (((sec) == 0) || ((usec) == 0) || (((sec) < 0) == ((usec) < 0)))) // A date/time or duration struct crm_time_s { int years; // Calendar year (date/time) or number of years (duration) int months; // Number of months (duration only) int days; // Ordinal day of year (date/time) or number of days (duration) int seconds; // Seconds of day (date/time) or number of seconds (duration) int offset; // Seconds offset from UTC (date/time only) bool duration; // True if duration }; static crm_time_t *parse_date(const char *date_str); static crm_time_t * crm_get_utc_time(const crm_time_t *dt) { crm_time_t *utc = NULL; if (dt == NULL) { errno = EINVAL; return NULL; } utc = crm_time_new_undefined(); utc->years = dt->years; utc->days = dt->days; utc->seconds = dt->seconds; utc->offset = 0; if (dt->offset) { crm_time_add_seconds(utc, -dt->offset); } else { /* Durations (which are the only things that can include months, never have a timezone */ utc->months = dt->months; } crm_time_log(LOG_TRACE, "utc-source", dt, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); crm_time_log(LOG_TRACE, "utc-target", utc, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); return utc; } crm_time_t * crm_time_new(const char *date_time) { tzset(); if (date_time == NULL) { return pcmk__copy_timet(time(NULL)); } return parse_date(date_time); } /*! * \brief Allocate memory for an uninitialized time object * * \return Newly allocated time object * \note The caller is responsible for freeing the return value using * crm_time_free(). */ crm_time_t * crm_time_new_undefined(void) { return (crm_time_t *) pcmk__assert_alloc(1, sizeof(crm_time_t)); } /*! * \brief Check whether a time object has been initialized yet * * \param[in] t Time object to check * * \return TRUE if time object has been initialized, FALSE otherwise */ bool crm_time_is_defined(const crm_time_t *t) { // Any nonzero member indicates something has been done to t return (t != NULL) && (t->years || t->months || t->days || t->seconds || t->offset || t->duration); } void crm_time_free(crm_time_t * dt) { if (dt == NULL) { return; } free(dt); } static int year_days(int year) { int d = 365; if (crm_time_leapyear(year)) { d++; } return d; } /* From http://myweb.ecu.edu/mccartyr/ISOwdALG.txt : * * 5. Find the Jan1Weekday for Y (Monday=1, Sunday=7) * YY = (Y-1) % 100 * C = (Y-1) - YY * G = YY + YY/4 * Jan1Weekday = 1 + (((((C / 100) % 4) x 5) + G) % 7) */ int crm_time_january1_weekday(int year) { int YY = (year - 1) % 100; int C = (year - 1) - YY; int G = YY + YY / 4; int jan1 = 1 + (((((C / 100) % 4) * 5) + G) % 7); crm_trace("YY=%d, C=%d, G=%d", YY, C, G); crm_trace("January 1 %.4d: %d", year, jan1); return jan1; } int crm_time_weeks_in_year(int year) { int weeks = 52; int jan1 = crm_time_january1_weekday(year); /* if jan1 == thursday */ if (jan1 == 4) { weeks++; } else { jan1 = crm_time_january1_weekday(year + 1); /* if dec31 == thursday aka. jan1 of next year is a friday */ if (jan1 == 5) { weeks++; } } return weeks; } // Jan-Dec plus Feb of leap years static int month_days[13] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29 }; /*! * \brief Return number of days in given month of given year * * \param[in] month Ordinal month (1-12) * \param[in] year Gregorian year * * \return Number of days in given month (0 if given month or year is invalid) */ int crm_time_days_in_month(int month, int year) { if ((month < 1) || (month > 12) || (year < 1)) { return 0; } if ((month == 2) && crm_time_leapyear(year)) { month = 13; } return month_days[month - 1]; } bool crm_time_leapyear(int year) { gboolean is_leap = FALSE; if (year % 4 == 0) { is_leap = TRUE; } if (year % 100 == 0 && year % 400 != 0) { is_leap = FALSE; } return is_leap; } /*! * \internal * \brief Get ordinal day number of year corresponding to given date * * \param[in] y Year * \param[in] m Month (1-12) * \param[in] d Day of month (1-31) * * \return Day number of year \p y corresponding to month \p m and day \p d, * or 0 for invalid arguments */ static int get_ordinal_days(uint32_t y, uint32_t m, uint32_t d) { int result = 0; CRM_CHECK((y > 0) && (y <= INT_MAX) && (m >= 1) && (m <= 12) && (d >= 1) && (d <= 31), return 0); result = d; for (int lpc = 1; lpc < m; lpc++) { result += crm_time_days_in_month(lpc, y); } return result; } void crm_time_log_alias(int log_level, const char *file, const char *function, int line, const char *prefix, const crm_time_t *date_time, int flags) { char *date_s = crm_time_as_string(date_time, flags); if (log_level == LOG_STDOUT) { printf("%s%s%s\n", (prefix? prefix : ""), (prefix? ": " : ""), date_s); } else { do_crm_log_alias(log_level, file, function, line, "%s%s%s", (prefix? prefix : ""), (prefix? ": " : ""), date_s); } free(date_s); } static void crm_time_get_sec(int sec, uint32_t *h, uint32_t *m, uint32_t *s) { uint32_t hours, minutes, seconds; seconds = QB_ABS(sec); hours = seconds / HOUR_SECONDS; seconds -= HOUR_SECONDS * hours; minutes = seconds / 60; seconds -= 60 * minutes; crm_trace("%d == %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, sec, hours, minutes, seconds); *h = hours; *m = minutes; *s = seconds; } int crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m, uint32_t *s) { crm_time_get_sec(dt->seconds, h, m, s); return TRUE; } int crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m) { uint32_t s; crm_time_get_sec(dt->seconds, h, m, &s); return TRUE; } long long crm_time_get_seconds(const crm_time_t *dt) { int lpc; crm_time_t *utc = NULL; long long in_seconds = 0; if (dt == NULL) { return 0; } // @TODO This is inefficient if dt is already in UTC utc = crm_get_utc_time(dt); if (utc == NULL) { return 0; } // @TODO We should probably use <= if dt is a duration for (lpc = 1; lpc < utc->years; lpc++) { long long dmax = year_days(lpc); in_seconds += DAY_SECONDS * dmax; } /* utc->months can be set only for durations. By definition, the value * varies depending on the (unknown) start date to which the duration will * be applied. Assume 30-day months so that something vaguely sane happens * in this case. */ if (utc->months > 0) { in_seconds += DAY_SECONDS * 30 * (long long) (utc->months); } if (utc->days > 0) { in_seconds += DAY_SECONDS * (long long) (utc->days - 1); } in_seconds += utc->seconds; crm_time_free(utc); return in_seconds; } #define EPOCH_SECONDS 62135596800ULL /* Calculated using crm_time_get_seconds() */ long long crm_time_get_seconds_since_epoch(const crm_time_t *dt) { return (dt == NULL)? 0 : (crm_time_get_seconds(dt) - EPOCH_SECONDS); } int crm_time_get_gregorian(const crm_time_t *dt, uint32_t *y, uint32_t *m, uint32_t *d) { int months = 0; int days = dt->days; if(dt->years != 0) { for (months = 1; months <= 12 && days > 0; months++) { int mdays = crm_time_days_in_month(months, dt->years); if (mdays >= days) { break; } else { days -= mdays; } } } else if (dt->months) { /* This is a duration including months, don't convert the days field */ months = dt->months; } else { /* This is a duration not including months, still don't convert the days field */ } *y = dt->years; *m = months; *d = days; crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days); return TRUE; } int crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d) { *y = dt->years; *d = dt->days; return TRUE; } int crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w, uint32_t *d) { /* * Monday 29 December 2008 is written "2009-W01-1" * Sunday 3 January 2010 is written "2009-W53-7" */ int year_num = 0; int jan1 = crm_time_january1_weekday(dt->years); int h = -1; CRM_CHECK(dt->days > 0, return FALSE); /* 6. Find the Weekday for Y M D */ h = dt->days + jan1 - 1; *d = 1 + ((h - 1) % 7); /* 7. Find if Y M D falls in YearNumber Y-1, WeekNumber 52 or 53 */ if (dt->days <= (8 - jan1) && jan1 > 4) { crm_trace("year--, jan1=%d", jan1); year_num = dt->years - 1; *w = crm_time_weeks_in_year(year_num); } else { year_num = dt->years; } /* 8. Find if Y M D falls in YearNumber Y+1, WeekNumber 1 */ if (year_num == dt->years) { int dmax = year_days(year_num); int correction = 4 - *d; if ((dmax - dt->days) < correction) { crm_trace("year++, jan1=%d, i=%d vs. %d", jan1, dmax - dt->days, correction); year_num = dt->years + 1; *w = 1; } } /* 9. Find if Y M D falls in YearNumber Y, WeekNumber 1 through 53 */ if (year_num == dt->years) { int j = dt->days + (7 - *d) + (jan1 - 1); *w = j / 7; if (jan1 > 4) { *w -= 1; } } *y = year_num; crm_trace("Converted %.4d-%.3d to %.4" PRIu32 "-W%.2" PRIu32 "-%" PRIu32, dt->years, dt->days, *y, *w, *d); return TRUE; } #define DATE_MAX 128 /*! * \internal * \brief Print "." to a buffer * - * \param[in] sec Seconds - * \param[in] usec Microseconds (must be of same sign as \p sec and of - * absolute value less than \p QB_TIME_US_IN_SEC) - * \param[in,out] buf Result buffer - * \param[in,out] offset Current offset within \p buf + * \param[in] sec Seconds + * \param[in] usec Microseconds (must be of same sign as \p sec and of + * absolute value less than \c QB_TIME_US_IN_SEC) + * \param[in,out] buf Result buffer */ static inline void -sec_usec_as_string(long long sec, int usec, char *buf, size_t *offset) +sec_usec_as_string(long long sec, int usec, GString *buf) { - *offset += snprintf(buf + *offset, DATE_MAX - *offset, "%s%lld.%06d", - ((sec == 0) && (usec < 0))? "-" : "", - sec, QB_ABS(usec)); + /* A negative value smaller than -1 second should have the negative sign + * before the 0, not before the usec part + */ + if ((sec == 0) && (usec < 0)) { + g_string_append_c(buf, '-'); + } + g_string_append_printf(buf, "%lld.%06d", sec, QB_ABS(usec)); } /*! * \internal * \brief Get a string representation of a duration * - * \param[in] dt Time object to interpret as a duration - * \param[in] usec Microseconds to add to \p dt - * \param[in] show_usec Whether to include microseconds in \p result - * \param[out] result Where to store the result string + * \param[in] dt Time object to interpret as a duration + * \param[in] usec Microseconds to add to \p dt + * \param[in] show_usec Whether to include microseconds in \p buf + * \param[in,out] buf Result buffer */ static void -crm_duration_as_string(const crm_time_t *dt, int usec, bool show_usec, - char *result) +duration_as_string(const crm_time_t *dt, int usec, bool show_usec, GString *buf) { - size_t offset = 0; - pcmk__assert(valid_sec_usec(dt->seconds, usec)); if (dt->years) { - offset += snprintf(result + offset, DATE_MAX - offset, "%4d year%s ", - dt->years, pcmk__plural_s(dt->years)); + g_string_append_printf(buf, "%4d year%s ", + dt->years, pcmk__plural_s(dt->years)); } if (dt->months) { - offset += snprintf(result + offset, DATE_MAX - offset, "%2d month%s ", - dt->months, pcmk__plural_s(dt->months)); + g_string_append_printf(buf, "%2d month%s ", + dt->months, pcmk__plural_s(dt->months)); } if (dt->days) { - offset += snprintf(result + offset, DATE_MAX - offset, "%2d day%s ", - dt->days, pcmk__plural_s(dt->days)); + g_string_append_printf(buf, "%2d day%s ", + dt->days, pcmk__plural_s(dt->days)); } // At least print seconds (and optionally usecs) - if ((offset == 0) || (dt->seconds != 0) || (show_usec && (usec != 0))) { + if ((buf->len == 0) || (dt->seconds != 0) || (show_usec && (usec != 0))) { if (show_usec) { - sec_usec_as_string(dt->seconds, usec, result, &offset); + sec_usec_as_string(dt->seconds, usec, buf); } else { - offset += snprintf(result + offset, DATE_MAX - offset, "%d", - dt->seconds); + g_string_append_printf(buf, "%d", dt->seconds); } - offset += snprintf(result + offset, DATE_MAX - offset, " second%s", - pcmk__plural_s(dt->seconds)); + g_string_append_printf(buf, " second%s", pcmk__plural_s(dt->seconds)); } // More than one minute, so provide a more readable breakdown into units if (QB_ABS(dt->seconds) >= 60) { uint32_t h = 0; uint32_t m = 0; uint32_t s = 0; uint32_t u = QB_ABS(usec); bool print_sec_component = false; crm_time_get_sec(dt->seconds, &h, &m, &s); print_sec_component = ((s != 0) || (show_usec && (u != 0))); - offset += snprintf(result + offset, DATE_MAX - offset, " ("); + g_string_append(buf, " ("); if (h) { - offset += snprintf(result + offset, DATE_MAX - offset, - "%" PRIu32 " hour%s%s", h, pcmk__plural_s(h), - ((m != 0) || print_sec_component)? " " : ""); + g_string_append_printf(buf, "%" PRIu32 " hour%s", + h, pcmk__plural_s(h)); + + if ((m != 0) || print_sec_component) { + g_string_append_c(buf, ' '); + } } if (m) { - offset += snprintf(result + offset, DATE_MAX - offset, - "%" PRIu32 " minute%s%s", m, pcmk__plural_s(m), - print_sec_component? " " : ""); + g_string_append_printf(buf, "%" PRIu32 " minute%s", + m, pcmk__plural_s(m)); + + if (print_sec_component) { + g_string_append_c(buf, ' '); + } } if (print_sec_component) { if (show_usec) { - sec_usec_as_string(s, u, result, &offset); + sec_usec_as_string(s, u, buf); } else { - offset += snprintf(result + offset, DATE_MAX - offset, - "%" PRIu32, s); + g_string_append_printf(buf, "%" PRIu32, s); } - offset += snprintf(result + offset, DATE_MAX - offset, " second%s", - pcmk__plural_s(dt->seconds)); + g_string_append_printf(buf, " second%s", + pcmk__plural_s(dt->seconds)); } - offset += snprintf(result + offset, DATE_MAX - offset, ")"); + g_string_append_c(buf, ')'); } } /*! * \internal * \brief Get a string representation of a time object * - * \param[in] dt Time to convert to string - * \param[in] usec Microseconds to add to \p dt - * \param[in] flags Group of \p crm_time_* string format options - * \param[out] result Where to store the result string + * \param[in] dt Time to convert to string + * \param[in] usec Microseconds to add to \p dt + * \param[in] flags Group of \c crm_time_* string format options * - * \note \p result must be of size \p DATE_MAX or larger. + * \return Newly allocated string representation of \p dt plus \p usec + * + * \note The caller is responsible for freeing the return value using \c free(). */ -static void -time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags, - char *result) +static char * +time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags) { crm_time_t *utc = NULL; - size_t offset = 0; + GString *buf = NULL; + char *result = NULL; if (!crm_time_is_defined(dt)) { - strcpy(result, ""); - return; + return pcmk__str_copy(""); } pcmk__assert(valid_sec_usec(dt->seconds, usec)); + buf = g_string_sized_new(128); + /* Simple cases: as duration, seconds, or seconds since epoch. * These never depend on time zone. */ if (pcmk_is_set(flags, crm_time_log_duration)) { - crm_duration_as_string(dt, usec, pcmk_is_set(flags, crm_time_usecs), - result); - return; + duration_as_string(dt, usec, pcmk_is_set(flags, crm_time_usecs), buf); + goto done; } if (pcmk_any_flags_set(flags, crm_time_seconds|crm_time_epoch)) { long long seconds = 0; if (pcmk_is_set(flags, crm_time_seconds)) { seconds = crm_time_get_seconds(dt); } else { seconds = crm_time_get_seconds_since_epoch(dt); } if (pcmk_is_set(flags, crm_time_usecs)) { - sec_usec_as_string(seconds, usec, result, &offset); + sec_usec_as_string(seconds, usec, buf); } else { - snprintf(result, DATE_MAX, "%lld", seconds); + g_string_append_printf(buf, "%lld", seconds); } - return; + goto done; } // Convert to UTC if local timezone was not requested if ((dt->offset != 0) && !pcmk_is_set(flags, crm_time_log_with_timezone)) { crm_trace("UTC conversion"); utc = crm_get_utc_time(dt); dt = utc; } // As readable string if (pcmk_is_set(flags, crm_time_log_date)) { if (pcmk_is_set(flags, crm_time_weeks)) { // YYYY-WW-D uint32_t y = 0; uint32_t w = 0; uint32_t d = 0; if (crm_time_get_isoweek(dt, &y, &w, &d)) { - offset += snprintf(result + offset, DATE_MAX - offset, - "%" PRIu32 "-W%.2" PRIu32 "-%" PRIu32, - y, w, d); + g_string_append_printf(buf, + "%" PRIu32 "-W%.2" PRIu32 "-%" PRIu32, + y, w, d); } } else if (pcmk_is_set(flags, crm_time_ordinal)) { // YYYY-DDD uint32_t y = 0; uint32_t d = 0; if (crm_time_get_ordinal(dt, &y, &d)) { - offset += snprintf(result + offset, DATE_MAX - offset, - "%" PRIu32 "-%.3" PRIu32, y, d); + g_string_append_printf(buf, "%" PRIu32 "-%.3" PRIu32, y, d); } } else { // YYYY-MM-DD uint32_t y = 0; uint32_t m = 0; uint32_t d = 0; if (crm_time_get_gregorian(dt, &y, &m, &d)) { - offset += snprintf(result + offset, DATE_MAX - offset, - "%.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, - y, m, d); + g_string_append_printf(buf, + "%.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, + y, m, d); } } } if (pcmk_is_set(flags, crm_time_log_timeofday)) { uint32_t h = 0, m = 0, s = 0; - if (offset > 0) { - offset += snprintf(result + offset, DATE_MAX - offset, " "); + if (buf->len > 0) { + g_string_append_c(buf, ' '); } if (crm_time_get_timeofday(dt, &h, &m, &s)) { - offset += snprintf(result + offset, DATE_MAX - offset, - "%.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, - h, m, s); + g_string_append_printf(buf, + "%.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, + h, m, s); if (pcmk_is_set(flags, crm_time_usecs)) { - offset += snprintf(result + offset, DATE_MAX - offset, - ".%06" PRIu32, QB_ABS(usec)); + g_string_append_printf(buf, ".%06" PRIu32, QB_ABS(usec)); } } if (pcmk_is_set(flags, crm_time_log_with_timezone) && (dt->offset != 0)) { crm_time_get_sec(dt->offset, &h, &m, &s); - offset += snprintf(result + offset, DATE_MAX - offset, - " %c%.2" PRIu32 ":%.2" PRIu32, - ((dt->offset < 0)? '-' : '+'), h, m); + g_string_append_printf(buf, " %c%.2" PRIu32 ":%.2" PRIu32, + ((dt->offset < 0)? '-' : '+'), h, m); + } else { - offset += snprintf(result + offset, DATE_MAX - offset, "Z"); + g_string_append_c(buf, 'Z'); } } +done: crm_time_free(utc); + result = pcmk__str_copy(buf->str); + g_string_free(buf, TRUE); + return result; } /*! * \brief Get a string representation of a \p crm_time_t object * * \param[in] dt Time to convert to string * \param[in] flags Group of \p crm_time_* string format options * * \note The caller is responsible for freeing the return value using \p free(). */ char * crm_time_as_string(const crm_time_t *dt, int flags) { - char result[DATE_MAX] = { '\0', }; - - time_as_string_common(dt, 0, flags, result); - return pcmk__str_copy(result); + return time_as_string_common(dt, 0, flags); } /*! * \internal * \brief Determine number of seconds from an hour:minute:second string * * \param[in] time_str Time specification string * \param[out] result Number of seconds equivalent to time_str * * \return TRUE if specification was valid, FALSE (and set errno) otherwise * \note This may return the number of seconds in a day (which is out of bounds * for a time object) if given 24:00:00. */ static bool crm_time_parse_sec(const char *time_str, int *result) { int rc; uint32_t hour = 0; uint32_t minute = 0; uint32_t second = 0; *result = 0; // Must have at least hour, but minutes and seconds are optional rc = sscanf(time_str, "%" SCNu32 ":%" SCNu32 ":%" SCNu32, &hour, &minute, &second); if (rc == 1) { rc = sscanf(time_str, "%2" SCNu32 "%2" SCNu32 "%2" SCNu32, &hour, &minute, &second); } if (rc == 0) { crm_err("%s is not a valid ISO 8601 time specification", time_str); errno = EINVAL; return FALSE; } crm_trace("Got valid time: %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, hour, minute, second); if ((hour == 24) && (minute == 0) && (second == 0)) { // Equivalent to 00:00:00 of next day, return number of seconds in day } else if (hour >= 24) { crm_err("%s is not a valid ISO 8601 time specification " "because %" PRIu32 " is not a valid hour", time_str, hour); errno = EINVAL; return FALSE; } if (minute >= 60) { crm_err("%s is not a valid ISO 8601 time specification " "because %" PRIu32 " is not a valid minute", time_str, minute); errno = EINVAL; return FALSE; } if (second >= 60) { crm_err("%s is not a valid ISO 8601 time specification " "because %" PRIu32 " is not a valid second", time_str, second); errno = EINVAL; return FALSE; } *result = (hour * HOUR_SECONDS) + (minute * 60) + second; return TRUE; } static bool crm_time_parse_offset(const char *offset_str, int *offset) { tzset(); if (offset_str == NULL) { // Use local offset #if defined(HAVE_STRUCT_TM_TM_GMTOFF) time_t now = time(NULL); struct tm *now_tm = localtime(&now); #endif int h_offset = GMTOFF(now_tm) / HOUR_SECONDS; int m_offset = (GMTOFF(now_tm) - (HOUR_SECONDS * h_offset)) / 60; if (h_offset < 0 && m_offset < 0) { m_offset = 0 - m_offset; } *offset = (HOUR_SECONDS * h_offset) + (60 * m_offset); return TRUE; } if (offset_str[0] == 'Z') { // @TODO invalid if anything after? *offset = 0; return TRUE; } *offset = 0; if ((offset_str[0] == '+') || (offset_str[0] == '-') || isdigit((int)offset_str[0])) { gboolean negate = FALSE; if (offset_str[0] == '+') { offset_str++; } else if (offset_str[0] == '-') { negate = TRUE; offset_str++; } if (crm_time_parse_sec(offset_str, offset) == FALSE) { return FALSE; } if (negate) { *offset = 0 - *offset; } } // @TODO else invalid? return TRUE; } /*! * \internal * \brief Parse the time portion of an ISO 8601 date/time string * * \param[in] time_str Time portion of specification (after any 'T') * \param[in,out] a_time Time object to parse into * * \return TRUE if valid time was parsed, FALSE (and set errno) otherwise * \note This may add a day to a_time (if the time is 24:00:00). */ static bool crm_time_parse(const char *time_str, crm_time_t *a_time) { uint32_t h, m, s; char *offset_s = NULL; tzset(); if (time_str) { if (crm_time_parse_sec(time_str, &(a_time->seconds)) == FALSE) { return FALSE; } offset_s = strstr(time_str, "Z"); if (offset_s == NULL) { offset_s = strstr(time_str, " "); if (offset_s) { while (isspace(offset_s[0])) { offset_s++; } } } } if (crm_time_parse_offset(offset_s, &(a_time->offset)) == FALSE) { return FALSE; } crm_time_get_sec(a_time->offset, &h, &m, &s); crm_trace("Got tz: %c%2." PRIu32 ":%.2" PRIu32, (a_time->offset < 0)? '-' : '+', h, m); if (a_time->seconds == DAY_SECONDS) { // 24:00:00 == 00:00:00 of next day a_time->seconds = 0; crm_time_add_days(a_time, 1); } return TRUE; } /* * \internal * \brief Parse a time object from an ISO 8601 date/time specification * * \param[in] date_str ISO 8601 date/time specification (or * \c PCMK__VALUE_EPOCH) * * \return New time object on success, NULL (and set errno) otherwise */ static crm_time_t * parse_date(const char *date_str) { const char *time_s = NULL; crm_time_t *dt = NULL; uint32_t year = 0U; uint32_t month = 0U; uint32_t day = 0U; uint32_t week = 0U; int rc = 0; if (pcmk__str_empty(date_str)) { crm_err("No ISO 8601 date/time specification given"); goto invalid; } if ((date_str[0] == 'T') || ((strlen(date_str) > 2) && (date_str[2] == ':'))) { /* Just a time supplied - Infer current date */ dt = crm_time_new(NULL); if (date_str[0] == 'T') { time_s = date_str + 1; } else { time_s = date_str; } goto parse_time; } dt = crm_time_new_undefined(); if ((strncasecmp(PCMK__VALUE_EPOCH, date_str, 5) == 0) && ((date_str[5] == '\0') || (date_str[5] == '/') || isspace(date_str[5]))) { dt->days = 1; dt->years = 1970; crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday); return dt; } /* YYYY-MM-DD */ rc = sscanf(date_str, "%" SCNu32 "-%" SCNu32 "-%" SCNu32 "", &year, &month, &day); if (rc == 1) { /* YYYYMMDD */ rc = sscanf(date_str, "%4" SCNu32 "%2" SCNu32 "%2" SCNu32 "", &year, &month, &day); } if (rc == 3) { if ((month < 1U) || (month > 12U)) { crm_err("'%s' is not a valid ISO 8601 date/time specification " "because '%" PRIu32 "' is not a valid month", date_str, month); goto invalid; } else if ((year < 1U) || (year > INT_MAX)) { crm_err("'%s' is not a valid ISO 8601 date/time specification " "because '%" PRIu32 "' is not a valid year", date_str, year); goto invalid; } else if ((day < 1) || (day > INT_MAX) || (day > crm_time_days_in_month(month, year))) { crm_err("'%s' is not a valid ISO 8601 date/time specification " "because '%" PRIu32 "' is not a valid day of the month", date_str, day); goto invalid; } else { dt->years = year; dt->days = get_ordinal_days(year, month, day); crm_trace("Parsed Gregorian date '%.4" PRIu32 "-%.3d' " "from date string '%s'", year, dt->days, date_str); } goto parse_time; } /* YYYY-DDD */ rc = sscanf(date_str, "%" SCNu32 "-%" SCNu32, &year, &day); if (rc == 2) { if ((year < 1U) || (year > INT_MAX)) { crm_err("'%s' is not a valid ISO 8601 date/time specification " "because '%" PRIu32 "' is not a valid year", date_str, year); goto invalid; } else if ((day < 1U) || (day > INT_MAX) || (day > year_days(year))) { crm_err("'%s' is not a valid ISO 8601 date/time specification " "because '%" PRIu32 "' is not a valid day of year %" PRIu32 " (1-%d)", date_str, day, year, year_days(year)); goto invalid; } crm_trace("Parsed ordinal year %d and days %d from date string '%s'", year, day, date_str); dt->days = day; dt->years = year; goto parse_time; } /* YYYY-Www-D */ rc = sscanf(date_str, "%" SCNu32 "-W%" SCNu32 "-%" SCNu32, &year, &week, &day); if (rc == 3) { if ((week < 1U) || (week > crm_time_weeks_in_year(year))) { crm_err("'%s' is not a valid ISO 8601 date/time specification " "because '%" PRIu32 "' is not a valid week of year %" PRIu32 " (1-%d)", date_str, week, year, crm_time_weeks_in_year(year)); goto invalid; } else if ((day < 1U) || (day > 7U)) { crm_err("'%s' is not a valid ISO 8601 date/time specification " "because '%" PRIu32 "' is not a valid day of the week", date_str, day); goto invalid; } else { /* * See https://en.wikipedia.org/wiki/ISO_week_date * * Monday 29 December 2008 is written "2009-W01-1" * Sunday 3 January 2010 is written "2009-W53-7" * Saturday 27 September 2008 is written "2008-W37-6" * * If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it * is in week 1. If 1 January is on a Friday, Saturday or Sunday, * it is in week 52 or 53 of the previous year. */ int jan1 = crm_time_january1_weekday(year); crm_trace("Parsed year %" PRIu32 " (Jan 1 = %d), week %" PRIu32 ", and day %" PRIu32 " from date string '%s'", year, jan1, week, day, date_str); dt->years = year; crm_time_add_days(dt, (week - 1) * 7); if (jan1 <= 4) { crm_time_add_days(dt, 1 - jan1); } else { crm_time_add_days(dt, 8 - jan1); } crm_time_add_days(dt, day); } goto parse_time; } crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str); goto invalid; parse_time: if (time_s == NULL) { time_s = date_str + strspn(date_str, "0123456789-W"); if ((time_s[0] == ' ') || (time_s[0] == 'T')) { ++time_s; } else { time_s = NULL; } } if ((time_s != NULL) && (crm_time_parse(time_s, dt) == FALSE)) { goto invalid; } crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday); if (crm_time_check(dt) == FALSE) { crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str); goto invalid; } return dt; invalid: crm_time_free(dt); errno = EINVAL; return NULL; } // Parse an ISO 8601 numeric value and return number of characters consumed static int parse_int(const char *str, int *result) { unsigned int lpc; int offset = (str[0] == 'T')? 1 : 0; bool negate = false; *result = 0; // @TODO This cannot handle combinations of these characters switch (str[offset]) { case '.': case ',': return 0; // Fractions are not supported case '-': negate = true; offset++; break; case '+': case ':': offset++; break; default: break; } for (lpc = 0; (lpc < 10) && isdigit(str[offset]); lpc++) { const int digit = str[offset++] - '0'; if ((*result * 10LL + digit) > INT_MAX) { return 0; // Overflow } *result = *result * 10 + digit; } if (negate) { *result = 0 - *result; } return (lpc > 0)? offset : 0; } /*! * \brief Parse a time duration from an ISO 8601 duration specification * * \param[in] period_s ISO 8601 duration specification (optionally followed by * whitespace, after which the rest of the string will be * ignored) * * \return New time object on success, NULL (and set errno) otherwise * \note It is the caller's responsibility to return the result using * crm_time_free(). */ crm_time_t * crm_time_parse_duration(const char *period_s) { gboolean is_time = FALSE; crm_time_t *diff = NULL; if (pcmk__str_empty(period_s)) { crm_err("No ISO 8601 time duration given"); goto invalid; } if (period_s[0] != 'P') { crm_err("'%s' is not a valid ISO 8601 time duration " "because it does not start with a 'P'", period_s); goto invalid; } if ((period_s[1] == '\0') || isspace(period_s[1])) { crm_err("'%s' is not a valid ISO 8601 time duration " "because nothing follows 'P'", period_s); goto invalid; } diff = crm_time_new_undefined(); for (const char *current = period_s + 1; current[0] && (current[0] != '/') && !isspace(current[0]); ++current) { int an_int = 0, rc; long long result = 0LL; if (current[0] == 'T') { /* A 'T' separates year/month/day from hour/minute/seconds. We don't * require it strictly, but just use it to differentiate month from * minutes. */ is_time = TRUE; continue; } // An integer must be next rc = parse_int(current, &an_int); if (rc == 0) { crm_err("'%s' is not a valid ISO 8601 time duration " "because no valid integer at '%s'", period_s, current); goto invalid; } current += rc; // A time unit must be next (we're not strict about the order) switch (current[0]) { case 'Y': diff->years = an_int; break; case 'M': if (!is_time) { // Months diff->months = an_int; } else { // Minutes result = diff->seconds + an_int * 60LL; if ((result < INT_MIN) || (result > INT_MAX)) { crm_err("'%s' is not a valid ISO 8601 time duration " "because integer at '%s' is too %s", period_s, current - rc, ((result > 0)? "large" : "small")); goto invalid; } else { diff->seconds = (int) result; } } break; case 'W': result = diff->days + an_int * 7LL; if ((result < INT_MIN) || (result > INT_MAX)) { crm_err("'%s' is not a valid ISO 8601 time duration " "because integer at '%s' is too %s", period_s, current - rc, ((result > 0)? "large" : "small")); goto invalid; } else { diff->days = (int) result; } break; case 'D': result = diff->days + (long long) an_int; if ((result < INT_MIN) || (result > INT_MAX)) { crm_err("'%s' is not a valid ISO 8601 time duration " "because integer at '%s' is too %s", period_s, current - rc, ((result > 0)? "large" : "small")); goto invalid; } else { diff->days = (int) result; } break; case 'H': result = diff->seconds + (long long) an_int * HOUR_SECONDS; if ((result < INT_MIN) || (result > INT_MAX)) { crm_err("'%s' is not a valid ISO 8601 time duration " "because integer at '%s' is too %s", period_s, current - rc, ((result > 0)? "large" : "small")); goto invalid; } else { diff->seconds = (int) result; } break; case 'S': result = diff->seconds + (long long) an_int; if ((result < INT_MIN) || (result > INT_MAX)) { crm_err("'%s' is not a valid ISO 8601 time duration " "because integer at '%s' is too %s", period_s, current - rc, ((result > 0)? "large" : "small")); goto invalid; } else { diff->seconds = (int) result; } break; case '\0': crm_err("'%s' is not a valid ISO 8601 time duration " "because no units after %d", period_s, an_int); goto invalid; default: crm_err("'%s' is not a valid ISO 8601 time duration " "because '%c' is not a valid time unit", period_s, current[0]); goto invalid; } } if (!crm_time_is_defined(diff)) { crm_err("'%s' is not a valid ISO 8601 time duration " "because no amounts and units given", period_s); goto invalid; } diff->duration = TRUE; return diff; invalid: crm_time_free(diff); errno = EINVAL; return NULL; } /*! * \brief Parse a time period from an ISO 8601 interval specification * * \param[in] period_str ISO 8601 interval specification (start/end, * start/duration, or duration/end) * * \return New time period object on success, NULL (and set errno) otherwise * \note The caller is responsible for freeing the result using * crm_time_free_period(). */ crm_time_period_t * crm_time_parse_period(const char *period_str) { const char *original = period_str; crm_time_period_t *period = NULL; if (pcmk__str_empty(period_str)) { crm_err("No ISO 8601 time period given"); goto invalid; } tzset(); period = pcmk__assert_alloc(1, sizeof(crm_time_period_t)); if (period_str[0] == 'P') { period->diff = crm_time_parse_duration(period_str); if (period->diff == NULL) { goto error; } } else { period->start = parse_date(period_str); if (period->start == NULL) { goto error; } } period_str = strstr(original, "/"); if (period_str) { ++period_str; if (period_str[0] == 'P') { if (period->diff != NULL) { crm_err("'%s' is not a valid ISO 8601 time period " "because it has two durations", original); goto invalid; } period->diff = crm_time_parse_duration(period_str); if (period->diff == NULL) { goto error; } } else { period->end = parse_date(period_str); if (period->end == NULL) { goto error; } } } else if (period->diff != NULL) { // Only duration given, assume start is now period->start = crm_time_new(NULL); } else { // Only start given crm_err("'%s' is not a valid ISO 8601 time period " "because it has no duration or ending time", original); goto invalid; } if (period->start == NULL) { period->start = crm_time_subtract(period->end, period->diff); } else if (period->end == NULL) { period->end = crm_time_add(period->start, period->diff); } if (crm_time_check(period->start) == FALSE) { crm_err("'%s' is not a valid ISO 8601 time period " "because the start is invalid", period_str); goto invalid; } if (crm_time_check(period->end) == FALSE) { crm_err("'%s' is not a valid ISO 8601 time period " "because the end is invalid", period_str); goto invalid; } return period; invalid: errno = EINVAL; error: crm_time_free_period(period); return NULL; } /*! * \brief Free a dynamically allocated time period object * * \param[in,out] period Time period to free */ void crm_time_free_period(crm_time_period_t *period) { if (period) { crm_time_free(period->start); crm_time_free(period->end); crm_time_free(period->diff); free(period); } } void crm_time_set(crm_time_t *target, const crm_time_t *source) { crm_trace("target=%p, source=%p", target, source); CRM_CHECK(target != NULL && source != NULL, return); target->years = source->years; target->days = source->days; target->months = source->months; /* Only for durations */ target->seconds = source->seconds; target->offset = source->offset; crm_time_log(LOG_TRACE, "source", source, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); crm_time_log(LOG_TRACE, "target", target, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); } static void ha_set_tm_time(crm_time_t *target, const struct tm *source) { int h_offset = 0; int m_offset = 0; /* Ensure target is fully initialized */ target->years = 0; target->months = 0; target->days = 0; target->seconds = 0; target->offset = 0; target->duration = FALSE; if (source->tm_year > 0) { /* years since 1900 */ target->years = 1900; crm_time_add_years(target, source->tm_year); } if (source->tm_yday >= 0) { /* days since January 1 [0-365] */ target->days = 1 + source->tm_yday; } if (source->tm_hour >= 0) { target->seconds += HOUR_SECONDS * source->tm_hour; } if (source->tm_min >= 0) { target->seconds += 60 * source->tm_min; } if (source->tm_sec >= 0) { target->seconds += source->tm_sec; } /* tm_gmtoff == offset from UTC in seconds */ h_offset = GMTOFF(source) / HOUR_SECONDS; m_offset = (GMTOFF(source) - (HOUR_SECONDS * h_offset)) / 60; crm_trace("Time offset is %lds (%.2d:%.2d)", GMTOFF(source), h_offset, m_offset); target->offset += HOUR_SECONDS * h_offset; target->offset += 60 * m_offset; } void crm_time_set_timet(crm_time_t *target, const time_t *source) { ha_set_tm_time(target, localtime(source)); } /*! * \internal * \brief Set one time object to another if the other is earlier * * \param[in,out] target Time object to set * \param[in] source Time object to use if earlier */ void pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source) { if ((target != NULL) && (source != NULL) && (!crm_time_is_defined(target) || (crm_time_compare(source, target) < 0))) { crm_time_set(target, source); } } crm_time_t * pcmk_copy_time(const crm_time_t *source) { crm_time_t *target = crm_time_new_undefined(); crm_time_set(target, source); return target; } /*! * \internal * \brief Convert a \p time_t time to a \p crm_time_t time * * \param[in] source Time to convert * * \return A \p crm_time_t object representing \p source */ crm_time_t * pcmk__copy_timet(time_t source) { crm_time_t *target = crm_time_new_undefined(); crm_time_set_timet(target, &source); return target; } crm_time_t * crm_time_add(const crm_time_t *dt, const crm_time_t *value) { crm_time_t *utc = NULL; crm_time_t *answer = NULL; if ((dt == NULL) || (value == NULL)) { errno = EINVAL; return NULL; } answer = pcmk_copy_time(dt); utc = crm_get_utc_time(value); if (utc == NULL) { crm_time_free(answer); return NULL; } crm_time_add_years(answer, utc->years); crm_time_add_months(answer, utc->months); crm_time_add_days(answer, utc->days); crm_time_add_seconds(answer, utc->seconds); crm_time_free(utc); return answer; } /*! * \internal * \brief Return the XML attribute name corresponding to a time component * * \param[in] component Component to check * * \return XML attribute name corresponding to \p component, or NULL if * \p component is invalid */ const char * pcmk__time_component_attr(enum pcmk__time_component component) { switch (component) { case pcmk__time_years: return PCMK_XA_YEARS; case pcmk__time_months: return PCMK_XA_MONTHS; case pcmk__time_weeks: return PCMK_XA_WEEKS; case pcmk__time_days: return PCMK_XA_DAYS; case pcmk__time_hours: return PCMK_XA_HOURS; case pcmk__time_minutes: return PCMK_XA_MINUTES; case pcmk__time_seconds: return PCMK_XA_SECONDS; default: return NULL; } } typedef void (*component_fn_t)(crm_time_t *, int); /*! * \internal * \brief Get the addition function corresponding to a time component * \param[in] component Component to check * * \return Addition function corresponding to \p component, or NULL if * \p component is invalid */ static component_fn_t component_fn(enum pcmk__time_component component) { switch (component) { case pcmk__time_years: return crm_time_add_years; case pcmk__time_months: return crm_time_add_months; case pcmk__time_weeks: return crm_time_add_weeks; case pcmk__time_days: return crm_time_add_days; case pcmk__time_hours: return crm_time_add_hours; case pcmk__time_minutes: return crm_time_add_minutes; case pcmk__time_seconds: return crm_time_add_seconds; default: return NULL; } } /*! * \internal * \brief Add the value of an XML attribute to a time object * * \param[in,out] t Time object to add to * \param[in] component Component of \p t to add to * \param[in] xml XML with value to add * * \return Standard Pacemaker return code */ int pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component, const xmlNode *xml) { long long value; const char *attr = pcmk__time_component_attr(component); component_fn_t add = component_fn(component); if ((t == NULL) || (attr == NULL) || (add == NULL)) { return EINVAL; } if (xml == NULL) { return pcmk_rc_ok; } if (pcmk__scan_ll(crm_element_value(xml, attr), &value, 0LL) != pcmk_rc_ok) { return pcmk_rc_unpack_error; } if ((value < INT_MIN) || (value > INT_MAX)) { return ERANGE; } if (value != 0LL) { add(t, (int) value); } return pcmk_rc_ok; } crm_time_t * crm_time_calculate_duration(const crm_time_t *dt, const crm_time_t *value) { crm_time_t *utc = NULL; crm_time_t *answer = NULL; if ((dt == NULL) || (value == NULL)) { errno = EINVAL; return NULL; } utc = crm_get_utc_time(value); if (utc == NULL) { return NULL; } answer = crm_get_utc_time(dt); if (answer == NULL) { crm_time_free(utc); return NULL; } answer->duration = TRUE; crm_time_add_years(answer, -utc->years); if(utc->months != 0) { crm_time_add_months(answer, -utc->months); } crm_time_add_days(answer, -utc->days); crm_time_add_seconds(answer, -utc->seconds); crm_time_free(utc); return answer; } crm_time_t * crm_time_subtract(const crm_time_t *dt, const crm_time_t *value) { crm_time_t *utc = NULL; crm_time_t *answer = NULL; if ((dt == NULL) || (value == NULL)) { errno = EINVAL; return NULL; } utc = crm_get_utc_time(value); if (utc == NULL) { return NULL; } answer = pcmk_copy_time(dt); crm_time_add_years(answer, -utc->years); if(utc->months != 0) { crm_time_add_months(answer, -utc->months); } crm_time_add_days(answer, -utc->days); crm_time_add_seconds(answer, -utc->seconds); crm_time_free(utc); return answer; } /*! * \brief Check whether a time object represents a sensible date/time * * \param[in] dt Date/time object to check * * \return \c true if years, days, and seconds are sensible, \c false otherwise */ bool crm_time_check(const crm_time_t *dt) { return (dt != NULL) && (dt->days > 0) && (dt->days <= year_days(dt->years)) && (dt->seconds >= 0) && (dt->seconds < DAY_SECONDS); } #define do_cmp_field(l, r, field) \ if(rc == 0) { \ if(l->field > r->field) { \ crm_trace("%s: %d > %d", \ #field, l->field, r->field); \ rc = 1; \ } else if(l->field < r->field) { \ crm_trace("%s: %d < %d", \ #field, l->field, r->field); \ rc = -1; \ } \ } int crm_time_compare(const crm_time_t *a, const crm_time_t *b) { int rc = 0; crm_time_t *t1 = crm_get_utc_time(a); crm_time_t *t2 = crm_get_utc_time(b); if ((t1 == NULL) && (t2 == NULL)) { rc = 0; } else if (t1 == NULL) { rc = -1; } else if (t2 == NULL) { rc = 1; } else { do_cmp_field(t1, t2, years); do_cmp_field(t1, t2, days); do_cmp_field(t1, t2, seconds); } crm_time_free(t1); crm_time_free(t2); return rc; } /*! * \brief Add a given number of seconds to a date/time or duration * * \param[in,out] a_time Date/time or duration to add seconds to * \param[in] extra Number of seconds to add */ void crm_time_add_seconds(crm_time_t *a_time, int extra) { int days = extra / DAY_SECONDS; pcmk__assert(a_time != NULL); crm_trace("Adding %d seconds (including %d whole day%s) to %d", extra, days, pcmk__plural_s(days), a_time->seconds); a_time->seconds += extra % DAY_SECONDS; // Check whether the addition crossed a day boundary if (a_time->seconds > DAY_SECONDS) { ++days; a_time->seconds -= DAY_SECONDS; } else if (a_time->seconds < 0) { --days; a_time->seconds += DAY_SECONDS; } crm_time_add_days(a_time, days); } #define ydays(t) (crm_time_leapyear((t)->years)? 366 : 365) /*! * \brief Add days to a date/time * * \param[in,out] a_time Time to modify * \param[in] extra Number of days to add (may be negative to subtract) */ void crm_time_add_days(crm_time_t *a_time, int extra) { pcmk__assert(a_time != NULL); crm_trace("Adding %d days to %.4d-%.3d", extra, a_time->years, a_time->days); if (extra > 0) { while ((a_time->days + (long long) extra) > ydays(a_time)) { if ((a_time->years + 1LL) > INT_MAX) { a_time->days = ydays(a_time); // Clip to latest we can handle return; } extra -= ydays(a_time); a_time->years++; } } else if (extra < 0) { const int min_days = a_time->duration? 0 : 1; while ((a_time->days + (long long) extra) < min_days) { if ((a_time->years - 1) < 1) { a_time->days = 1; // Clip to earliest we can handle (no BCE) return; } a_time->years--; extra += ydays(a_time); } } a_time->days += extra; } void crm_time_add_months(crm_time_t * a_time, int extra) { int lpc; uint32_t y, m, d, dmax; crm_time_get_gregorian(a_time, &y, &m, &d); crm_trace("Adding %d months to %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, extra, y, m, d); if (extra > 0) { for (lpc = extra; lpc > 0; lpc--) { m++; if (m == 13) { m = 1; y++; } } } else { for (lpc = -extra; lpc > 0; lpc--) { m--; if (m == 0) { m = 12; y--; } } } dmax = crm_time_days_in_month(m, y); if (dmax < d) { /* Preserve day-of-month unless the month doesn't have enough days */ d = dmax; } crm_trace("Calculated %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d); a_time->years = y; a_time->days = get_ordinal_days(y, m, d); crm_time_get_gregorian(a_time, &y, &m, &d); crm_trace("Got %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d); } void crm_time_add_minutes(crm_time_t * a_time, int extra) { crm_time_add_seconds(a_time, extra * 60); } void crm_time_add_hours(crm_time_t * a_time, int extra) { crm_time_add_seconds(a_time, extra * HOUR_SECONDS); } void crm_time_add_weeks(crm_time_t * a_time, int extra) { crm_time_add_days(a_time, extra * 7); } void crm_time_add_years(crm_time_t * a_time, int extra) { pcmk__assert(a_time != NULL); if ((extra > 0) && ((a_time->years + (long long) extra) > INT_MAX)) { a_time->years = INT_MAX; } else if ((extra < 0) && ((a_time->years + (long long) extra) < 1)) { a_time->years = 1; // Clip to earliest we can handle (no BCE) } else { a_time->years += extra; } } -static void -ha_get_tm_time(struct tm *target, const crm_time_t *source) -{ - *target = (struct tm) { - .tm_year = source->years - 1900, - .tm_mday = source->days, - .tm_sec = source->seconds % 60, - .tm_min = ( source->seconds / 60 ) % 60, - .tm_hour = source->seconds / HOUR_SECONDS, - .tm_isdst = -1, /* don't adjust */ - -#if defined(HAVE_STRUCT_TM_TM_GMTOFF) - .tm_gmtoff = source->offset -#endif - }; - mktime(target); -} - /* The high-resolution variant of time object was added to meet an immediate * need, and is kept internal API. * * @TODO The long-term goal is to come up with a clean, unified design for a * time type (or types) that meets all the various needs, to replace * crm_time_t, pcmk__time_hr_t, and struct timespec (in lrmd_cmd_t). */ -pcmk__time_hr_t * -pcmk__time_hr_convert(pcmk__time_hr_t *target, const crm_time_t *dt) +static pcmk__time_hr_t * +time_to_hr(const crm_time_t *dt) { pcmk__time_hr_t *hr_dt = NULL; - if (dt) { - hr_dt = target; - if (hr_dt == NULL) { - hr_dt = pcmk__assert_alloc(1, sizeof(pcmk__time_hr_t)); - } - - *hr_dt = (pcmk__time_hr_t) { - .years = dt->years, - .months = dt->months, - .days = dt->days, - .seconds = dt->seconds, - .offset = dt->offset, - .duration = dt->duration - }; - } + pcmk__assert(dt != NULL); + hr_dt = pcmk__assert_alloc(1, sizeof(pcmk__time_hr_t)); + hr_dt->years = dt->years; + hr_dt->months = dt->months; + hr_dt->days = dt->days; + hr_dt->seconds = dt->seconds; + hr_dt->offset = dt->offset; + hr_dt->duration = dt->duration; return hr_dt; } -void -pcmk__time_set_hr_dt(crm_time_t *target, const pcmk__time_hr_t *hr_dt) -{ - pcmk__assert((target != NULL) && (hr_dt != NULL)); - *target = (crm_time_t) { - .years = hr_dt->years, - .months = hr_dt->months, - .days = hr_dt->days, - .seconds = hr_dt->seconds, - .offset = hr_dt->offset, - .duration = hr_dt->duration - }; -} - /*! * \internal * \brief Return the current time as a high-resolution time * * \param[out] epoch If not NULL, this will be set to seconds since epoch * * \return Newly allocated high-resolution time set to the current time */ pcmk__time_hr_t * pcmk__time_hr_now(time_t *epoch) { struct timespec tv; crm_time_t dt; pcmk__time_hr_t *hr; qb_util_timespec_from_epoch_get(&tv); if (epoch != NULL) { *epoch = tv.tv_sec; } crm_time_set_timet(&dt, &(tv.tv_sec)); - hr = pcmk__time_hr_convert(NULL, &dt); - if (hr != NULL) { - hr->useconds = tv.tv_nsec / QB_TIME_NS_IN_USEC; - } + hr = time_to_hr(&dt); + hr->useconds = tv.tv_nsec / QB_TIME_NS_IN_USEC; return hr; } pcmk__time_hr_t * pcmk__time_hr_new(const char *date_time) { pcmk__time_hr_t *hr_dt = NULL; if (date_time == NULL) { hr_dt = pcmk__time_hr_now(NULL); } else { - crm_time_t *dt; + crm_time_t *dt = parse_date(date_time); - dt = parse_date(date_time); - hr_dt = pcmk__time_hr_convert(NULL, dt); + hr_dt = time_to_hr(dt); crm_time_free(dt); } return hr_dt; } void pcmk__time_hr_free(pcmk__time_hr_t * hr_dt) { free(hr_dt); } +static void +ha_get_tm_time(struct tm *target, const pcmk__time_hr_t *source) +{ + *target = (struct tm) { + .tm_year = source->years - 1900, + + /* source->days is day of year, but we assign it to tm_mday instead of + * tm_yday. mktime() fixes it. See the mktime(3) man page for details. + */ + .tm_mday = source->days, + + // mktime() converts this to hours/minutes/seconds appropriately + .tm_sec = source->seconds, + + // Don't adjust DST here; let mktime() try to determine DST status + .tm_isdst = -1, + +#if defined(HAVE_STRUCT_TM_TM_GMTOFF) + .tm_gmtoff = source->offset +#endif + }; + mktime(target); +} + /*! * \internal - * \brief Expand a date/time format string, including %N for nanoseconds + * \brief Convert a struct tm to a \c GDateTime + * + * \param[in] tm Time object to convert + * \param[in] offset Offset from UTC (in seconds) * - * \param[in] format Date/time format string as per strftime(3) with the - * addition of %N for nanoseconds + * \return Newly allocated \c GDateTime object corresponding to \p tm, or + * \c NULL on error + * + * \note The caller is responsible for freeing the return value using + * \c g_date_time_unref(). + */ +static GDateTime * +get_g_date_time(const struct tm *tm, int offset) +{ + // Accept an offset argument in case tm lacks a tm_gmtoff member + char buf[sizeof("+hh:mm")] = { '\0', }; + const char *offset_s = NULL; + + GTimeZone *tz = NULL; + GDateTime *dt = NULL; + + if (QB_ABS(offset) <= DAY_SECONDS) { + uint32_t hours = 0; + uint32_t minutes = 0; + uint32_t seconds = 0; + int rc = 0; + + crm_time_get_sec(offset, &hours, &minutes, &seconds); + + rc = snprintf(buf, sizeof(buf), "%c%02" PRIu32 ":%02" PRIu32, + ((offset >= 0)? '+' : '-'), hours, minutes); + pcmk__assert(rc == (sizeof(buf) - 1)); + offset_s = buf; + + } else { + // offset out of range; use NULL as offset_s + CRM_LOG_ASSERT(QB_ABS(offset) <= DAY_SECONDS); + } + + /* @FIXME @COMPAT As of glib 2.68, g_time_zone_new() is deprecated in favor + * of g_time_zone_new_identifier(). However, calling + * g_time_zone_new_identifier() results in compiler warnings, even on a + * system with glib 2.84 installed. It is unclear why. + * + * The *_new_identifier() function was added (and the *_new() function + * deprecated) in version 2.68. They have the same signature. Ideally, we + * would choose which function to call here and below based the installed + * glib version using a CPP guard. + */ + tz = g_time_zone_new(offset_s); + dt = g_date_time_new(tz, tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec); + g_time_zone_unref(tz); + + return dt; +} + +/*! + * \internal + * \brief Expand a date/time format string, with support for fractional seconds + * + * \param[in] format Date/time format string compatible with + * \c g_date_time_format(), with additional support for + * \c "%N" for fractional seconds * \param[in] hr_dt Time value to format * - * \return Newly allocated string with formatted string + * \return Newly allocated string with formatted string, or \c NULL on error + * + * \note This function falls back to trying \c strftime() with a fixed-size + * buffer if \c g_date_time_format() fails. This fallback will be removed + * in a future release. */ char * pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt) { int scanned_pos = 0; // How many characters of format have been parsed int printed_pos = 0; // How many characters of format have been processed - size_t date_len = 0; - - char nano_s[10] = { '\0', }; - char date_s[128] = { '\0', }; + GString *buf = NULL; + char *result = NULL; struct tm tm = { 0, }; - crm_time_t dt = { 0, }; + GDateTime *gdt = NULL; if (format == NULL) { return NULL; } - pcmk__time_set_hr_dt(&dt, hr_dt); - ha_get_tm_time(&tm, &dt); - sprintf(nano_s, "%06d000", hr_dt->useconds); + + buf = g_string_sized_new(128); + + ha_get_tm_time(&tm, hr_dt); + gdt = get_g_date_time(&tm, hr_dt->offset); + if (gdt == NULL) { + goto done; + } while (format[scanned_pos] != '\0') { - int fmt_pos; // Index after last character to pass as-is - int nano_digits = 0; // Length of %N field width (if any) - char *tmp_fmt_s = NULL; - size_t nbytes = 0; + int fmt_pos = 0; // Index after last character to pass as-is + int frac_digits = 0; // %N specifier's width field value (if any) + gchar *tmp_fmt_s = NULL; + gchar *date_s = NULL; // Look for next format specifier const char *mark_s = strchr(&format[scanned_pos], '%'); if (mark_s == NULL) { // No more specifiers, so pass remaining string to strftime() as-is scanned_pos = strlen(format); fmt_pos = scanned_pos; } else { fmt_pos = mark_s - format; // Index of % - // Skip % and any field width + // Skip % and any width field scanned_pos = fmt_pos + 1; while (isdigit(format[scanned_pos])) { scanned_pos++; } switch (format[scanned_pos]) { case '\0': // Literal % and possibly digits at end of string fmt_pos = scanned_pos; // Pass remaining string as-is break; case 'N': // %[width]N + /* Fractional seconds. This was supposed to represent + * nanoseconds. However, we only store times at microsecond + * resolution, and the width field support makes this a + * general fractional component specifier rather than a + * nanoseconds specifier. + * + * Further, since we cap the width at 6 digits, a user + * cannot display times at greater than microsecond + * resolution. + * + * A leading zero in the width field is ignored, not treated + * as "use zero-padding." For example, "%03N" and "%3N" + * produce the same result. + */ scanned_pos++; - // Parse field width - nano_digits = atoi(&format[fmt_pos + 1]); - nano_digits = QB_MAX(nano_digits, 0); - nano_digits = QB_MIN(nano_digits, 6); + // Parse width field + frac_digits = atoi(&format[fmt_pos + 1]); + frac_digits = QB_MAX(frac_digits, 0); + frac_digits = QB_MIN(frac_digits, 6); break; default: // Some other specifier if (format[++scanned_pos] != '\0') { // More to parse continue; } fmt_pos = scanned_pos; // Pass remaining string as-is break; } } - if (date_len >= sizeof(date_s)) { - return NULL; // No room for remaining string - } + tmp_fmt_s = g_strndup(&format[printed_pos], fmt_pos - printed_pos); + date_s = g_date_time_format(gdt, tmp_fmt_s); - tmp_fmt_s = strndup(&format[printed_pos], fmt_pos - printed_pos); - if (tmp_fmt_s == NULL) { - return NULL; - } + if (date_s == NULL) { + char compat_date_s[1024] = { '\0' }; + size_t nbytes = 0; + + // @COMPAT Drop this fallback + crm_warn("Could not format time using format string '%s' with " + "g_date_time_format(); trying strftime(). In a future " + "release, use of strftime() as a fallback will be removed", + format); #ifdef HAVE_FORMAT_NONLITERAL #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" -#endif - nbytes = strftime(&date_s[date_len], sizeof(date_s) - date_len, - tmp_fmt_s, &tm); +#endif // HAVE_FORMAT_NONLITERAL + nbytes = strftime(compat_date_s, sizeof(compat_date_s), tmp_fmt_s, + &tm); #ifdef HAVE_FORMAT_NONLITERAL #pragma GCC diagnostic pop -#endif - free(tmp_fmt_s); - if (nbytes == 0) { // Would overflow buffer - return NULL; +#endif // HAVE_FORMAT_NONLITERAL + + if (nbytes == 0) { + // Truncation, empty string, or error; impossible to discern + crm_err("Could not format time using format string '%s'", + format); + + // Ensure we return NULL + g_string_truncate(buf, 0); + g_free(tmp_fmt_s); + goto done; + } + date_s = g_strdup(compat_date_s); } - date_len += nbytes; + + g_string_append(buf, date_s); + g_free(date_s); + g_free(tmp_fmt_s); + printed_pos = scanned_pos; - if (nano_digits != 0) { - int nc = 0; - if (date_len >= sizeof(date_s)) { - return NULL; // No room to add nanoseconds - } - nc = snprintf(&date_s[date_len], sizeof(date_s) - date_len, - "%.*s", nano_digits, nano_s); + if (frac_digits != 0) { + // Descending powers of 10 (10^5 down to 10^0) + static const int powers[6] = { 1e5, 1e4, 1e3, 1e2, 1e1, 1e0 }; - if ((nc < 0) || (nc == (sizeof(date_s) - date_len))) { - return NULL; // Error or would overflow buffer - } - date_len += nc; + // Sanity check to ensure array access is in bounds + pcmk__assert((frac_digits > 0) && (frac_digits <= 6)); + + /* Append fractional seconds at the requested resolution, truncated + * toward zero. We're basically converting from microseconds to + * another unit here. For example, suppose the width field + * (frac_digits) is 3. This means "use millisecond resolution." Then + * we need to divide our microseconds value by 10^3, which is + * powers[3 - 1]. + * + * If the width field is 6 (microsecond resolution), then we divide + * our microseconds value by 10^0 == 1, which is powers[6 - 1]. + */ + g_string_append_printf(buf, "%0*d", frac_digits, + hr_dt->useconds / powers[frac_digits - 1]); } } - return (date_len == 0)? NULL : pcmk__str_copy(date_s); +done: + if (buf->len > 0) { + result = pcmk__str_copy(buf->str); + } + g_string_free(buf, TRUE); + + if (gdt != NULL) { + g_date_time_unref(gdt); + } + return result; } /*! * \internal * \brief Return a human-friendly string corresponding to an epoch time value * * \param[in] source Pointer to epoch time value (or \p NULL for current time) * \param[in] flags Group of \p crm_time_* flags controlling display format * (0 to use \p ctime() with newline removed) * * \return String representation of \p source on success (may be empty depending * on \p flags; guaranteed not to be \p NULL) * * \note The caller is responsible for freeing the return value using \p free(). */ char * pcmk__epoch2str(const time_t *source, uint32_t flags) { time_t epoch_time = (source == NULL)? time(NULL) : *source; if (flags == 0) { return pcmk__str_copy(pcmk__trim(ctime(&epoch_time))); } else { crm_time_t dt; crm_time_set_timet(&dt, &epoch_time); return crm_time_as_string(&dt, flags); } } /*! * \internal * \brief Return a human-friendly string corresponding to seconds-and- * nanoseconds value * * Time is shown with microsecond resolution if \p crm_time_usecs is in \p * flags. * * \param[in] ts Time in seconds and nanoseconds (or \p NULL for current * time) * \param[in] flags Group of \p crm_time_* flags controlling display format * * \return String representation of \p ts on success (may be empty depending on * \p flags; guaranteed not to be \p NULL) * * \note The caller is responsible for freeing the return value using \p free(). */ char * pcmk__timespec2str(const struct timespec *ts, uint32_t flags) { struct timespec tmp_ts; crm_time_t dt; - char result[DATE_MAX] = { 0 }; if (ts == NULL) { qb_util_timespec_from_epoch_get(&tmp_ts); ts = &tmp_ts; } crm_time_set_timet(&dt, &ts->tv_sec); - time_as_string_common(&dt, ts->tv_nsec / QB_TIME_NS_IN_USEC, flags, result); - return pcmk__str_copy(result); + return time_as_string_common(&dt, ts->tv_nsec / QB_TIME_NS_IN_USEC, flags); } /*! * \internal * \brief Given a millisecond interval, return a log-friendly string * * \param[in] interval_ms Interval in milliseconds * * \return Readable version of \p interval_ms * - * \note The return value is a pointer to static memory that will be - * overwritten by later calls to this function. + * \note The return value is a pointer to static memory that may be overwritten + * by later calls to this function. */ const char * pcmk__readable_interval(guint interval_ms) { #define MS_IN_S (1000) #define MS_IN_M (MS_IN_S * 60) #define MS_IN_H (MS_IN_M * 60) #define MS_IN_D (MS_IN_H * 24) #define MAXSTR sizeof("..d..h..m..s...ms") static char str[MAXSTR]; - int offset = 0; + GString *buf = NULL; + + if (interval_ms == 0) { + return "0s"; + } + + buf = g_string_sized_new(128); - str[0] = '\0'; if (interval_ms >= MS_IN_D) { - offset += snprintf(str + offset, MAXSTR - offset, "%ud", - interval_ms / MS_IN_D); + g_string_append_printf(buf, "%ud", interval_ms / MS_IN_D); interval_ms -= (interval_ms / MS_IN_D) * MS_IN_D; } if (interval_ms >= MS_IN_H) { - offset += snprintf(str + offset, MAXSTR - offset, "%uh", - interval_ms / MS_IN_H); + g_string_append_printf(buf, "%uh", interval_ms / MS_IN_H); interval_ms -= (interval_ms / MS_IN_H) * MS_IN_H; } if (interval_ms >= MS_IN_M) { - offset += snprintf(str + offset, MAXSTR - offset, "%um", - interval_ms / MS_IN_M); + g_string_append_printf(buf, "%um", interval_ms / MS_IN_M); interval_ms -= (interval_ms / MS_IN_M) * MS_IN_M; } // Ns, N.NNNs, or NNNms if (interval_ms >= MS_IN_S) { - offset += snprintf(str + offset, MAXSTR - offset, "%u", - interval_ms / MS_IN_S); + g_string_append_printf(buf, "%u", interval_ms / MS_IN_S); interval_ms -= (interval_ms / MS_IN_S) * MS_IN_S; + if (interval_ms > 0) { - offset += snprintf(str + offset, MAXSTR - offset, ".%03u", - interval_ms); + g_string_append_printf(buf, ".%03u", interval_ms); } - (void) snprintf(str + offset, MAXSTR - offset, "s"); + g_string_append_c(buf, 's'); } else if (interval_ms > 0) { - (void) snprintf(str + offset, MAXSTR - offset, "%ums", interval_ms); - - } else if (str[0] == '\0') { - strcpy(str, "0s"); + g_string_append_printf(buf, "%ums", interval_ms); } + + pcmk__assert(buf->len < sizeof(str)); + strncpy(str, buf->str, sizeof(str) - 1); + g_string_free(buf, TRUE); return str; } diff --git a/lib/common/logging.c b/lib/common/logging.c index 7ba407721e..5fa8cd20c6 100644 --- a/lib/common/logging.c +++ b/lib/common/logging.c @@ -1,1299 +1,1310 @@ /* - * Copyright 2004-2024 the Pacemaker project contributors + * Copyright 2004-2025 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Use high-resolution (millisecond) timestamps if libqb supports them #ifdef QB_FEATURE_LOG_HIRES_TIMESTAMPS #define TIMESTAMP_FORMAT_SPEC "%%T" typedef struct timespec *log_time_t; #else #define TIMESTAMP_FORMAT_SPEC "%%t" typedef time_t log_time_t; #endif unsigned int crm_log_level = LOG_INFO; unsigned int crm_trace_nonlog = 0; bool pcmk__is_daemon = false; static unsigned int crm_log_priority = LOG_NOTICE; static guint pcmk__log_id = 0; static guint pcmk__glib_log_id = 0; static guint pcmk__gio_log_id = 0; static guint pcmk__gmodule_log_id = 0; static guint pcmk__gthread_log_id = 0; static pcmk__output_t *logger_out = NULL; pcmk__config_error_func pcmk__config_error_handler = NULL; pcmk__config_warning_func pcmk__config_warning_handler = NULL; void *pcmk__config_error_context = NULL; void *pcmk__config_warning_context = NULL; static gboolean crm_tracing_enabled(void); static void crm_glib_handler(const gchar * log_domain, GLogLevelFlags flags, const gchar * message, gpointer user_data) { int log_level = LOG_WARNING; GLogLevelFlags msg_level = (flags & G_LOG_LEVEL_MASK); static struct qb_log_callsite *glib_cs = NULL; if (glib_cs == NULL) { glib_cs = qb_log_callsite_get(__func__, __FILE__, "glib-handler", LOG_DEBUG, __LINE__, crm_trace_nonlog); } switch (msg_level) { case G_LOG_LEVEL_CRITICAL: log_level = LOG_CRIT; if (!crm_is_callsite_active(glib_cs, LOG_DEBUG, crm_trace_nonlog)) { /* log and record how we got here */ crm_abort(__FILE__, __func__, __LINE__, message, TRUE, TRUE); } break; case G_LOG_LEVEL_ERROR: log_level = LOG_ERR; break; case G_LOG_LEVEL_MESSAGE: log_level = LOG_NOTICE; break; case G_LOG_LEVEL_INFO: log_level = LOG_INFO; break; case G_LOG_LEVEL_DEBUG: log_level = LOG_DEBUG; break; case G_LOG_LEVEL_WARNING: case G_LOG_FLAG_RECURSION: case G_LOG_FLAG_FATAL: case G_LOG_LEVEL_MASK: log_level = LOG_WARNING; break; } do_crm_log(log_level, "%s: %s", log_domain, message); } #ifndef NAME_MAX # define NAME_MAX 256 #endif /*! * \internal * \brief Write out a blackbox (enabling blackboxes if needed) * * \param[in] nsig Signal number that was received * * \note This is a true signal handler, and so must be async-safe. */ static void crm_trigger_blackbox(int nsig) { if(nsig == SIGTRAP) { /* Turn it on if it wasn't already */ crm_enable_blackbox(nsig); } crm_write_blackbox(nsig, NULL); } void crm_log_deinit(void) { if (pcmk__log_id == 0) { return; } g_log_remove_handler(G_LOG_DOMAIN, pcmk__log_id); pcmk__log_id = 0; g_log_remove_handler("GLib", pcmk__glib_log_id); pcmk__glib_log_id = 0; g_log_remove_handler("GLib-GIO", pcmk__gio_log_id); pcmk__gio_log_id = 0; g_log_remove_handler("GModule", pcmk__gmodule_log_id); pcmk__gmodule_log_id = 0; g_log_remove_handler("GThread", pcmk__gthread_log_id); pcmk__gthread_log_id = 0; } -#define FMT_MAX 256 - /*! * \internal * \brief Set the log format string based on the passed-in method * * \param[in] method The detail level of the log output * \param[in] daemon The daemon ID included in error messages * \param[in] use_pid Cached result of getpid() call, for efficiency * \param[in] use_nodename Cached result of uname() call, for efficiency * */ /* XXX __attribute__((nonnull)) for use_nodename parameter */ static void set_format_string(int method, const char *daemon, pid_t use_pid, const char *use_nodename) { if (method == QB_LOG_SYSLOG) { // The system log gets a simplified, user-friendly format qb_log_ctl(method, QB_LOG_CONF_EXTENDED, QB_FALSE); qb_log_format_set(method, "%g %p: %b"); } else { // Everything else gets more detail, for advanced troubleshooting - - int offset = 0; - char fmt[FMT_MAX]; + GString *fmt = g_string_sized_new(256); if (method > QB_LOG_STDERR) { // If logging to file, prefix with timestamp, node name, daemon ID - offset += snprintf(fmt + offset, FMT_MAX - offset, - TIMESTAMP_FORMAT_SPEC " %s %-20s[%lu] ", - use_nodename, daemon, (unsigned long) use_pid); + g_string_append_printf(fmt, + TIMESTAMP_FORMAT_SPEC " %s %-20s[%lld] ", + use_nodename, daemon, (long long) use_pid); } // Add function name (in parentheses) - offset += snprintf(fmt + offset, FMT_MAX - offset, "(%%n"); + g_string_append(fmt, "(%n"); if (crm_tracing_enabled()) { // When tracing, add file and line number - offset += snprintf(fmt + offset, FMT_MAX - offset, "@%%f:%%l"); + g_string_append(fmt, "@%f:%l"); } - offset += snprintf(fmt + offset, FMT_MAX - offset, ")"); + g_string_append_c(fmt, ')'); // Add tag (if any), severity, and actual message - offset += snprintf(fmt + offset, FMT_MAX - offset, " %%g\t%%p: %%b"); + g_string_append(fmt, " %g\t%p: %b"); - CRM_LOG_ASSERT(offset > 0); - qb_log_format_set(method, fmt); + CRM_LOG_ASSERT(fmt->len > 0); + qb_log_format_set(method, fmt->str); + g_string_free(fmt, TRUE); } } #define DEFAULT_LOG_FILE CRM_LOG_DIR "/pacemaker.log" static bool logfile_disabled(const char *filename) { return pcmk__str_eq(filename, PCMK_VALUE_NONE, pcmk__str_casei) || pcmk__str_eq(filename, "/dev/null", pcmk__str_none); } /*! * \internal * \brief Fix log file ownership if group is wrong or doesn't have access * * \param[in] filename Log file name (for logging only) * \param[in] logfd Log file descriptor * * \return Standard Pacemaker return code */ static int chown_logfile(const char *filename, int logfd) { uid_t pcmk_uid = 0; gid_t pcmk_gid = 0; struct stat st; int rc; // Get the log file's current ownership and permissions if (fstat(logfd, &st) < 0) { return errno; } // Any other errors don't prevent file from being used as log rc = pcmk_daemon_user(&pcmk_uid, &pcmk_gid); if (rc != pcmk_ok) { rc = pcmk_legacy2rc(rc); crm_warn("Not changing '%s' ownership because user information " "unavailable: %s", filename, pcmk_rc_str(rc)); return pcmk_rc_ok; } if ((st.st_gid == pcmk_gid) && ((st.st_mode & S_IRWXG) == (S_IRGRP|S_IWGRP))) { return pcmk_rc_ok; } if (fchown(logfd, pcmk_uid, pcmk_gid) < 0) { crm_warn("Couldn't change '%s' ownership to user %s gid %d: %s", filename, CRM_DAEMON_USER, pcmk_gid, strerror(errno)); } return pcmk_rc_ok; } // Reset log file permissions (using environment variable if set) static void chmod_logfile(const char *filename, int logfd) { const char *modestr = pcmk__env_option(PCMK__ENV_LOGFILE_MODE); mode_t filemode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP; if (modestr != NULL) { long filemode_l = strtol(modestr, NULL, 8); if ((filemode_l != LONG_MIN) && (filemode_l != LONG_MAX)) { filemode = (mode_t) filemode_l; } } if ((filemode != 0) && (fchmod(logfd, filemode) < 0)) { crm_warn("Couldn't change '%s' mode to %04o: %s", filename, filemode, strerror(errno)); } } // If we're root, correct a log file's permissions if needed static int set_logfile_permissions(const char *filename, FILE *logfile) { if (geteuid() == 0) { int logfd = fileno(logfile); int rc = chown_logfile(filename, logfd); if (rc != pcmk_rc_ok) { return rc; } chmod_logfile(filename, logfd); } return pcmk_rc_ok; } // Enable libqb logging to a new log file static void enable_logfile(int fd) { qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_TRUE); #if 0 qb_log_ctl(fd, QB_LOG_CONF_FILE_SYNC, 1); // Turn on synchronous writes #endif #ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN // Longer than default, for logging long XML lines qb_log_ctl(fd, QB_LOG_CONF_MAX_LINE_LEN, 800); #endif crm_update_callsites(); } static inline void disable_logfile(int fd) { qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_FALSE); } static void setenv_logfile(const char *filename) { // Some resource agents will log only if environment variable is set if (pcmk__env_option(PCMK__ENV_LOGFILE) == NULL) { pcmk__set_env_option(PCMK__ENV_LOGFILE, filename, true); } } /*! * \brief Add a file to be used as a Pacemaker detail log * * \param[in] filename Name of log file to use * * \return Standard Pacemaker return code */ int pcmk__add_logfile(const char *filename) { /* No log messages from this function will be logged to the new log! * If another target such as syslog has already been added, the messages * should show up there. */ int fd = 0; int rc = pcmk_rc_ok; FILE *logfile = NULL; bool is_default = false; static int default_fd = -1; static bool have_logfile = false; // Use default if caller didn't specify (and we don't already have one) if (filename == NULL) { if (have_logfile) { return pcmk_rc_ok; } filename = DEFAULT_LOG_FILE; } // If the user doesn't want logging, we're done if (logfile_disabled(filename)) { return pcmk_rc_ok; } // If the caller wants the default and we already have it, we're done is_default = pcmk__str_eq(filename, DEFAULT_LOG_FILE, pcmk__str_none); if (is_default && (default_fd >= 0)) { return pcmk_rc_ok; } // Check whether we have write access to the file logfile = fopen(filename, "a"); if (logfile == NULL) { rc = errno; crm_warn("Logging to '%s' is disabled: %s " QB_XS " uid=%u gid=%u", filename, strerror(rc), geteuid(), getegid()); return rc; } rc = set_logfile_permissions(filename, logfile); if (rc != pcmk_rc_ok) { crm_warn("Logging to '%s' is disabled: %s " QB_XS " permissions", filename, strerror(rc)); fclose(logfile); return rc; } // Close and reopen as libqb logging target fclose(logfile); fd = qb_log_file_open(filename); if (fd < 0) { crm_warn("Logging to '%s' is disabled: %s " QB_XS " qb_log_file_open", filename, strerror(-fd)); return -fd; // == +errno } if (is_default) { default_fd = fd; setenv_logfile(filename); } else if (default_fd >= 0) { crm_notice("Switching logging to %s", filename); disable_logfile(default_fd); } crm_notice("Additional logging available in %s", filename); enable_logfile(fd); have_logfile = true; return pcmk_rc_ok; } /*! * \brief Add multiple additional log files * * \param[in] log_files Array of log files to add * \param[in] out Output object to use for error reporting * * \return Standard Pacemaker return code */ void pcmk__add_logfiles(gchar **log_files, pcmk__output_t *out) { if (log_files == NULL) { return; } for (gchar **fname = log_files; *fname != NULL; fname++) { int rc = pcmk__add_logfile(*fname); if (rc != pcmk_rc_ok) { out->err(out, "Logging to %s is disabled: %s", *fname, pcmk_rc_str(rc)); } } } static int blackbox_trigger = 0; static volatile char *blackbox_file_prefix = NULL; static void blackbox_logger(int32_t t, struct qb_log_callsite *cs, log_time_t timestamp, const char *msg) { if(cs && cs->priority < LOG_ERR) { crm_write_blackbox(SIGTRAP, cs); /* Bypass the over-dumping logic */ } else { crm_write_blackbox(0, cs); } } static void crm_control_blackbox(int nsig, bool enable) { int lpc = 0; if (blackbox_file_prefix == NULL) { pid_t pid = getpid(); blackbox_file_prefix = crm_strdup_printf("%s/%s-%lu", CRM_BLACKBOX_DIR, crm_system_name, (unsigned long) pid); } if (enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_SIZE, 5 * 1024 * 1024); /* Any size change drops existing entries */ qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); /* Setting the size seems to disable it */ /* Enable synchronous logging */ for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) { qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_TRUE); } crm_notice("Initiated blackbox recorder: %s", blackbox_file_prefix); /* Save to disk on abnormal termination */ crm_signal_handler(SIGSEGV, crm_trigger_blackbox); crm_signal_handler(SIGABRT, crm_trigger_blackbox); crm_signal_handler(SIGILL, crm_trigger_blackbox); crm_signal_handler(SIGBUS, crm_trigger_blackbox); crm_signal_handler(SIGFPE, crm_trigger_blackbox); crm_update_callsites(); blackbox_trigger = qb_log_custom_open(blackbox_logger, NULL, NULL, NULL); qb_log_ctl(blackbox_trigger, QB_LOG_CONF_ENABLED, QB_TRUE); crm_trace("Trigger: %d is %d %d", blackbox_trigger, qb_log_ctl(blackbox_trigger, QB_LOG_CONF_STATE_GET, 0), QB_LOG_STATE_ENABLED); crm_update_callsites(); } else if (!enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) == QB_LOG_STATE_ENABLED) { qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); /* Disable synchronous logging again when the blackbox is disabled */ for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) { qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_FALSE); } } } void crm_enable_blackbox(int nsig) { crm_control_blackbox(nsig, TRUE); } void crm_disable_blackbox(int nsig) { crm_control_blackbox(nsig, FALSE); } /*! * \internal * \brief Write out a blackbox, if blackboxes are enabled * * \param[in] nsig Signal that was received * \param[in] cs libqb callsite * * \note This may be called via a true signal handler and so must be async-safe. * @TODO actually make this async-safe */ void crm_write_blackbox(int nsig, const struct qb_log_callsite *cs) { static volatile int counter = 1; static volatile time_t last = 0; - char buffer[NAME_MAX]; + char *buffer = NULL; + int rc = 0; time_t now = time(NULL); if (blackbox_file_prefix == NULL) { return; } switch (nsig) { case 0: case SIGTRAP: /* The graceful case - such as assertion failure or user request */ if (nsig == 0 && now == last) { /* Prevent over-dumping */ return; } - snprintf(buffer, NAME_MAX, "%s.%d", blackbox_file_prefix, counter++); + buffer = crm_strdup_printf("%s.%d", blackbox_file_prefix, + counter++); if (nsig == SIGTRAP) { crm_notice("Blackbox dump requested, please see %s for contents", buffer); } else if (cs) { syslog(LOG_NOTICE, "Problem detected at %s:%d (%s), please see %s for additional details", cs->function, cs->lineno, cs->filename, buffer); } else { crm_notice("Problem detected, please see %s for additional details", buffer); } last = now; - qb_log_blackbox_write_to_file(buffer); + + rc = qb_log_blackbox_write_to_file(buffer); + if (rc < 0) { + // System errno + crm_err("Failed to write blackbox file %s: %s", buffer, + strerror(-rc)); + } /* Flush the existing contents * A size change would also work */ qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); break; default: /* Do as little as possible, just try to get what we have out * We logged the filename when the blackbox was enabled */ crm_signal_handler(nsig, SIG_DFL); qb_log_blackbox_write_to_file((const char *)blackbox_file_prefix); qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); raise(nsig); break; } + + free(buffer); } static const char * crm_quark_to_string(uint32_t tag) { const char *text = g_quark_to_string(tag); if (text) { return text; } return ""; } static void crm_log_filter_source(int source, const char *trace_files, const char *trace_fns, const char *trace_fmts, const char *trace_tags, const char *trace_blackbox, struct qb_log_callsite *cs) { if (qb_log_ctl(source, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { return; } else if (cs->tags != crm_trace_nonlog && source == QB_LOG_BLACKBOX) { /* Blackbox gets everything if enabled */ qb_bit_set(cs->targets, source); } else if (source == blackbox_trigger && blackbox_trigger > 0) { /* Should this log message result in the blackbox being dumped */ if (cs->priority <= LOG_ERR) { qb_bit_set(cs->targets, source); } else if (trace_blackbox) { char *key = crm_strdup_printf("%s:%d", cs->function, cs->lineno); if (strstr(trace_blackbox, key) != NULL) { qb_bit_set(cs->targets, source); } free(key); } } else if (source == QB_LOG_SYSLOG) { /* No tracing to syslog */ if (cs->priority <= crm_log_priority && cs->priority <= crm_log_level) { qb_bit_set(cs->targets, source); } /* Log file tracing options... */ } else if (cs->priority <= crm_log_level) { qb_bit_set(cs->targets, source); } else if (trace_files && strstr(trace_files, cs->filename) != NULL) { qb_bit_set(cs->targets, source); } else if (trace_fns && strstr(trace_fns, cs->function) != NULL) { qb_bit_set(cs->targets, source); } else if (trace_fmts && strstr(trace_fmts, cs->format) != NULL) { qb_bit_set(cs->targets, source); } else if (trace_tags && cs->tags != 0 && cs->tags != crm_trace_nonlog && g_quark_to_string(cs->tags) != NULL) { qb_bit_set(cs->targets, source); } } #ifndef HAVE_STRCHRNUL /* strchrnul() is a GNU extension. If not present, use our own definition. * The GNU version returns char*, but we only need it to be const char*. */ static const char * strchrnul(const char *s, int c) { while ((*s != c) && (*s != '\0')) { ++s; } return s; } #endif static void crm_log_filter(struct qb_log_callsite *cs) { int lpc = 0; static int need_init = 1; static const char *trace_fns = NULL; static const char *trace_tags = NULL; static const char *trace_fmts = NULL; static const char *trace_files = NULL; static const char *trace_blackbox = NULL; if (need_init) { need_init = 0; trace_fns = pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS); trace_fmts = pcmk__env_option(PCMK__ENV_TRACE_FORMATS); trace_tags = pcmk__env_option(PCMK__ENV_TRACE_TAGS); trace_files = pcmk__env_option(PCMK__ENV_TRACE_FILES); trace_blackbox = pcmk__env_option(PCMK__ENV_TRACE_BLACKBOX); if (trace_tags != NULL) { uint32_t tag; - char token[500]; const char *offset = NULL; const char *next = trace_tags; + // @TODO Use g_strsplit() to simplify do { + char *token = NULL; + offset = next; next = strchrnul(offset, ','); - snprintf(token, sizeof(token), "%.*s", (int)(next - offset), offset); + token = crm_strdup_printf("%.*s", (int) (next - offset), offset); tag = g_quark_from_string(token); crm_info("Created GQuark %u from token '%s' in '%s'", tag, token, trace_tags); + free(token); + if (next[0] != 0) { next++; } } while (next != NULL && next[0] != 0); } } cs->targets = 0; /* Reset then find targets to enable */ for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) { crm_log_filter_source(lpc, trace_files, trace_fns, trace_fmts, trace_tags, trace_blackbox, cs); } } gboolean crm_is_callsite_active(struct qb_log_callsite *cs, uint8_t level, uint32_t tags) { gboolean refilter = FALSE; if (cs == NULL) { return FALSE; } if (cs->priority != level) { cs->priority = level; refilter = TRUE; } if (cs->tags != tags) { cs->tags = tags; refilter = TRUE; } if (refilter) { crm_log_filter(cs); } if (cs->targets == 0) { return FALSE; } return TRUE; } void crm_update_callsites(void) { static gboolean log = TRUE; if (log) { log = FALSE; crm_debug ("Enabling callsites based on priority=%d, files=%s, functions=%s, formats=%s, tags=%s", crm_log_level, pcmk__env_option(PCMK__ENV_TRACE_FILES), pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS), pcmk__env_option(PCMK__ENV_TRACE_FORMATS), pcmk__env_option(PCMK__ENV_TRACE_TAGS)); } qb_log_filter_fn_set(crm_log_filter); } static gboolean crm_tracing_enabled(void) { return (crm_log_level == LOG_TRACE) || (pcmk__env_option(PCMK__ENV_TRACE_FILES) != NULL) || (pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS) != NULL) || (pcmk__env_option(PCMK__ENV_TRACE_FORMATS) != NULL) || (pcmk__env_option(PCMK__ENV_TRACE_TAGS) != NULL); } static int crm_priority2int(const char *name) { struct syslog_names { const char *name; int priority; }; static struct syslog_names p_names[] = { {"emerg", LOG_EMERG}, {"alert", LOG_ALERT}, {"crit", LOG_CRIT}, {"error", LOG_ERR}, {"warning", LOG_WARNING}, {"notice", LOG_NOTICE}, {"info", LOG_INFO}, {"debug", LOG_DEBUG}, {NULL, -1} }; int lpc; for (lpc = 0; name != NULL && p_names[lpc].name != NULL; lpc++) { if (pcmk__str_eq(p_names[lpc].name, name, pcmk__str_none)) { return p_names[lpc].priority; } } return crm_log_priority; } /*! * \internal * \brief Set the identifier for the current process * * If the identifier crm_system_name is not already set, then it is set as follows: * - it is passed to the function via the "entity" parameter, or * - it is derived from the executable name * * The identifier can be used in logs, IPC, and more. * * This method also sets the PCMK_service environment variable. * * \param[in] entity If not NULL, will be assigned to the identifier * \param[in] argc The number of command line parameters * \param[in] argv The command line parameter values */ static void set_identity(const char *entity, int argc, char *const *argv) { if (crm_system_name != NULL) { return; // Already set, don't overwrite } if (entity != NULL) { crm_system_name = pcmk__str_copy(entity); } else if ((argc > 0) && (argv != NULL)) { char *mutable = strdup(argv[0]); char *modified = basename(mutable); if (strstr(modified, "lt-") == modified) { modified += 3; } crm_system_name = pcmk__str_copy(modified); free(mutable); } else { crm_system_name = pcmk__str_copy("Unknown"); } // Used by fencing.py.py (in fence-agents) pcmk__set_env_option(PCMK__ENV_SERVICE, crm_system_name, false); } void crm_log_preinit(const char *entity, int argc, char *const *argv) { /* Configure libqb logging with nothing turned on */ struct utsname res; int lpc = 0; int32_t qb_facility = 0; pid_t pid = getpid(); const char *nodename = "localhost"; static bool have_logging = false; GLogLevelFlags log_levels; if (have_logging) { return; } have_logging = true; /* @TODO Try to create a more obvious "global Pacemaker initializer" * function than crm_log_preinit(), and call pcmk__schema_init() there. * See also https://projects.clusterlabs.org/T840. */ pcmk__schema_init(); if (crm_trace_nonlog == 0) { crm_trace_nonlog = g_quark_from_static_string("Pacemaker non-logging tracepoint"); } umask(S_IWGRP | S_IWOTH | S_IROTH); /* Add a log handler for messages from our log domain at any log level. */ log_levels = G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION; pcmk__log_id = g_log_set_handler(G_LOG_DOMAIN, log_levels, crm_glib_handler, NULL); /* Add a log handler for messages from the GLib domains at any log level. */ pcmk__glib_log_id = g_log_set_handler("GLib", log_levels, crm_glib_handler, NULL); pcmk__gio_log_id = g_log_set_handler("GLib-GIO", log_levels, crm_glib_handler, NULL); pcmk__gmodule_log_id = g_log_set_handler("GModule", log_levels, crm_glib_handler, NULL); pcmk__gthread_log_id = g_log_set_handler("GThread", log_levels, crm_glib_handler, NULL); /* glib should not abort for any messages from the Pacemaker domain, but * other domains are still free to specify their own behavior. However, * note that G_LOG_LEVEL_ERROR is always fatal regardless of what we do * here. */ g_log_set_fatal_mask(G_LOG_DOMAIN, 0); /* Set crm_system_name, which is used as the logging name. It may also * be used for other purposes such as an IPC client name. */ set_identity(entity, argc, argv); qb_facility = qb_log_facility2int("local0"); qb_log_init(crm_system_name, qb_facility, LOG_ERR); crm_log_level = LOG_CRIT; /* Nuke any syslog activity until it's asked for */ qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_FALSE); #ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN // Shorter than default, generous for what we *should* send to syslog qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_MAX_LINE_LEN, 256); #endif if (uname(memset(&res, 0, sizeof(res))) == 0 && *res.nodename != '\0') { nodename = res.nodename; } /* Set format strings and disable threading * Pacemaker and threads do not mix well (due to the amount of forking) */ qb_log_tags_stringify_fn_set(crm_quark_to_string); for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) { qb_log_ctl(lpc, QB_LOG_CONF_THREADED, QB_FALSE); #ifdef HAVE_qb_log_conf_QB_LOG_CONF_ELLIPSIS // End truncated lines with '...' qb_log_ctl(lpc, QB_LOG_CONF_ELLIPSIS, QB_TRUE); #endif set_format_string(lpc, crm_system_name, pid, nodename); } #ifdef ENABLE_NLS /* Enable translations (experimental). Currently we only have a few * proof-of-concept translations for some option help. The goal would be to * offer translations for option help and man pages rather than logs or * documentation, to reduce the burden of maintaining them. */ // Load locale information for the local host from the environment setlocale(LC_ALL, ""); // Tell gettext where to find Pacemaker message catalogs pcmk__assert(bindtextdomain(PACKAGE, PCMK__LOCALE_DIR) != NULL); // Tell gettext to use the Pacemaker message catalogs pcmk__assert(textdomain(PACKAGE) != NULL); // Tell gettext that the translated strings are stored in UTF-8 bind_textdomain_codeset(PACKAGE, "UTF-8"); #endif } gboolean crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_stderr, int argc, char **argv, gboolean quiet) { const char *syslog_priority = NULL; const char *facility = pcmk__env_option(PCMK__ENV_LOGFACILITY); const char *f_copy = facility; pcmk__is_daemon = daemon; crm_log_preinit(entity, argc, argv); if (level > LOG_TRACE) { level = LOG_TRACE; } if(level > crm_log_level) { crm_log_level = level; } /* Should we log to syslog */ if (facility == NULL) { if (pcmk__is_daemon) { facility = "daemon"; } else { facility = PCMK_VALUE_NONE; } pcmk__set_env_option(PCMK__ENV_LOGFACILITY, facility, true); } if (pcmk__str_eq(facility, PCMK_VALUE_NONE, pcmk__str_casei)) { quiet = TRUE; } else { qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_FACILITY, qb_log_facility2int(facility)); } if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_DEBUG)) { /* Override the default setting */ crm_log_level = LOG_DEBUG; } /* What lower threshold do we have for sending to syslog */ syslog_priority = pcmk__env_option(PCMK__ENV_LOGPRIORITY); if (syslog_priority) { crm_log_priority = crm_priority2int(syslog_priority); } qb_log_filter_ctl(QB_LOG_SYSLOG, QB_LOG_FILTER_ADD, QB_LOG_FILTER_FILE, "*", crm_log_priority); // Log to syslog unless requested to be quiet if (!quiet) { qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_TRUE); } /* Should we log to stderr */ if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_STDERR)) { /* Override the default setting */ to_stderr = TRUE; } crm_enable_stderr(to_stderr); // Log to a file if we're a daemon or user asked for one { const char *logfile = pcmk__env_option(PCMK__ENV_LOGFILE); if (!pcmk__str_eq(PCMK_VALUE_NONE, logfile, pcmk__str_casei) && (pcmk__is_daemon || (logfile != NULL))) { // Daemons always get a log file, unless explicitly set to "none" pcmk__add_logfile(logfile); } } if (pcmk__is_daemon && pcmk__env_option_enabled(crm_system_name, PCMK__ENV_BLACKBOX)) { crm_enable_blackbox(0); } /* Summary */ crm_trace("Quiet: %d, facility %s", quiet, f_copy); pcmk__env_option(PCMK__ENV_LOGFILE); pcmk__env_option(PCMK__ENV_LOGFACILITY); crm_update_callsites(); /* Ok, now we can start logging... */ // Disable daemon request if user isn't root or Pacemaker daemon user if (pcmk__is_daemon) { const char *user = getenv("USER"); if (user != NULL && !pcmk__strcase_any_of(user, "root", CRM_DAEMON_USER, NULL)) { crm_trace("Not switching to corefile directory for %s", user); pcmk__is_daemon = false; } } if (pcmk__is_daemon) { int user = getuid(); struct passwd *pwent = getpwuid(user); if (pwent == NULL) { crm_perror(LOG_ERR, "Cannot get name for uid: %d", user); } else if (!pcmk__strcase_any_of(pwent->pw_name, "root", CRM_DAEMON_USER, NULL)) { crm_trace("Don't change active directory for regular user: %s", pwent->pw_name); } else if (chdir(CRM_CORE_DIR) < 0) { crm_perror(LOG_INFO, "Cannot change active directory to " CRM_CORE_DIR); } else { crm_info("Changed active directory to " CRM_CORE_DIR); } /* Original meanings from signal(7) * * Signal Value Action Comment * SIGTRAP 5 Core Trace/breakpoint trap * SIGUSR1 30,10,16 Term User-defined signal 1 * SIGUSR2 31,12,17 Term User-defined signal 2 * * Our usage is as similar as possible */ mainloop_add_signal(SIGUSR1, crm_enable_blackbox); mainloop_add_signal(SIGUSR2, crm_disable_blackbox); mainloop_add_signal(SIGTRAP, crm_trigger_blackbox); } else if (!quiet) { crm_log_args(argc, argv); } return TRUE; } /* returns the old value */ unsigned int set_crm_log_level(unsigned int level) { unsigned int old = crm_log_level; if (level > LOG_TRACE) { level = LOG_TRACE; } crm_log_level = level; crm_update_callsites(); crm_trace("New log level: %d", level); return old; } void crm_enable_stderr(int enable) { if (enable && qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_TRUE); crm_update_callsites(); } else if (enable == FALSE) { qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_FALSE); } } /*! * \brief Make logging more verbose * * If logging to stderr is not already enabled when this function is called, * enable it. Otherwise, increase the log level by 1. * * \param[in] argc Ignored * \param[in] argv Ignored */ void crm_bump_log_level(int argc, char **argv) { if (qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { crm_enable_stderr(TRUE); } else { set_crm_log_level(crm_log_level + 1); } } unsigned int get_crm_log_level(void) { return crm_log_level; } /*! * \brief Log the command line (once) * * \param[in] Number of values in \p argv * \param[in] Command-line arguments (including command name) * * \note This function will only log once, even if called with different * arguments. */ void crm_log_args(int argc, char **argv) { static bool logged = false; gchar *arg_string = NULL; if ((argc == 0) || (argv == NULL) || logged) { return; } logged = true; arg_string = g_strjoinv(" ", argv); crm_notice("Invoked: %s", arg_string); g_free(arg_string); } void crm_log_output_fn(const char *file, const char *function, int line, int level, const char *prefix, const char *output) { const char *next = NULL; const char *offset = NULL; if (level == LOG_NEVER) { return; } if (output == NULL) { if (level != LOG_STDOUT) { level = LOG_TRACE; } output = "-- empty --"; } next = output; do { offset = next; next = strchrnul(offset, '\n'); do_crm_log_alias(level, file, function, line, "%s [ %.*s ]", prefix, (int)(next - offset), offset); if (next[0] != 0) { next++; } } while (next != NULL && next[0] != 0); } void pcmk__cli_init_logging(const char *name, unsigned int verbosity) { crm_log_init(name, LOG_ERR, FALSE, FALSE, 0, NULL, TRUE); for (int i = 0; i < verbosity; i++) { /* These arguments are ignored, so pass placeholders. */ crm_bump_log_level(0, NULL); } } /*! * \brief Log XML line-by-line in a formatted fashion * * \param[in] file File name to use for log filtering * \param[in] function Function name to use for log filtering * \param[in] line Line number to use for log filtering * \param[in] tags Logging tags to use for log filtering * \param[in] level Priority at which to log the messages * \param[in] text Prefix for each line * \param[in] xml XML to log * * \note This does nothing when \p level is \p LOG_STDOUT. * \note Do not call this function directly. It should be called only from the * \p do_crm_log_xml() macro. */ void pcmk_log_xml_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const char *text, const xmlNode *xml) { if (xml == NULL) { do_crm_log(level, "%s%sNo data to dump as XML", pcmk__s(text, ""), pcmk__str_empty(text)? "" : " "); } else { if (logger_out == NULL) { CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); } pcmk__output_set_log_level(logger_out, level); pcmk__output_set_log_filter(logger_out, file, function, line, tags); pcmk__xml_show(logger_out, text, xml, 1, pcmk__xml_fmt_pretty |pcmk__xml_fmt_open |pcmk__xml_fmt_children |pcmk__xml_fmt_close); pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U); } } /*! * \internal * \brief Log XML changes line-by-line in a formatted fashion * * \param[in] file File name to use for log filtering * \param[in] function Function name to use for log filtering * \param[in] line Line number to use for log filtering * \param[in] tags Logging tags to use for log filtering * \param[in] level Priority at which to log the messages * \param[in] xml XML whose changes to log * * \note This does nothing when \p level is \c LOG_STDOUT. */ void pcmk__log_xml_changes_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const xmlNode *xml) { if (xml == NULL) { do_crm_log(level, "No XML to dump"); return; } if (logger_out == NULL) { CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); } pcmk__output_set_log_level(logger_out, level); pcmk__output_set_log_filter(logger_out, file, function, line, tags); pcmk__xml_show_changes(logger_out, xml); pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U); } /*! * \internal * \brief Log an XML patchset line-by-line in a formatted fashion * * \param[in] file File name to use for log filtering * \param[in] function Function name to use for log filtering * \param[in] line Line number to use for log filtering * \param[in] tags Logging tags to use for log filtering * \param[in] level Priority at which to log the messages * \param[in] patchset XML patchset to log * * \note This does nothing when \p level is \c LOG_STDOUT. */ void pcmk__log_xml_patchset_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const xmlNode *patchset) { if (patchset == NULL) { do_crm_log(level, "No patchset to dump"); return; } if (logger_out == NULL) { CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); } pcmk__output_set_log_level(logger_out, level); pcmk__output_set_log_filter(logger_out, file, function, line, tags); logger_out->message(logger_out, "xml-patchset", patchset); pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U); } /*! * \internal * \brief Free the logging library's internal log output object */ void pcmk__free_common_logger(void) { if (logger_out != NULL) { logger_out->finish(logger_out, CRM_EX_OK, true, NULL); pcmk__output_free(logger_out); logger_out = NULL; } } void pcmk__set_config_error_handler(pcmk__config_error_func error_handler, void *error_context) { pcmk__config_error_handler = error_handler; pcmk__config_error_context = error_context; } void pcmk__set_config_warning_handler(pcmk__config_warning_func warning_handler, void *warning_context) { pcmk__config_warning_handler = warning_handler; pcmk__config_warning_context = warning_context; } diff --git a/lib/common/mock.c b/lib/common/mock.c index bc82d7836c..2fb231ff3e 100644 --- a/lib/common/mock.c +++ b/lib/common/mock.c @@ -1,461 +1,459 @@ /* * Copyright 2021-2025 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 #include #include #include #include #include #include #include #include #include #include // pid_t, size_t #include #include #include #include #include #include "mock_private.h" /* This file is only used when running "make check". It is built into * libcrmcommon_test.a, not into libcrmcommon.so. It is used to support * constructing mock versions of library functions for unit testing. * * HOW TO ADD A MOCKED FUNCTION: * * - In this file, declare a bool pcmk__mock_X variable, and define a __wrap_X * function with the same prototype as the actual function that performs the * desired behavior if pcmk__mock_X is true and calls __real_X otherwise. * You can use cmocka's mock_type() and mock_ptr_type() to pass extra * information to the mocked function (see existing examples for details). * * - In mock_private.h, add declarations for extern bool pcmk__mock_X and the * __real_X and __wrap_X function prototypes. * * - In mk/tap.mk, add the function name to the WRAPPED variable. * * HOW TO USE A MOCKED FUNCTION: * * - #include "mock_private.h" in your test file. * * - Write your test cases using pcmk__mock_X and cmocka's will_return() as * needed per the comments for the mocked function below. See existing test * cases for examples. */ // LCOV_EXCL_START /* abort() * * Always mock abort - there's no pcmk__mock_abort tuneable to control this. * Because abort calls _exit(), which doesn't run any of the things registered * with atexit(), coverage numbers do not get written out. This most noticably * affects places where we are testing that things abort when they should. * * The solution is this wrapper that is always enabled when we are running * unit tests (mock.c does not get included for the regular libcrmcommon.so). * All it does is dump coverage data and call the real abort(). */ _Noreturn void __wrap_abort(void) { #if (PCMK__WITH_COVERAGE == 1) __gcov_dump(); #endif __real_abort(); } /* calloc() * * If pcmk__mock_calloc is set to true, later calls to calloc() will return * NULL and must be preceded by: * * expect_*(__wrap_calloc, nmemb[, ...]); * expect_*(__wrap_calloc, size[, ...]); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_calloc = false; void * __wrap_calloc(size_t nmemb, size_t size) { if (!pcmk__mock_calloc) { return __real_calloc(nmemb, size); } check_expected(nmemb); check_expected(size); return NULL; } /* getenv() * * If pcmk__mock_getenv is set to true, later calls to getenv() must be preceded * by: * * expect_*(__wrap_getenv, name[, ...]); * will_return(__wrap_getenv, return_value); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_getenv = false; char * __wrap_getenv(const char *name) { if (!pcmk__mock_getenv) { return __real_getenv(name); } check_expected_ptr(name); return mock_ptr_type(char *); } /* realloc() * * If pcmk__mock_realloc is set to true, later calls to realloc() will return * NULL and must be preceded by: * * expect_*(__wrap_realloc, ptr[, ...]); * expect_*(__wrap_realloc, size[, ...]); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_realloc = false; void * __wrap_realloc(void *ptr, size_t size) { if (!pcmk__mock_realloc) { return __real_realloc(ptr, size); } check_expected_ptr(ptr); check_expected(size); return NULL; } /* setenv() * * If pcmk__mock_setenv is set to true, later calls to setenv() must be preceded * by: * * expect_*(__wrap_setenv, name[, ...]); * expect_*(__wrap_setenv, value[, ...]); * expect_*(__wrap_setenv, overwrite[, ...]); * will_return(__wrap_setenv, errno_to_set); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html * * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. */ bool pcmk__mock_setenv = false; int __wrap_setenv(const char *name, const char *value, int overwrite) { if (!pcmk__mock_setenv) { return __real_setenv(name, value, overwrite); } check_expected_ptr(name); check_expected_ptr(value); check_expected(overwrite); errno = mock_type(int); return (errno == 0)? 0 : -1; } /* unsetenv() * * If pcmk__mock_unsetenv is set to true, later calls to unsetenv() must be * preceded by: * * expect_*(__wrap_unsetenv, name[, ...]); * will_return(__wrap_setenv, errno_to_set); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html * * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. */ bool pcmk__mock_unsetenv = false; int __wrap_unsetenv(const char *name) { if (!pcmk__mock_unsetenv) { return __real_unsetenv(name); } check_expected_ptr(name); errno = mock_type(int); return (errno == 0)? 0 : -1; } /* getpid() * * If pcmk__mock_getpid is set to true, later calls to getpid() must be preceded * by: * * will_return(__wrap_getpid, return_value); */ bool pcmk__mock_getpid = false; pid_t __wrap_getpid(void) { return pcmk__mock_getpid? mock_type(pid_t) : __real_getpid(); } /* setgrent(), getgrent() and endgrent() * * If pcmk__mock_grent is set to true, getgrent() will behave as if the only * groups on the system are: * * - grp0 (user0, user1) * - grp1 (user1) * - grp2 (user2, user1) */ bool pcmk__mock_grent = false; // Index of group that will be returned next from getgrent() static int group_idx = 0; // Data used for testing static const char* grp0_members[] = { "user0", "user1", NULL }; static const char* grp1_members[] = { "user1", NULL }; static const char* grp2_members[] = { "user2", "user1", NULL }; /* An array of "groups" (a struct from grp.h) * * The members of the groups are initalized here to some testing data, casting * away the consts to make the compiler happy and simplify initialization. We * never actually change these variables during the test! * * string literal = const char* (cannot be changed b/c ? ) * vs. char* (it's getting casted to this) */ static const int NUM_GROUPS = 3; static struct group groups[] = { {(char*)"grp0", (char*)"", 0, (char**)grp0_members}, {(char*)"grp1", (char*)"", 1, (char**)grp1_members}, {(char*)"grp2", (char*)"", 2, (char**)grp2_members}, }; // This function resets the group_idx to 0. void __wrap_setgrent(void) { if (pcmk__mock_grent) { group_idx = 0; } else { __real_setgrent(); } } /* This function returns the next group entry in the list of groups, or * NULL if there aren't any left. * group_idx is a global variable which keeps track of where you are in the list */ struct group * __wrap_getgrent(void) { if (pcmk__mock_grent) { if (group_idx >= NUM_GROUPS) { return NULL; } return &groups[group_idx++]; } else { return __real_getgrent(); } } void __wrap_endgrent(void) { if (!pcmk__mock_grent) { __real_endgrent(); } } /* fopen() * * If pcmk__mock_fopen is set to true, later calls to fopen() must be * preceded by: * * expect_*(__wrap_fopen, pathname[, ...]); * expect_*(__wrap_fopen, mode[, ...]); * will_return(__wrap_fopen, errno_to_set); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html * * This has two mocked functions, since fopen() is sometimes actually fopen64(). */ bool pcmk__mock_fopen = false; FILE * __wrap_fopen(const char *pathname, const char *mode) { if (pcmk__mock_fopen) { check_expected_ptr(pathname); check_expected_ptr(mode); errno = mock_type(int); if (errno != 0) { return NULL; } else { return __real_fopen(pathname, mode); } } else { return __real_fopen(pathname, mode); } } #ifdef HAVE_FOPEN64 FILE * __wrap_fopen64(const char *pathname, const char *mode) { if (pcmk__mock_fopen) { check_expected_ptr(pathname); check_expected_ptr(mode); errno = mock_type(int); if (errno != 0) { return NULL; } else { return __real_fopen64(pathname, mode); } } else { return __real_fopen64(pathname, mode); } } #endif /* getpwnam_r() * * If pcmk__mock_getpwnam_r is set to true, later calls to getpwnam_r() must be * preceded by: * * expect_*(__wrap_getpwnam_r, name[, ...]); * expect_*(__wrap_getpwnam_r, pwd[, ...]); * expect_*(__wrap_getpwnam_r, buf[, ...]); * expect_*(__wrap_getpwnam_r, buflen[, ...]); * expect_*(__wrap_getpwnam_r, result[, ...]); * will_return(__wrap_getpwnam_r, return_value); * will_return(__wrap_getpwnam_r, ptr_to_result_struct); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_getpwnam_r = false; int __wrap_getpwnam_r(const char *name, struct passwd *pwd, char *buf, size_t buflen, struct passwd **result) { if (pcmk__mock_getpwnam_r) { int retval = mock_type(int); check_expected_ptr(name); check_expected_ptr(pwd); check_expected_ptr(buf); check_expected(buflen); check_expected_ptr(result); *result = mock_ptr_type(struct passwd *); return retval; } else { return __real_getpwnam_r(name, pwd, buf, buflen, result); } } /* * If pcmk__mock_readlink is set to true, later calls to readlink() must be * preceded by: * * expect_*(__wrap_readlink, path[, ...]); - * expect_*(__wrap_readlink, buf[, ...]); * expect_*(__wrap_readlink, bufsize[, ...]); * will_return(__wrap_readlink, errno_to_set); * will_return(__wrap_readlink, link_contents); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html * * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. */ bool pcmk__mock_readlink = false; ssize_t __wrap_readlink(const char *restrict path, char *restrict buf, size_t bufsize) { if (pcmk__mock_readlink) { const char *contents = NULL; check_expected_ptr(path); - check_expected(buf); check_expected(bufsize); errno = mock_type(int); contents = mock_ptr_type(const char *); if (errno == 0) { strncpy(buf, contents, bufsize - 1); return strlen(contents); } return -1; } else { return __real_readlink(path, buf, bufsize); } } /* strdup() * * If pcmk__mock_strdup is set to true, later calls to strdup() will return * NULL and must be preceded by: * * expect_*(__wrap_strdup, s[, ...]); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_strdup = false; char * __wrap_strdup(const char *s) { if (!pcmk__mock_strdup) { return __real_strdup(s); } check_expected_ptr(s); return NULL; } // LCOV_EXCL_STOP diff --git a/lib/common/options.c b/lib/common/options.c index b8f4943fda..cbcb0204eb 100644 --- a/lib/common/options.c +++ b/lib/common/options.c @@ -1,1563 +1,1544 @@ /* * Copyright 2004-2025 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 #include #include #include #include #include #include #include void pcmk__cli_help(char cmd) { if (cmd == 'v' || cmd == '$') { printf("Pacemaker %s\n", PACEMAKER_VERSION); printf("Written by Andrew Beekhof and " "the Pacemaker project contributors\n"); } else if (cmd == '!') { printf("Pacemaker %s (Build: %s): %s\n", PACEMAKER_VERSION, BUILD_VERSION, CRM_FEATURES); } crm_exit(CRM_EX_OK); while(1); // above does not return } /* * Option metadata */ static const pcmk__cluster_option_t cluster_options[] = { /* name, old name, type, allowed values, * default value, validator, * flags, * short description, * long description */ { PCMK_OPT_DC_VERSION, NULL, PCMK_VALUE_VERSION, NULL, NULL, NULL, pcmk__opt_controld|pcmk__opt_generated, N_("Pacemaker version on cluster node elected Designated Controller " "(DC)"), N_("Includes a hash which identifies the exact revision the code was " "built from. Used for diagnostic purposes."), }, { PCMK_OPT_CLUSTER_INFRASTRUCTURE, NULL, PCMK_VALUE_STRING, NULL, NULL, NULL, pcmk__opt_controld|pcmk__opt_generated, N_("The messaging layer on which Pacemaker is currently running"), N_("Used for informational and diagnostic purposes."), }, { PCMK_OPT_CLUSTER_NAME, NULL, PCMK_VALUE_STRING, NULL, NULL, NULL, pcmk__opt_controld, N_("An arbitrary name for the cluster"), N_("This optional value is mostly for users' convenience as desired " "in administration, but may also be used in Pacemaker " "configuration rules via the #cluster-name node attribute, and " "by higher-level tools and resource agents."), }, { PCMK_OPT_DC_DEADTIME, NULL, PCMK_VALUE_DURATION, NULL, "20s", pcmk__valid_interval_spec, pcmk__opt_controld, N_("How long to wait for a response from other nodes during start-up"), N_("The optimal value will depend on the speed and load of your " "network and the type of switches used."), }, { PCMK_OPT_CLUSTER_RECHECK_INTERVAL, NULL, PCMK_VALUE_DURATION, NULL, "15min", pcmk__valid_interval_spec, pcmk__opt_controld, N_("Polling interval to recheck cluster state and evaluate rules " "with date specifications"), N_("Pacemaker is primarily event-driven, and looks ahead to know when " "to recheck cluster state for failure-timeout settings and most " "time-based rules. However, it will also recheck the cluster after " "this amount of inactivity, to evaluate rules with date " "specifications and serve as a fail-safe for certain types of " "scheduler bugs. A value of 0 disables polling. A positive value " "sets an interval in seconds, unless other units are specified " "(for example, \"5min\")."), }, { PCMK_OPT_FENCE_REACTION, NULL, PCMK_VALUE_SELECT, PCMK_VALUE_STOP ", " PCMK_VALUE_PANIC, PCMK_VALUE_STOP, NULL, pcmk__opt_controld, N_("How a cluster node should react if notified of its own fencing"), N_("A cluster node may receive notification of a \"succeeded\" " "fencing that targeted it if fencing is misconfigured, or if " "fabric fencing is in use that doesn't cut cluster communication. " "Use \"stop\" to attempt to immediately stop Pacemaker and stay " "stopped, or \"panic\" to attempt to immediately reboot the local " "node, falling back to stop on failure."), }, { PCMK_OPT_ELECTION_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL, "2min", pcmk__valid_interval_spec, pcmk__opt_controld|pcmk__opt_advanced, N_("Declare an election failed if it is not decided within this much " "time. If you need to adjust this value, it probably indicates " "the presence of a bug."), NULL, }, { PCMK_OPT_SHUTDOWN_ESCALATION, NULL, PCMK_VALUE_DURATION, NULL, "20min", pcmk__valid_interval_spec, pcmk__opt_controld|pcmk__opt_advanced, N_("Exit immediately if shutdown does not complete within this much " "time. If you need to adjust this value, it probably indicates " "the presence of a bug."), NULL, }, { PCMK_OPT_JOIN_INTEGRATION_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL, "3min", pcmk__valid_interval_spec, pcmk__opt_controld|pcmk__opt_advanced, N_("If you need to adjust this value, it probably indicates " "the presence of a bug."), NULL, }, { PCMK_OPT_JOIN_FINALIZATION_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL, "30min", pcmk__valid_interval_spec, pcmk__opt_controld|pcmk__opt_advanced, N_("If you need to adjust this value, it probably indicates " "the presence of a bug."), NULL, }, { PCMK_OPT_TRANSITION_DELAY, NULL, PCMK_VALUE_DURATION, NULL, "0s", pcmk__valid_interval_spec, pcmk__opt_controld|pcmk__opt_advanced, N_("Enabling this option will slow down cluster recovery under all " "conditions"), N_("Delay cluster recovery for this much time to allow for additional " "events to occur. Useful if your configuration is sensitive to " "the order in which ping updates arrive."), }, { PCMK_OPT_NO_QUORUM_POLICY, NULL, PCMK_VALUE_SELECT, PCMK_VALUE_STOP ", " PCMK_VALUE_FREEZE ", " PCMK_VALUE_IGNORE ", " PCMK_VALUE_DEMOTE ", " PCMK_VALUE_FENCE ", " PCMK_VALUE_FENCE_LEGACY, PCMK_VALUE_STOP, pcmk__valid_no_quorum_policy, pcmk__opt_schedulerd, N_("What to do when the cluster does not have quorum"), NULL, }, { PCMK_OPT_SHUTDOWN_LOCK, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_FALSE, pcmk__valid_boolean, pcmk__opt_schedulerd, N_("Whether to lock resources to a cleanly shut down node"), N_("When true, resources active on a node when it is cleanly shut down " "are kept \"locked\" to that node (not allowed to run elsewhere) " "until they start again on that node after it rejoins (or for at " "most shutdown-lock-limit, if set). Stonith resources and " "Pacemaker Remote connections are never locked. Clone and bundle " "instances and the promoted role of promotable clones are " "currently never locked, though support could be added in a future " "release."), }, { PCMK_OPT_SHUTDOWN_LOCK_LIMIT, NULL, PCMK_VALUE_DURATION, NULL, "0", pcmk__valid_interval_spec, pcmk__opt_schedulerd, N_("Do not lock resources to a cleanly shut down node longer than " "this"), N_("If shutdown-lock is true and this is set to a nonzero time " "duration, shutdown locks will expire after this much time has " "passed since the shutdown was initiated, even if the node has not " "rejoined."), }, { PCMK_OPT_ENABLE_ACL, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_FALSE, pcmk__valid_boolean, pcmk__opt_based, N_("Enable Access Control Lists (ACLs) for the CIB"), NULL, }, { PCMK_OPT_SYMMETRIC_CLUSTER, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_TRUE, pcmk__valid_boolean, pcmk__opt_schedulerd, N_("Whether resources can run on any node by default"), NULL, }, { PCMK_OPT_MAINTENANCE_MODE, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_FALSE, pcmk__valid_boolean, pcmk__opt_schedulerd, N_("Whether the cluster should refrain from monitoring, starting, and " "stopping resources"), NULL, }, { PCMK_OPT_START_FAILURE_IS_FATAL, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_TRUE, pcmk__valid_boolean, pcmk__opt_schedulerd, N_("Whether a start failure should prevent a resource from being " "recovered on the same node"), N_("When true, the cluster will immediately ban a resource from a node " "if it fails to start there. When false, the cluster will instead " "check the resource's fail count against its migration-threshold.") }, { PCMK_OPT_ENABLE_STARTUP_PROBES, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_TRUE, pcmk__valid_boolean, pcmk__opt_schedulerd, N_("Whether the cluster should check for active resources during " "start-up"), NULL, }, // Fencing-related options { PCMK_OPT_FENCE_REMOTE_WITHOUT_QUORUM, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_FALSE, pcmk__valid_boolean, pcmk__opt_schedulerd|pcmk__opt_advanced, N_("Whether remote nodes can be fenced without quorum"), N_("By default, an inquorate node can not fence Pacemaker Remote nodes " "that are part of its partition as long as the cluster thinks they " "can be restarted. If true, inquorate nodes will be able to fence " "remote nodes regardless."), }, { PCMK_OPT_STONITH_ENABLED, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_TRUE, pcmk__valid_boolean, pcmk__opt_schedulerd|pcmk__opt_advanced, N_("Whether nodes may be fenced as part of recovery"), N_("If false, unresponsive nodes are immediately assumed to be " "harmless, and resources that were active on them may be recovered " "elsewhere. This can result in a \"split-brain\" situation, " "potentially leading to data loss and/or service unavailability."), }, { PCMK_OPT_STONITH_ACTION, NULL, PCMK_VALUE_SELECT, PCMK_ACTION_REBOOT ", " PCMK_ACTION_OFF, PCMK_ACTION_REBOOT, pcmk__is_fencing_action, pcmk__opt_schedulerd, N_("Action to send to fence device when a node needs to be fenced"), NULL, }, { PCMK_OPT_STONITH_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL, "60s", pcmk__valid_interval_spec, pcmk__opt_schedulerd, N_("How long to wait for on, off, and reboot fence actions to complete " "by default"), NULL, }, { PCMK_OPT_HAVE_WATCHDOG, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_FALSE, pcmk__valid_boolean, pcmk__opt_schedulerd|pcmk__opt_generated, N_("Whether watchdog integration is enabled"), N_("This is set automatically by the cluster according to whether SBD " "is detected to be in use. User-configured values are ignored. " "The value `true` is meaningful if diskless SBD is used and " "`stonith-watchdog-timeout` is nonzero. In that case, if fencing " "is required, watchdog-based self-fencing will be performed via " "SBD without requiring a fencing resource explicitly configured."), }, { /* @COMPAT Currently, unparsable values default to -1 (auto-calculate), * while missing values default to 0 (disable). All values are accepted * (unless the controller finds that the value conflicts with the * SBD_WATCHDOG_TIMEOUT). * * At a compatibility break: properly validate as a timeout, let * either negative values or a particular string like "auto" mean auto- * calculate, and use 0 as the single default for when the option either * is unset or fails to validate. */ PCMK_OPT_STONITH_WATCHDOG_TIMEOUT, NULL, PCMK_VALUE_TIMEOUT, NULL, "0", NULL, pcmk__opt_controld, N_("How long before nodes can be assumed to be safely down when " "watchdog-based self-fencing via SBD is in use"), N_("If this is set to a positive value, lost nodes are assumed to " "achieve self-fencing using watchdog-based SBD within this much " "time. This does not require a fencing resource to be explicitly " "configured, though a fence_watchdog resource can be configured, to " "limit use to specific nodes. If this is set to 0 (the default), " "the cluster will never assume watchdog-based self-fencing. If this " "is set to a negative value, the cluster will use twice the local " "value of the `SBD_WATCHDOG_TIMEOUT` environment variable if that " "is positive, or otherwise treat this as 0. WARNING: When used, " "this timeout must be larger than `SBD_WATCHDOG_TIMEOUT` on all " "nodes that use watchdog-based SBD, and Pacemaker will refuse to " "start on any of those nodes where this is not true for the local " "value or SBD is not active. When this is set to a negative value, " "`SBD_WATCHDOG_TIMEOUT` must be set to the same value on all nodes " "that use SBD, otherwise data corruption or loss could occur."), }, { PCMK_OPT_STONITH_MAX_ATTEMPTS, NULL, PCMK_VALUE_SCORE, NULL, "10", pcmk__valid_positive_int, pcmk__opt_controld, N_("How many times fencing can fail before it will no longer be " "immediately re-attempted on a target"), NULL, }, { PCMK_OPT_CONCURRENT_FENCING, NULL, PCMK_VALUE_BOOLEAN, NULL, #if PCMK__CONCURRENT_FENCING_DEFAULT_TRUE PCMK_VALUE_TRUE, #else PCMK_VALUE_FALSE, #endif pcmk__valid_boolean, pcmk__opt_schedulerd|pcmk__opt_deprecated, N_("Allow performing fencing operations in parallel"), NULL, }, { PCMK_OPT_STARTUP_FENCING, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_TRUE, pcmk__valid_boolean, pcmk__opt_schedulerd|pcmk__opt_advanced, N_("Whether to fence unseen nodes at start-up"), N_("Setting this to false may lead to a \"split-brain\" situation, " "potentially leading to data loss and/or service unavailability."), }, { PCMK_OPT_PRIORITY_FENCING_DELAY, NULL, PCMK_VALUE_DURATION, NULL, "0", pcmk__valid_interval_spec, pcmk__opt_schedulerd, N_("Apply fencing delay targeting the lost nodes with the highest " "total resource priority"), N_("Apply specified delay for the fencings that are targeting the lost " "nodes with the highest total resource priority in case we don't " "have the majority of the nodes in our cluster partition, so that " "the more significant nodes potentially win any fencing match, " "which is especially meaningful under split-brain of 2-node " "cluster. A promoted resource instance takes the base priority + 1 " "on calculation if the base priority is not 0. Any static/random " "delays that are introduced by `pcmk_delay_base/max` configured " "for the corresponding fencing resources will be added to this " "delay. This delay should be significantly greater than, safely " "twice, the maximum `pcmk_delay_base/max`. By default, priority " "fencing delay is disabled."), }, { PCMK_OPT_NODE_PENDING_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL, "0", pcmk__valid_interval_spec, pcmk__opt_schedulerd, N_("How long to wait for a node that has joined the cluster to join " "the controller process group"), N_("Fence nodes that do not join the controller process group within " "this much time after joining the cluster, to allow the cluster " "to continue managing resources. A value of 0 means never fence " "pending nodes. Setting the value to 2h means fence nodes after " "2 hours."), }, { PCMK_OPT_CLUSTER_DELAY, NULL, PCMK_VALUE_DURATION, NULL, "60s", pcmk__valid_interval_spec, pcmk__opt_schedulerd, N_("Maximum time for node-to-node communication"), N_("The node elected Designated Controller (DC) will consider an action " "failed if it does not get a response from the node executing the " "action within this time (after considering the action's own " "timeout). The \"correct\" value will depend on the speed and " "load of your network and cluster nodes.") }, // Limits { PCMK_OPT_LOAD_THRESHOLD, NULL, PCMK_VALUE_PERCENTAGE, NULL, "80%", pcmk__valid_percentage, pcmk__opt_controld, N_("Maximum amount of system load that should be used by cluster " "nodes"), N_("The cluster will slow down its recovery process when the amount of " "system resources used (currently CPU) approaches this limit"), }, { PCMK_OPT_NODE_ACTION_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL, "0", pcmk__valid_int, pcmk__opt_controld, N_("Maximum number of jobs that can be scheduled per node (defaults to " "2x cores)"), NULL, }, { PCMK_OPT_BATCH_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL, "0", pcmk__valid_int, pcmk__opt_schedulerd, N_("Maximum number of jobs that the cluster may execute in parallel " "across all nodes"), N_("The \"correct\" value will depend on the speed and load of your " "network and cluster nodes. If set to 0, the cluster will " "impose a dynamically calculated limit when any node has a " "high load."), }, { PCMK_OPT_MIGRATION_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL, "-1", pcmk__valid_int, pcmk__opt_schedulerd, N_("The number of live migration actions that the cluster is allowed " "to execute in parallel on a node (-1 means no limit)"), NULL, }, { /* @TODO This is actually ignored if not strictly positive. We should * overhaul value types in Pacemaker Explained. There are lots of * inaccurate ranges (assumptions of 32-bit width, "nonnegative" when * positive is required, etc.). * * Maybe a single integer type with the allowed range specified would be * better. * * Drop the PCMK_VALUE_NONNEGATIVE_INTEGER constant if we do this before * a release. */ PCMK_OPT_CLUSTER_IPC_LIMIT, NULL, PCMK_VALUE_NONNEGATIVE_INTEGER, NULL, "500", pcmk__valid_positive_int, pcmk__opt_based, N_("Maximum IPC message backlog before disconnecting a cluster daemon"), N_("Raise this if log has \"Evicting client\" messages for cluster " "daemon PIDs (a good value is the number of resources in the " "cluster multiplied by the number of nodes)."), }, // Orphans and stopping { PCMK_OPT_STOP_ALL_RESOURCES, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_FALSE, pcmk__valid_boolean, pcmk__opt_schedulerd, N_("Whether the cluster should stop all active resources"), NULL, }, { PCMK_OPT_STOP_ORPHAN_RESOURCES, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_TRUE, pcmk__valid_boolean, pcmk__opt_schedulerd, N_("Whether to stop resources that were removed from the " "configuration"), NULL, }, { PCMK_OPT_STOP_ORPHAN_ACTIONS, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_TRUE, pcmk__valid_boolean, pcmk__opt_schedulerd, N_("Whether to cancel recurring actions removed from the " "configuration"), NULL, }, // Storing inputs { PCMK_OPT_PE_ERROR_SERIES_MAX, NULL, PCMK_VALUE_INTEGER, NULL, "-1", pcmk__valid_int, pcmk__opt_schedulerd, N_("The number of scheduler inputs resulting in errors to save"), N_("Zero to disable, -1 to store unlimited."), }, { PCMK_OPT_PE_WARN_SERIES_MAX, NULL, PCMK_VALUE_INTEGER, NULL, "5000", pcmk__valid_int, pcmk__opt_schedulerd, N_("The number of scheduler inputs resulting in warnings to save"), N_("Zero to disable, -1 to store unlimited."), }, { PCMK_OPT_PE_INPUT_SERIES_MAX, NULL, PCMK_VALUE_INTEGER, NULL, "4000", pcmk__valid_int, pcmk__opt_schedulerd, N_("The number of scheduler inputs without errors or warnings to save"), N_("Zero to disable, -1 to store unlimited."), }, // Node health { PCMK_OPT_NODE_HEALTH_STRATEGY, NULL, PCMK_VALUE_SELECT, PCMK_VALUE_NONE ", " PCMK_VALUE_MIGRATE_ON_RED ", " PCMK_VALUE_ONLY_GREEN ", " PCMK_VALUE_PROGRESSIVE ", " PCMK_VALUE_CUSTOM, PCMK_VALUE_NONE, pcmk__validate_health_strategy, pcmk__opt_schedulerd, N_("How cluster should react to node health attributes"), N_("Requires external entities to create node attributes (named with " "the prefix \"#health\") with values \"red\", \"yellow\", or " "\"green\".") }, { PCMK_OPT_NODE_HEALTH_BASE, NULL, PCMK_VALUE_SCORE, NULL, "0", pcmk__valid_int, pcmk__opt_schedulerd, N_("Base health score assigned to a node"), N_("Only used when \"node-health-strategy\" is set to " "\"progressive\"."), }, { PCMK_OPT_NODE_HEALTH_GREEN, NULL, PCMK_VALUE_SCORE, NULL, "0", pcmk__valid_int, pcmk__opt_schedulerd, N_("The score to use for a node health attribute whose value is " "\"green\""), N_("Only used when \"node-health-strategy\" is set to \"custom\" or " "\"progressive\"."), }, { PCMK_OPT_NODE_HEALTH_YELLOW, NULL, PCMK_VALUE_SCORE, NULL, "0", pcmk__valid_int, pcmk__opt_schedulerd, N_("The score to use for a node health attribute whose value is " "\"yellow\""), N_("Only used when \"node-health-strategy\" is set to \"custom\" or " "\"progressive\"."), }, { PCMK_OPT_NODE_HEALTH_RED, NULL, PCMK_VALUE_SCORE, NULL, "-INFINITY", pcmk__valid_int, pcmk__opt_schedulerd, N_("The score to use for a node health attribute whose value is " "\"red\""), N_("Only used when \"node-health-strategy\" is set to \"custom\" or " "\"progressive\".") }, // Placement strategy { PCMK_OPT_PLACEMENT_STRATEGY, NULL, PCMK_VALUE_SELECT, PCMK_VALUE_DEFAULT ", " PCMK_VALUE_UTILIZATION ", " PCMK_VALUE_MINIMAL ", " PCMK_VALUE_BALANCED, PCMK_VALUE_DEFAULT, pcmk__valid_placement_strategy, pcmk__opt_schedulerd, N_("How the cluster should allocate resources to nodes"), NULL, }, { NULL, }, }; static const pcmk__cluster_option_t fencing_params[] = { /* name, old name, type, allowed values, * default value, validator, * flags, * short description, * long description */ { PCMK_STONITH_HOST_ARGUMENT, NULL, PCMK_VALUE_STRING, NULL, NULL, NULL, pcmk__opt_advanced, N_("Name of agent parameter that should be set to the fencing target"), N_("If the fencing agent metadata advertises support for the \"port\" " "or \"plug\" parameter, that will be used as the default, " "otherwise \"none\" will be used, which tells the cluster not to " "supply any additional parameters."), }, { PCMK_STONITH_HOST_MAP, NULL, PCMK_VALUE_STRING, NULL, NULL, NULL, pcmk__opt_none, N_("A mapping of node names to port numbers for devices that do not " "support node names."), N_("For example, \"node1:1;node2:2,3\" would tell the cluster to use " "port 1 for node1 and ports 2 and 3 for node2."), }, { PCMK_STONITH_HOST_LIST, NULL, PCMK_VALUE_STRING, NULL, NULL, NULL, pcmk__opt_none, N_("Nodes targeted by this device"), N_("Comma-separated list of nodes that can be targeted by this device " "(for example, \"node1,node2,node3\"). If pcmk_host_check is " "\"static-list\", either this or pcmk_host_map must be set."), }, { PCMK_STONITH_HOST_CHECK, NULL, PCMK_VALUE_SELECT, PCMK_VALUE_DYNAMIC_LIST ", " PCMK_VALUE_STATIC_LIST ", " PCMK_VALUE_STATUS ", " PCMK_VALUE_NONE, NULL, NULL, pcmk__opt_none, N_("How to determine which nodes can be targeted by the device"), N_("Use \"dynamic-list\" to query the device via the 'list' command; " "\"static-list\" to check the pcmk_host_list attribute; " "\"status\" to query the device via the 'status' command; or " "\"none\" to assume every device can fence every node. " "The default value is \"static-list\" if pcmk_host_map or " "pcmk_host_list is set; otherwise \"dynamic-list\" if the device " "supports the list operation; otherwise \"status\" if the device " "supports the status operation; otherwise \"none\""), }, { PCMK_STONITH_DELAY_MAX, NULL, PCMK_VALUE_DURATION, NULL, "0s", NULL, pcmk__opt_none, N_("Enable a delay of no more than the time specified before executing " "fencing actions."), N_("Enable a delay of no more than the time specified before executing " "fencing actions. Pacemaker derives the overall delay by taking " "the value of pcmk_delay_base and adding a random delay value such " "that the sum is kept below this maximum."), }, { PCMK_STONITH_DELAY_BASE, NULL, PCMK_VALUE_STRING, NULL, "0s", NULL, pcmk__opt_none, N_("Enable a base delay for fencing actions and specify base delay " "value."), N_("This enables a static delay for fencing actions, which can help " "avoid \"death matches\" where two nodes try to fence each other " "at the same time. If pcmk_delay_max is also used, a random delay " "will be added such that the total delay is kept below that value. " "This can be set to a single time value to apply to any node " "targeted by this device (useful if a separate device is " "configured for each target), or to a node map (for example, " "\"node1:1s;node2:5\") to set a different value for each target."), }, { PCMK_STONITH_ACTION_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL, "1", NULL, pcmk__opt_none, N_("The maximum number of actions can be performed in parallel on this " "device"), N_("If the concurrent-fencing cluster property is \"true\", this " "specifies the maximum number of actions that can be performed in " "parallel on this device. A value of -1 means unlimited."), }, { "pcmk_reboot_action", NULL, PCMK_VALUE_STRING, NULL, PCMK_ACTION_REBOOT, NULL, pcmk__opt_advanced, N_("An alternate command to run instead of 'reboot'"), N_("Some devices do not support the standard commands or may provide " "additional ones. Use this to specify an alternate, device-" "specific, command that implements the 'reboot' action."), }, { "pcmk_reboot_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL, "60s", NULL, pcmk__opt_advanced, N_("Specify an alternate timeout to use for 'reboot' actions instead " "of stonith-timeout"), N_("Some devices need much more/less time to complete than normal. " "Use this to specify an alternate, device-specific, timeout for " "'reboot' actions."), }, { "pcmk_reboot_retries", NULL, PCMK_VALUE_INTEGER, NULL, "2", NULL, pcmk__opt_advanced, N_("The maximum number of times to try the 'reboot' command within the " "timeout period"), N_("Some devices do not support multiple connections. Operations may " "\"fail\" if the device is busy with another task. In that case, " "Pacemaker will automatically retry the operation if there is time " "remaining. Use this option to alter the number of times Pacemaker " "tries a 'reboot' action before giving up."), }, { "pcmk_off_action", NULL, PCMK_VALUE_STRING, NULL, PCMK_ACTION_OFF, NULL, pcmk__opt_advanced, N_("An alternate command to run instead of 'off'"), N_("Some devices do not support the standard commands or may provide " "additional ones. Use this to specify an alternate, device-" "specific, command that implements the 'off' action."), }, { "pcmk_off_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL, "60s", NULL, pcmk__opt_advanced, N_("Specify an alternate timeout to use for 'off' actions instead of " "stonith-timeout"), N_("Some devices need much more/less time to complete than normal. " "Use this to specify an alternate, device-specific, timeout for " "'off' actions."), }, { "pcmk_off_retries", NULL, PCMK_VALUE_INTEGER, NULL, "2", NULL, pcmk__opt_advanced, N_("The maximum number of times to try the 'off' command within the " "timeout period"), N_("Some devices do not support multiple connections. Operations may " "\"fail\" if the device is busy with another task. In that case, " "Pacemaker will automatically retry the operation if there is time " "remaining. Use this option to alter the number of times Pacemaker " "tries a 'off' action before giving up."), }, { "pcmk_on_action", NULL, PCMK_VALUE_STRING, NULL, PCMK_ACTION_ON, NULL, pcmk__opt_advanced, N_("An alternate command to run instead of 'on'"), N_("Some devices do not support the standard commands or may provide " "additional ones. Use this to specify an alternate, device-" "specific, command that implements the 'on' action."), }, { "pcmk_on_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL, "60s", NULL, pcmk__opt_advanced, N_("Specify an alternate timeout to use for 'on' actions instead of " "stonith-timeout"), N_("Some devices need much more/less time to complete than normal. " "Use this to specify an alternate, device-specific, timeout for " "'on' actions."), }, { "pcmk_on_retries", NULL, PCMK_VALUE_INTEGER, NULL, "2", NULL, pcmk__opt_advanced, N_("The maximum number of times to try the 'on' command within the " "timeout period"), N_("Some devices do not support multiple connections. Operations may " "\"fail\" if the device is busy with another task. In that case, " "Pacemaker will automatically retry the operation if there is time " "remaining. Use this option to alter the number of times Pacemaker " "tries a 'on' action before giving up."), }, { "pcmk_list_action", NULL, PCMK_VALUE_STRING, NULL, PCMK_ACTION_LIST, NULL, pcmk__opt_advanced, N_("An alternate command to run instead of 'list'"), N_("Some devices do not support the standard commands or may provide " "additional ones. Use this to specify an alternate, device-" "specific, command that implements the 'list' action."), }, { "pcmk_list_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL, "60s", NULL, pcmk__opt_advanced, N_("Specify an alternate timeout to use for 'list' actions instead of " "stonith-timeout"), N_("Some devices need much more/less time to complete than normal. " "Use this to specify an alternate, device-specific, timeout for " "'list' actions."), }, { "pcmk_list_retries", NULL, PCMK_VALUE_INTEGER, NULL, "2", NULL, pcmk__opt_advanced, N_("The maximum number of times to try the 'list' command within the " "timeout period"), N_("Some devices do not support multiple connections. Operations may " "\"fail\" if the device is busy with another task. In that case, " "Pacemaker will automatically retry the operation if there is time " "remaining. Use this option to alter the number of times Pacemaker " "tries a 'list' action before giving up."), }, { "pcmk_monitor_action", NULL, PCMK_VALUE_STRING, NULL, PCMK_ACTION_MONITOR, NULL, pcmk__opt_advanced, N_("An alternate command to run instead of 'monitor'"), N_("Some devices do not support the standard commands or may provide " "additional ones. Use this to specify an alternate, device-" "specific, command that implements the 'monitor' action."), }, { "pcmk_monitor_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL, "60s", NULL, pcmk__opt_advanced, N_("Specify an alternate timeout to use for 'monitor' actions instead " "of stonith-timeout"), N_("Some devices need much more/less time to complete than normal. " "Use this to specify an alternate, device-specific, timeout for " "'monitor' actions."), }, { "pcmk_monitor_retries", NULL, PCMK_VALUE_INTEGER, NULL, "2", NULL, pcmk__opt_advanced, N_("The maximum number of times to try the 'monitor' command within " "the timeout period"), N_("Some devices do not support multiple connections. Operations may " "\"fail\" if the device is busy with another task. In that case, " "Pacemaker will automatically retry the operation if there is time " "remaining. Use this option to alter the number of times Pacemaker " "tries a 'monitor' action before giving up."), }, { "pcmk_status_action", NULL, PCMK_VALUE_STRING, NULL, PCMK_ACTION_STATUS, NULL, pcmk__opt_advanced, N_("An alternate command to run instead of 'status'"), N_("Some devices do not support the standard commands or may provide " "additional ones. Use this to specify an alternate, device-" "specific, command that implements the 'status' action."), }, { "pcmk_status_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL, "60s", NULL, pcmk__opt_advanced, N_("Specify an alternate timeout to use for 'status' actions instead " "of stonith-timeout"), N_("Some devices need much more/less time to complete than normal. " "Use this to specify an alternate, device-specific, timeout for " "'status' actions."), }, { "pcmk_status_retries", NULL, PCMK_VALUE_INTEGER, NULL, "2", NULL, pcmk__opt_advanced, N_("The maximum number of times to try the 'status' command within " "the timeout period"), N_("Some devices do not support multiple connections. Operations may " "\"fail\" if the device is busy with another task. In that case, " "Pacemaker will automatically retry the operation if there is time " "remaining. Use this option to alter the number of times Pacemaker " "tries a 'status' action before giving up."), }, { NULL, }, }; static const pcmk__cluster_option_t primitive_meta[] = { /* name, old name, type, allowed values, * default value, validator, * flags, * short description, * long description */ { PCMK_META_PRIORITY, NULL, PCMK_VALUE_SCORE, NULL, "0", NULL, pcmk__opt_none, N_("Resource assignment priority"), N_("If not all resources can be active, the cluster will stop " "lower-priority resources in order to keep higher-priority ones " "active."), }, { PCMK_META_CRITICAL, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_TRUE, NULL, pcmk__opt_none, N_("Default value for influence in colocation constraints"), N_("Use this value as the default for influence in all colocation " "constraints involving this resource, as well as in the implicit " "colocation constraints created if this resource is in a group."), }, { PCMK_META_TARGET_ROLE, NULL, PCMK_VALUE_SELECT, PCMK_ROLE_STOPPED ", " PCMK_ROLE_STARTED ", " PCMK_ROLE_UNPROMOTED ", " PCMK_ROLE_PROMOTED, PCMK_ROLE_STARTED, NULL, pcmk__opt_none, N_("State the cluster should attempt to keep this resource in"), N_("\"Stopped\" forces the resource to be stopped. " "\"Started\" allows the resource to be started (and in the case of " "promotable clone resources, promoted if appropriate). " "\"Unpromoted\" allows the resource to be started, but only in the " "unpromoted role if the resource is promotable. " "\"Promoted\" is equivalent to \"Started\"."), }, { PCMK_META_IS_MANAGED, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_TRUE, NULL, pcmk__opt_none, N_("Whether the cluster is allowed to actively change the resource's " "state"), N_("If false, the cluster will not start, stop, promote, or demote the " "resource on any node. Recurring actions for the resource are " "unaffected. If true, a true value for the maintenance-mode " "cluster option, the maintenance node attribute, or the " "maintenance resource meta-attribute overrides this."), }, { PCMK_META_MAINTENANCE, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_FALSE, NULL, pcmk__opt_none, N_("If true, the cluster will not schedule any actions involving the " "resource"), N_("If true, the cluster will not start, stop, promote, or demote the " "resource on any node, and will pause any recurring monitors " "(except those specifying role as \"Stopped\"). If false, a true " "value for the maintenance-mode cluster option or maintenance node " "attribute overrides this."), }, { PCMK_META_RESOURCE_STICKINESS, NULL, PCMK_VALUE_SCORE, NULL, NULL, NULL, pcmk__opt_none, N_("Score to add to the current node when a resource is already " "active"), N_("Score to add to the current node when a resource is already " "active. This allows running resources to stay where they are, " "even if they would be placed elsewhere if they were being started " "from a stopped state. " "The default is 1 for individual clone instances, and 0 for all " "other resources."), }, { PCMK_META_REQUIRES, NULL, PCMK_VALUE_SELECT, PCMK_VALUE_NOTHING ", " PCMK_VALUE_QUORUM ", " PCMK_VALUE_FENCING ", " PCMK_VALUE_UNFENCING, NULL, NULL, pcmk__opt_none, N_("Conditions under which the resource can be started"), N_("Conditions under which the resource can be started. " "\"nothing\" means the cluster can always start this resource. " "\"quorum\" means the cluster can start this resource only if a " "majority of the configured nodes are active. " "\"fencing\" means the cluster can start this resource only if a " "majority of the configured nodes are active and any failed or " "unknown nodes have been fenced. " "\"unfencing\" means the cluster can start this resource only if " "a majority of the configured nodes are active and any failed or " "unknown nodes have been fenced, and only on nodes that have been " "unfenced. " "The default is \"quorum\" for resources with a class of stonith; " "otherwise, \"unfencing\" if unfencing is active in the cluster; " "otherwise, \"fencing\" if the stonith-enabled cluster option is " "true; " "otherwise, \"quorum\"."), }, { PCMK_META_MIGRATION_THRESHOLD, NULL, PCMK_VALUE_SCORE, NULL, PCMK_VALUE_INFINITY, NULL, pcmk__opt_none, N_("Number of failures on a node before the resource becomes " "ineligible to run there."), N_("Number of failures that may occur for this resource on a node, " "before that node is marked ineligible to host this resource. A " "value of 0 indicates that this feature is disabled (the node will " "never be marked ineligible). By contrast, the cluster treats " "\"INFINITY\" (the default) as a very large but finite number. " "This option has an effect only if the failed operation specifies " "its on-fail attribute as \"restart\" (the default), and " "additionally for failed start operations, if the " "start-failure-is-fatal cluster property is set to false."), }, { PCMK_META_FAILURE_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL, "0", NULL, pcmk__opt_none, N_("Number of seconds before acting as if a failure had not occurred"), N_("Number of seconds after a failed action for this resource before " "acting as if the failure had not occurred, and potentially " "allowing the resource back to the node on which it failed. " "A value of 0 indicates that this feature is disabled."), }, { PCMK_META_MULTIPLE_ACTIVE, NULL, PCMK_VALUE_SELECT, PCMK_VALUE_BLOCK ", " PCMK_VALUE_STOP_ONLY ", " PCMK_VALUE_STOP_START ", " PCMK_VALUE_STOP_UNEXPECTED, PCMK_VALUE_STOP_START, NULL, pcmk__opt_none, N_("What to do if the cluster finds the resource active on more than " "one node"), N_("What to do if the cluster finds the resource active on more than " "one node. " "\"block\" means to mark the resource as unmanaged. " "\"stop_only\" means to stop all active instances of this resource " "and leave them stopped. " "\"stop_start\" means to stop all active instances of this " "resource and start the resource in one location only. " "\"stop_unexpected\" means to stop all active instances of this " "resource except where the resource should be active. (This should " "be used only when extra instances are not expected to disrupt " "existing instances, and the resource agent's monitor of an " "existing instance is capable of detecting any problems that could " "be caused. Note that any resources ordered after this one will " "still need to be restarted.)"), }, { PCMK_META_ALLOW_MIGRATE, NULL, PCMK_VALUE_BOOLEAN, NULL, NULL, NULL, pcmk__opt_none, N_("Whether the cluster should try to \"live migrate\" this resource " "when it needs to be moved"), N_("Whether the cluster should try to \"live migrate\" this resource " "when it needs to be moved. " "The default is true for ocf:pacemaker:remote resources, and false " "otherwise."), }, { PCMK_META_ALLOW_UNHEALTHY_NODES, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_FALSE, NULL, pcmk__opt_none, N_("Whether the resource should be allowed to run on a node even if " "the node's health score would otherwise prevent it"), NULL, }, { PCMK_META_CONTAINER_ATTRIBUTE_TARGET, NULL, PCMK_VALUE_STRING, NULL, NULL, NULL, pcmk__opt_none, N_("Where to check user-defined node attributes"), N_("Whether to check user-defined node attributes on the physical host " "where a container is running or on the local node. This is " "usually set for a bundle resource and inherited by the bundle's " "primitive resource. " "A value of \"host\" means to check user-defined node attributes " "on the underlying physical host. Any other value means to check " "user-defined node attributes on the local node (for a bundled " "primitive resource, this is the bundle node)."), }, { PCMK_META_REMOTE_NODE, NULL, PCMK_VALUE_STRING, NULL, NULL, NULL, pcmk__opt_none, N_("Name of the Pacemaker Remote guest node this resource is " "associated with, if any"), N_("Name of the Pacemaker Remote guest node this resource is " "associated with, if any. If specified, this both enables the " "resource as a guest node and defines the unique name used to " "identify the guest node. The guest must be configured to run the " "Pacemaker Remote daemon when it is started. " "WARNING: This value cannot overlap with any resource or node " "IDs."), }, { PCMK_META_REMOTE_ADDR, NULL, PCMK_VALUE_STRING, NULL, NULL, NULL, pcmk__opt_none, N_("If remote-node is specified, the IP address or hostname used to " "connect to the guest via Pacemaker Remote"), N_("If remote-node is specified, the IP address or hostname used to " "connect to the guest via Pacemaker Remote. The Pacemaker Remote " "daemon on the guest must be configured to accept connections on " "this address. " "The default is the value of the remote-node meta-attribute."), }, { PCMK_META_REMOTE_PORT, NULL, PCMK_VALUE_PORT, NULL, "3121", NULL, pcmk__opt_none, N_("If remote-node is specified, port on the guest used for its " "Pacemaker Remote connection"), N_("If remote-node is specified, the port on the guest used for its " "Pacemaker Remote connection. The Pacemaker Remote daemon on the " "guest must be configured to listen on this port."), }, { PCMK_META_REMOTE_CONNECT_TIMEOUT, NULL, PCMK_VALUE_TIMEOUT, NULL, "60s", NULL, pcmk__opt_none, N_("If remote-node is specified, how long before a pending Pacemaker " "Remote guest connection times out."), NULL, }, { PCMK_META_REMOTE_ALLOW_MIGRATE, NULL, PCMK_VALUE_BOOLEAN, NULL, PCMK_VALUE_TRUE, NULL, pcmk__opt_none, N_("If remote-node is specified, this acts as the allow-migrate " "meta-attribute for the implicit remote connection resource " "(ocf:pacemaker:remote)."), NULL, }, { NULL, }, }; /* * Environment variable option handling */ /*! * \internal * \brief Get the value of a Pacemaker environment variable option * - * If an environment variable option is set, with either a PCMK_ or (for - * backward compatibility) HA_ prefix, log and return the value. + * If an environment variable option is set, with either a \c "PCMK_" or (for + * backward compatibility) \c "HA_" prefix, log and return the value. * * \param[in] option Environment variable name (without prefix) * - * \return Value of environment variable option, or NULL in case of - * option name too long or value not found + * \return Value of environment variable, or \c NULL if not set */ const char * pcmk__env_option(const char *option) { - const char *const prefixes[] = {"PCMK_", "HA_"}; - char env_name[NAME_MAX]; - const char *value = NULL; + // @COMPAT Drop support for "HA_" options eventually + static const char *const prefixes[] = { "PCMK", "HA" }; CRM_CHECK(!pcmk__str_empty(option), return NULL); for (int i = 0; i < PCMK__NELEM(prefixes); i++) { - int rv = snprintf(env_name, NAME_MAX, "%s%s", prefixes[i], option); - - if (rv < 0) { - crm_err("Failed to write %s%s to buffer: %s", prefixes[i], option, - strerror(errno)); - return NULL; - } - - if (rv >= sizeof(env_name)) { - crm_trace("\"%s%s\" is too long", prefixes[i], option); - continue; - } + char *env_name = crm_strdup_printf("%s_%s", prefixes[i], option); + const char *value = getenv(env_name); - value = getenv(env_name); if (value != NULL) { crm_trace("Found %s = %s", env_name, value); + free(env_name); return value; } + free(env_name); } crm_trace("Nothing found for %s", option); return NULL; } /*! + * \internal * \brief Set or unset a Pacemaker environment variable option * * Set an environment variable option with a \c "PCMK_" prefix and optionally * an \c "HA_" prefix for backward compatibility. * * \param[in] option Environment variable name (without prefix) * \param[in] value New value (or NULL to unset) * \param[in] compat If false and \p value is not \c NULL, set only * \c "PCMK_