diff --git a/daemons/based/based_remote.c b/daemons/based/based_remote.c index 8dcb514872..799f3b2121 100644 --- a/daemons/based/based_remote.c +++ b/daemons/based/based_remote.c @@ -1,671 +1,671 @@ /* * 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 #include // PRIx64 #include #include #include #include #include #include #include #include #include #include #include #include #include #include "pacemaker-based.h" #include #include #include #if HAVE_SECURITY_PAM_APPL_H # include # define HAVE_PAM 1 #elif HAVE_PAM_PAM_APPL_H # include # define HAVE_PAM 1 #endif static pcmk__tls_t *tls = NULL; extern int remote_tls_fd; extern gboolean cib_shutdown_flag; int init_remote_listener(int port, gboolean encrypted); void cib_remote_connection_destroy(gpointer user_data); // @TODO This is rather short for someone to type their password #define REMOTE_AUTH_TIMEOUT 10000 int num_clients; static bool authenticate_user(const char *user, const char *passwd); static int cib_remote_listen(gpointer data); static int cib_remote_msg(gpointer data); static void remote_connection_destroy(gpointer user_data) { pcmk__info("No longer listening for remote connections"); return; } int init_remote_listener(int port, gboolean encrypted) { int rc; int *ssock = NULL; struct sockaddr_in saddr; int optval; static struct mainloop_fd_callbacks remote_listen_fd_callbacks = { .dispatch = cib_remote_listen, .destroy = remote_connection_destroy, }; if (port <= 0) { /* don't start it */ return 0; } if (encrypted) { bool use_cert = pcmk__x509_enabled(); pcmk__notice("Starting TLS listener on port %d", port); rc = pcmk__init_tls(&tls, true, use_cert ? GNUTLS_CRD_CERTIFICATE : GNUTLS_CRD_ANON); if (rc != pcmk_rc_ok) { return -1; } } else { pcmk__warn("Starting plain-text listener on port %d", port); } #ifndef HAVE_PAM pcmk__warn("This build does not support remote administrators because PAM " "support is not available"); #endif /* create server socket */ ssock = pcmk__assert_alloc(1, sizeof(int)); *ssock = socket(AF_INET, SOCK_STREAM, 0); if (*ssock == -1) { pcmk__err("Listener socket creation failed: %s", pcmk_rc_str(errno)); free(ssock); return -1; } /* reuse address */ optval = 1; rc = setsockopt(*ssock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); if (rc < 0) { pcmk__err("Local address reuse not allowed on listener socket: %s", pcmk_rc_str(errno)); } /* bind server socket */ memset(&saddr, '\0', sizeof(saddr)); saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons(port); if (bind(*ssock, (struct sockaddr *)&saddr, sizeof(saddr)) == -1) { pcmk__err("Cannot bind to listener socket: %s", pcmk_rc_str(errno)); close(*ssock); free(ssock); return -2; } if (listen(*ssock, 10) == -1) { pcmk__err("Cannot listen on socket: %s", pcmk_rc_str(errno)); close(*ssock); free(ssock); return -3; } mainloop_add_fd("cib-remote", G_PRIORITY_DEFAULT, *ssock, ssock, &remote_listen_fd_callbacks); pcmk__debug("Started listener on port %d", port); return *ssock; } static int check_group_membership(const char *usr, const char *grp) { int index = 0; gid_t gid = 0; struct group *group = NULL; int rc = pcmk_rc_ok; rc = pcmk__lookup_user(usr, NULL, &gid); if (rc != pcmk_rc_ok) { pcmk__notice("Rejecting remote client: could not find user '%s': %s", usr, pcmk_rc_str(rc)); return FALSE; } group = getgrgid(gid); if (group != NULL && pcmk__str_eq(grp, group->gr_name, pcmk__str_none)) { return TRUE; } group = getgrnam(grp); if (group == NULL) { pcmk__err("Rejecting remote client: '%s' is not a valid group", grp); return FALSE; } while (TRUE) { char *member = group->gr_mem[index++]; if (member == NULL) { break; } else if (pcmk__str_eq(usr, member, pcmk__str_none)) { return TRUE; } } pcmk__notice("Rejecting remote client: User '%s' is not a member of group " "'%s'", usr, grp); return FALSE; } static gboolean cib_remote_auth(xmlNode * login) { const char *user = NULL; const char *pass = NULL; const char *tmp = NULL; if (login == NULL) { return FALSE; } if (!pcmk__xe_is(login, PCMK__XE_CIB_COMMAND)) { pcmk__warn("Rejecting remote client: Unrecognizable message (element " "'%s' not '" PCMK__XE_CIB_COMMAND "')", login->name); - crm_log_xml_debug(login, "bad"); + pcmk__log_xml_debug(login, "bad"); return FALSE; } tmp = pcmk__xe_get(login, PCMK_XA_OP); if (!pcmk__str_eq(tmp, "authenticate", pcmk__str_casei)) { pcmk__warn("Rejecting remote client: Unrecognizable message (operation " "'%s' not 'authenticate')", tmp); - crm_log_xml_debug(login, "bad"); + pcmk__log_xml_debug(login, "bad"); return FALSE; } user = pcmk__xe_get(login, PCMK_XA_USER); pass = pcmk__xe_get(login, PCMK__XA_PASSWORD); if (!user || !pass) { pcmk__warn("Rejecting remote client: No %s given", ((user == NULL)? "username" : "password")); - crm_log_xml_debug(login, "bad"); + pcmk__log_xml_debug(login, "bad"); return FALSE; } - crm_log_xml_debug(login, "auth"); + pcmk__log_xml_debug(login, "auth"); return check_group_membership(user, CRM_DAEMON_GROUP) && authenticate_user(user, pass); } static gboolean remote_auth_timeout_cb(gpointer data) { pcmk__client_t *client = data; client->remote->auth_timeout = 0; if (pcmk__is_set(client->flags, pcmk__client_authenticated)) { return FALSE; } mainloop_del_fd(client->remote->source); pcmk__err("Remote client authentication timed out"); return FALSE; } static int cib_remote_listen(gpointer data) { int csock = -1; unsigned laddr; struct sockaddr_storage addr; char ipstr[INET6_ADDRSTRLEN]; int ssock = *(int *)data; int rc; pcmk__client_t *new_client = NULL; static struct mainloop_fd_callbacks remote_client_fd_callbacks = { .dispatch = cib_remote_msg, .destroy = cib_remote_connection_destroy, }; /* accept the connection */ laddr = sizeof(addr); memset(&addr, 0, sizeof(addr)); csock = accept(ssock, (struct sockaddr *)&addr, &laddr); if (csock == -1) { pcmk__warn("Could not accept remote connection: %s", pcmk_rc_str(errno)); return TRUE; } pcmk__sockaddr2str(&addr, ipstr); rc = pcmk__set_nonblocking(csock); if (rc != pcmk_rc_ok) { pcmk__warn("Dropping remote connection from %s because it could not be " "set to non-blocking: %s", ipstr, pcmk_rc_str(rc)); close(csock); return TRUE; } num_clients++; new_client = pcmk__new_unauth_client(NULL); new_client->remote = pcmk__assert_alloc(1, sizeof(pcmk__remote_t)); if (ssock == remote_tls_fd) { pcmk__set_client_flags(new_client, pcmk__client_tls); /* create gnutls session for the server socket */ new_client->remote->tls_session = pcmk__new_tls_session(tls, csock); if (new_client->remote->tls_session == NULL) { close(csock); return TRUE; } } else { pcmk__set_client_flags(new_client, pcmk__client_tcp); new_client->remote->tcp_socket = csock; } // Require the client to authenticate within this time new_client->remote->auth_timeout = pcmk__create_timer(REMOTE_AUTH_TIMEOUT, remote_auth_timeout_cb, new_client); pcmk__info("%s connection from %s pending authentication for client %s", ((ssock == remote_tls_fd)? "Encrypted" : "Clear-text"), ipstr, new_client->id); new_client->remote->source = mainloop_add_fd("cib-remote-client", G_PRIORITY_DEFAULT, csock, new_client, &remote_client_fd_callbacks); return TRUE; } void cib_remote_connection_destroy(gpointer user_data) { pcmk__client_t *client = user_data; int csock = -1; if (client == NULL) { return; } pcmk__trace("Cleaning up after client %s disconnect", pcmk__client_name(client)); num_clients--; pcmk__trace("Num unfree'd clients: %d", num_clients); switch (PCMK__CLIENT_TYPE(client)) { case pcmk__client_tcp: csock = client->remote->tcp_socket; break; case pcmk__client_tls: if (client->remote->tls_session) { csock = pcmk__tls_get_client_sock(client->remote); if (pcmk__is_set(client->flags, pcmk__client_tls_handshake_complete)) { gnutls_bye(client->remote->tls_session, GNUTLS_SHUT_WR); } gnutls_deinit(client->remote->tls_session); client->remote->tls_session = NULL; } break; default: pcmk__warn("Unknown transport for client %s " QB_XS " flags=%#016" PRIx64, pcmk__client_name(client), client->flags); } if (csock >= 0) { close(csock); } pcmk__free_client(client); pcmk__trace("Freed the cib client"); if (cib_shutdown_flag) { cib_shutdown(0); } return; } static void cib_handle_remote_msg(pcmk__client_t *client, xmlNode *command) { if (!pcmk__xe_is(command, PCMK__XE_CIB_COMMAND)) { crm_log_xml_trace(command, "bad"); return; } if (client->name == NULL) { client->name = pcmk__str_copy(client->id); } /* unset dangerous options */ pcmk__xe_remove_attr(command, PCMK__XA_SRC); pcmk__xe_remove_attr(command, PCMK__XA_CIB_HOST); pcmk__xe_remove_attr(command, PCMK__XA_CIB_UPDATE); pcmk__xe_set(command, PCMK__XA_T, PCMK__VALUE_CIB); pcmk__xe_set(command, PCMK__XA_CIB_CLIENTID, client->id); pcmk__xe_set(command, PCMK__XA_CIB_CLIENTNAME, client->name); pcmk__xe_set(command, PCMK__XA_CIB_USER, client->user); if (pcmk__xe_get(command, PCMK__XA_CIB_CALLID) == NULL) { char *call_uuid = pcmk__generate_uuid(); /* fix the command */ pcmk__xe_set(command, PCMK__XA_CIB_CALLID, call_uuid); free(call_uuid); } if (pcmk__xe_get(command, PCMK__XA_CIB_CALLOPT) == NULL) { pcmk__xe_set_int(command, PCMK__XA_CIB_CALLOPT, 0); } crm_log_xml_trace(command, "Remote command: "); cib_common_callback_worker(0, 0, command, client, TRUE); } static int cib_remote_msg(gpointer data) { xmlNode *command = NULL; pcmk__client_t *client = data; int rc; const char *client_name = pcmk__client_name(client); pcmk__trace("Remote %s message received for client %s", pcmk__client_type_str(PCMK__CLIENT_TYPE(client)), client_name); if ((PCMK__CLIENT_TYPE(client) == pcmk__client_tls) && !pcmk__is_set(client->flags, pcmk__client_tls_handshake_complete)) { 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; } pcmk__debug("Completed TLS handshake with remote client %s", client_name); pcmk__set_client_flags(client, pcmk__client_tls_handshake_complete); if (client->remote->auth_timeout) { g_source_remove(client->remote->auth_timeout); } /* 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); // Require the client to authenticate within this time client->remote->auth_timeout = pcmk__create_timer(REMOTE_AUTH_TIMEOUT, remote_auth_timeout_cb, client); return 0; } 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 */ pcmk__trace("Error reading from remote client: %s", pcmk_rc_str(rc)); return -1; } /* must pass auth before we will process anything else */ if (!pcmk__is_set(client->flags, pcmk__client_authenticated)) { xmlNode *reg; const char *user = NULL; command = pcmk__remote_message_xml(client->remote); if (cib_remote_auth(command) == FALSE) { pcmk__xml_free(command); return -1; } pcmk__set_client_flags(client, pcmk__client_authenticated); g_source_remove(client->remote->auth_timeout); client->remote->auth_timeout = 0; client->name = pcmk__xe_get_copy(command, PCMK_XA_NAME); user = pcmk__xe_get(command, PCMK_XA_USER); if (user) { client->user = pcmk__str_copy(user); } pcmk__notice("Remote connection accepted for authenticated user %s " QB_XS " client %s", pcmk__s(user, ""), client_name); /* send ACK */ reg = pcmk__xe_create(NULL, PCMK__XE_CIB_RESULT); pcmk__xe_set(reg, PCMK__XA_CIB_OP, CRM_OP_REGISTER); pcmk__xe_set(reg, PCMK__XA_CIB_CLIENTID, client->id); pcmk__remote_send_xml(client->remote, reg); pcmk__xml_free(reg); pcmk__xml_free(command); } command = pcmk__remote_message_xml(client->remote); if (command != NULL) { pcmk__trace("Remote message received from client %s", client_name); cib_handle_remote_msg(client, command); pcmk__xml_free(command); } return 0; } #ifdef HAVE_PAM /*! * \internal * \brief Pass remote user's password to PAM * * \param[in] num_msg Number of entries in \p msg * \param[in] msg Array of PAM messages * \param[out] response Where to set response to PAM * \param[in] data User data (the password string) * * \return PAM return code (PAM_BUF_ERR for memory errors, PAM_CONV_ERR for all * other errors, or PAM_SUCCESS on success) * \note See pam_conv(3) for more explanation */ static int construct_pam_passwd(int num_msg, const struct pam_message **msg, struct pam_response **response, void *data) { /* In theory, multiple messages are allowed, but due to OS compatibility * issues, PAM implementations are recommended to only send one message at a * time. We can require that here for simplicity. */ CRM_CHECK((num_msg == 1) && (msg != NULL) && (response != NULL) && (data != NULL), return PAM_CONV_ERR); switch (msg[0]->msg_style) { case PAM_PROMPT_ECHO_OFF: case PAM_PROMPT_ECHO_ON: // Password requested break; case PAM_TEXT_INFO: pcmk__info("PAM: %s", msg[0]->msg); data = NULL; break; case PAM_ERROR_MSG: /* In theory we should show msg[0]->msg, but that might * contain the password, which we don't want in the logs */ pcmk__err("PAM reported an error"); data = NULL; break; default: pcmk__warn("Ignoring PAM message of unrecognized type %d", msg[0]->msg_style); return PAM_CONV_ERR; } *response = calloc(1, sizeof(struct pam_response)); if (*response == NULL) { return PAM_BUF_ERR; } (*response)->resp_retcode = 0; (*response)->resp = pcmk__str_copy((const char *) data); // Caller will free return PAM_SUCCESS; } #endif /*! * \internal * \brief Verify the username and password passed for a remote CIB connection * * \param[in] user Username passed for remote CIB connection * \param[in] passwd Password passed for remote CIB connection * * \return \c true if the username and password are accepted, otherwise \c false * \note This function rejects all credentials when built without PAM support. */ static bool authenticate_user(const char *user, const char *passwd) { #ifdef HAVE_PAM int rc = 0; bool pass = false; const void *p_user = NULL; struct pam_conv p_conv; struct pam_handle *pam_h = NULL; static const char *pam_name = NULL; if (pam_name == NULL) { pam_name = getenv("CIB_pam_service"); if (pam_name == NULL) { pam_name = "login"; } } p_conv.conv = construct_pam_passwd; p_conv.appdata_ptr = (void *) passwd; rc = pam_start(pam_name, user, &p_conv, &pam_h); if (rc != PAM_SUCCESS) { pcmk__warn("Rejecting remote client for user %s because PAM " "initialization failed: %s", user, pam_strerror(pam_h, rc)); goto bail; } // Check user credentials rc = pam_authenticate(pam_h, PAM_SILENT); if (rc != PAM_SUCCESS) { pcmk__notice("Access for remote user %s denied: %s", user, pam_strerror(pam_h, rc)); goto bail; } /* Get the authenticated user name (PAM modules can map the original name to * something else). Since the CIB manager runs as the daemon user (not * root), that is the only user that can be successfully authenticated. */ rc = pam_get_item(pam_h, PAM_USER, &p_user); if (rc != PAM_SUCCESS) { pcmk__warn("Rejecting remote client for user %s because PAM failed to " "return final user name: %s", user, pam_strerror(pam_h, rc)); goto bail; } if (p_user == NULL) { pcmk__warn("Rejecting remote client for user %s because PAM returned " "no final user name", user); goto bail; } // @TODO Why do we require these to match? if (!pcmk__str_eq(p_user, user, pcmk__str_none)) { pcmk__warn("Rejecting remote client for user %s because PAM returned " "different final user name %s", user, p_user); goto bail; } // Check user account restrictions (expiration, etc.) rc = pam_acct_mgmt(pam_h, PAM_SILENT); if (rc != PAM_SUCCESS) { pcmk__notice("Access for remote user %s denied: %s", user, pam_strerror(pam_h, rc)); goto bail; } pass = true; bail: pam_end(pam_h, rc); return pass; #else // @TODO Implement for non-PAM environments pcmk__warn("Rejecting remote user %s because this build does not have PAM " "support", user); return false; #endif } diff --git a/daemons/controld/controld_fsa.c b/daemons/controld/controld_fsa.c index c6673155e3..e95b96961e 100644 --- a/daemons/controld/controld_fsa.c +++ b/daemons/controld/controld_fsa.c @@ -1,686 +1,686 @@ /* * 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 // PRIx64 #include #include #include // uint64_t #include #include #include #include #include #include #include #include #include //! Triggers an FSA invocation static crm_trigger_t *fsa_trigger = NULL; static void do_state_transition(enum crmd_fsa_state cur_state, enum crmd_fsa_state next_state, fsa_data_t *msg_data); void s_crmd_fsa_actions(fsa_data_t * fsa_data); void log_fsa_input(fsa_data_t * stored_msg); static void do_fsa_action(fsa_data_t * fsa_data, long long an_action, void (*function) (long long action, enum crmd_fsa_cause cause, enum crmd_fsa_state cur_state, enum crmd_fsa_input cur_input, fsa_data_t * msg_data)) { controld_clear_fsa_action_flags(an_action); function(an_action, fsa_data->fsa_cause, controld_globals.fsa_state, fsa_data->fsa_input, fsa_data); } static const uint64_t startup_actions = A_STARTUP | A_CIB_START | A_LRM_CONNECT | A_HA_CONNECT | A_READCONFIG | A_STARTED | A_CL_JOIN_QUERY; // A_LOG, A_WARN, A_ERROR void do_log(long long action, enum crmd_fsa_cause cause, enum crmd_fsa_state cur_state, enum crmd_fsa_input current_input, fsa_data_t *msg_data) { unsigned log_type = PCMK__LOG_TRACE; if (action & A_LOG) { log_type = LOG_INFO; } else if (action & A_WARN) { log_type = LOG_WARNING; } else if (action & A_ERROR) { log_type = LOG_ERR; } do_crm_log(log_type, "Input %s received in state %s from %s", fsa_input2string(msg_data->fsa_input), fsa_state2string(cur_state), msg_data->origin); if (msg_data->data_type == fsa_dt_ha_msg) { ha_msg_input_t *input = fsa_typed_data(msg_data->data_type); - crm_log_xml_debug(input->msg, __func__); + pcmk__log_xml_debug(input->msg, __func__); } else if (msg_data->data_type == fsa_dt_xml) { xmlNode *input = fsa_typed_data(msg_data->data_type); - crm_log_xml_debug(input, __func__); + pcmk__log_xml_debug(input, __func__); } else if (msg_data->data_type == fsa_dt_lrm) { lrmd_event_data_t *input = fsa_typed_data(msg_data->data_type); do_crm_log(log_type, "Resource %s: Call ID %d returned %d (%d)." " New status if rc=0: %s", input->rsc_id, input->call_id, input->rc, input->op_status, (char *)input->user_data); } } /*! * \internal * \brief Initialize the FSA trigger */ void controld_init_fsa_trigger(void) { fsa_trigger = mainloop_add_trigger(G_PRIORITY_HIGH, crm_fsa_trigger, NULL); } /*! * \internal * \brief Destroy the FSA trigger */ void controld_destroy_fsa_trigger(void) { // This basically will not work, since mainloop has a reference to it mainloop_destroy_trigger(fsa_trigger); fsa_trigger = NULL; } /*! * \internal * \brief Trigger an FSA invocation * * \param[in] fn Calling function name * \param[in] line Line number where call occurred */ void controld_trigger_fsa_as(const char *fn, int line) { if (fsa_trigger != NULL) { pcmk__trace("%s:%d - Triggered FSA invocation", fn, line); mainloop_set_trigger(fsa_trigger); } } enum crmd_fsa_state s_crmd_fsa(enum crmd_fsa_cause cause) { controld_globals_t *globals = &controld_globals; fsa_data_t *fsa_data = NULL; uint64_t register_copy = controld_globals.fsa_input_register; uint64_t new_actions = A_NOTHING; enum crmd_fsa_state last_state; pcmk__trace("FSA invoked with Cause: %s\tState: %s", fsa_cause2string(cause), fsa_state2string(globals->fsa_state)); fsa_dump_actions(controld_globals.fsa_actions, "Initial"); controld_clear_global_flags(controld_fsa_is_stalled); if ((controld_globals.fsa_message_queue == NULL) && (controld_globals.fsa_actions != A_NOTHING)) { /* fake the first message so we can get into the loop */ fsa_data = pcmk__assert_alloc(1, sizeof(fsa_data_t)); fsa_data->fsa_input = I_NULL; fsa_data->fsa_cause = C_FSA_INTERNAL; fsa_data->origin = __func__; fsa_data->data_type = fsa_dt_none; controld_globals.fsa_message_queue = g_list_append(controld_globals.fsa_message_queue, fsa_data); } while ((controld_globals.fsa_message_queue != NULL) && !pcmk__is_set(controld_globals.flags, controld_fsa_is_stalled)) { pcmk__trace("Checking messages (%u remaining)", g_list_length(controld_globals.fsa_message_queue)); fsa_data = get_message(); if(fsa_data == NULL) { continue; } log_fsa_input(fsa_data); /* add any actions back to the queue */ controld_set_fsa_action_flags(fsa_data->actions); fsa_dump_actions(fsa_data->actions, "Restored actions"); /* get the next batch of actions */ new_actions = controld_fsa_get_action(fsa_data->fsa_input); controld_set_fsa_action_flags(new_actions); fsa_dump_actions(new_actions, "New actions"); if (fsa_data->fsa_input != I_NULL && fsa_data->fsa_input != I_ROUTER) { pcmk__debug("Processing %s: [ state=%s cause=%s origin=%s ]", fsa_input2string(fsa_data->fsa_input), fsa_state2string(globals->fsa_state), fsa_cause2string(fsa_data->fsa_cause), fsa_data->origin); } /* logging : *before* the state is changed */ if (pcmk__is_set(controld_globals.fsa_actions, A_ERROR)) { do_fsa_action(fsa_data, A_ERROR, do_log); } if (pcmk__is_set(controld_globals.fsa_actions, A_WARN)) { do_fsa_action(fsa_data, A_WARN, do_log); } if (pcmk__is_set(controld_globals.fsa_actions, A_LOG)) { do_fsa_action(fsa_data, A_LOG, do_log); } /* update state variables */ last_state = globals->fsa_state; globals->fsa_state = controld_fsa_get_next_state(fsa_data->fsa_input); /* * Remove certain actions during shutdown */ if ((globals->fsa_state == S_STOPPING) || pcmk__is_set(controld_globals.fsa_input_register, R_SHUTDOWN)) { controld_clear_fsa_action_flags(startup_actions); } /* * Hook for change of state. * Allows actions to be added or removed when entering a state */ if (last_state != globals->fsa_state) { do_state_transition(last_state, globals->fsa_state, fsa_data); } /* start doing things... */ s_crmd_fsa_actions(fsa_data); delete_fsa_input(fsa_data); } if ((controld_globals.fsa_message_queue != NULL) || (controld_globals.fsa_actions != A_NOTHING) || pcmk__is_set(controld_globals.flags, controld_fsa_is_stalled)) { pcmk__debug("Exiting the FSA: queue=%d, fsa_actions=%" PRIx64 ", stalled=%s", g_list_length(controld_globals.fsa_message_queue), controld_globals.fsa_actions, pcmk__flag_text(controld_globals.flags, controld_fsa_is_stalled)); } else { pcmk__trace("Exiting the FSA"); } /* cleanup inputs? */ if (register_copy != controld_globals.fsa_input_register) { uint64_t same = register_copy & controld_globals.fsa_input_register; fsa_dump_inputs(LOG_DEBUG, "Added", controld_globals.fsa_input_register ^ same); fsa_dump_inputs(LOG_DEBUG, "Removed", register_copy ^ same); } fsa_dump_actions(controld_globals.fsa_actions, "Remaining"); fsa_dump_queue(LOG_DEBUG); return globals->fsa_state; } void s_crmd_fsa_actions(fsa_data_t * fsa_data) { /* * Process actions in order of priority but do only one * action at a time to avoid complicating the ordering. */ CRM_CHECK(fsa_data != NULL, return); while ((controld_globals.fsa_actions != A_NOTHING) && !pcmk__is_set(controld_globals.flags, controld_fsa_is_stalled)) { /* regular action processing in order of action priority * * Make sure all actions that connect to required systems * are performed first */ if (pcmk__is_set(controld_globals.fsa_actions, A_ERROR)) { do_fsa_action(fsa_data, A_ERROR, do_log); } else if (pcmk__is_set(controld_globals.fsa_actions, A_WARN)) { do_fsa_action(fsa_data, A_WARN, do_log); } else if (pcmk__is_set(controld_globals.fsa_actions, A_LOG)) { do_fsa_action(fsa_data, A_LOG, do_log); /* get out of here NOW! before anything worse happens */ } else if (pcmk__is_set(controld_globals.fsa_actions, A_EXIT_1)) { do_fsa_action(fsa_data, A_EXIT_1, do_exit); /* sub-system restart */ } else if (pcmk__all_flags_set(controld_globals.fsa_actions, O_LRM_RECONNECT)) { do_fsa_action(fsa_data, O_LRM_RECONNECT, do_lrm_control); } else if (pcmk__all_flags_set(controld_globals.fsa_actions, O_CIB_RESTART)) { do_fsa_action(fsa_data, O_CIB_RESTART, do_cib_control); } else if (pcmk__all_flags_set(controld_globals.fsa_actions, O_PE_RESTART)) { do_fsa_action(fsa_data, O_PE_RESTART, do_pe_control); } else if (pcmk__all_flags_set(controld_globals.fsa_actions, O_TE_RESTART)) { do_fsa_action(fsa_data, O_TE_RESTART, do_te_control); /* essential start tasks */ } else if (pcmk__is_set(controld_globals.fsa_actions, A_STARTUP)) { do_fsa_action(fsa_data, A_STARTUP, do_startup); } else if (pcmk__is_set(controld_globals.fsa_actions, A_CIB_START)) { do_fsa_action(fsa_data, A_CIB_START, do_cib_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_HA_CONNECT)) { do_fsa_action(fsa_data, A_HA_CONNECT, do_ha_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_READCONFIG)) { do_fsa_action(fsa_data, A_READCONFIG, do_read_config); /* sub-system start/connect */ } else if (pcmk__is_set(controld_globals.fsa_actions, A_LRM_CONNECT)) { do_fsa_action(fsa_data, A_LRM_CONNECT, do_lrm_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_TE_START)) { do_fsa_action(fsa_data, A_TE_START, do_te_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_PE_START)) { do_fsa_action(fsa_data, A_PE_START, do_pe_control); /* Timers */ } else if (pcmk__is_set(controld_globals.fsa_actions, A_DC_TIMER_STOP)) { do_fsa_action(fsa_data, A_DC_TIMER_STOP, do_timer_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_INTEGRATE_TIMER_STOP)) { do_fsa_action(fsa_data, A_INTEGRATE_TIMER_STOP, do_timer_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_INTEGRATE_TIMER_START)) { do_fsa_action(fsa_data, A_INTEGRATE_TIMER_START, do_timer_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_FINALIZE_TIMER_STOP)) { do_fsa_action(fsa_data, A_FINALIZE_TIMER_STOP, do_timer_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_FINALIZE_TIMER_START)) { do_fsa_action(fsa_data, A_FINALIZE_TIMER_START, do_timer_control); /* * Highest priority actions */ } else if (pcmk__is_set(controld_globals.fsa_actions, A_MSG_ROUTE)) { do_fsa_action(fsa_data, A_MSG_ROUTE, do_msg_route); } else if (pcmk__is_set(controld_globals.fsa_actions, A_RECOVER)) { do_fsa_action(fsa_data, A_RECOVER, do_recover); } else if (pcmk__is_set(controld_globals.fsa_actions, A_CL_JOIN_RESULT)) { do_fsa_action(fsa_data, A_CL_JOIN_RESULT, do_cl_join_finalize_respond); } else if (pcmk__is_set(controld_globals.fsa_actions, A_CL_JOIN_REQUEST)) { do_fsa_action(fsa_data, A_CL_JOIN_REQUEST, do_cl_join_offer_respond); } else if (pcmk__is_set(controld_globals.fsa_actions, A_SHUTDOWN_REQ)) { do_fsa_action(fsa_data, A_SHUTDOWN_REQ, do_shutdown_req); } else if (pcmk__is_set(controld_globals.fsa_actions, A_ELECTION_VOTE)) { do_fsa_action(fsa_data, A_ELECTION_VOTE, do_election_vote); } else if (pcmk__is_set(controld_globals.fsa_actions, A_ELECTION_COUNT)) { do_fsa_action(fsa_data, A_ELECTION_COUNT, do_election_count_vote); /* * High priority actions */ } else if (pcmk__is_set(controld_globals.fsa_actions, A_STARTED)) { do_fsa_action(fsa_data, A_STARTED, do_started); } else if (pcmk__is_set(controld_globals.fsa_actions, A_CL_JOIN_QUERY)) { do_fsa_action(fsa_data, A_CL_JOIN_QUERY, do_cl_join_query); } else if (pcmk__is_set(controld_globals.fsa_actions, A_DC_TIMER_START)) { do_fsa_action(fsa_data, A_DC_TIMER_START, do_timer_control); /* * Medium priority actions * - Membership */ } else if (pcmk__is_set(controld_globals.fsa_actions, A_DC_TAKEOVER)) { do_fsa_action(fsa_data, A_DC_TAKEOVER, do_dc_takeover); } else if (pcmk__is_set(controld_globals.fsa_actions, A_DC_RELEASE)) { do_fsa_action(fsa_data, A_DC_RELEASE, do_dc_release); } else if (pcmk__is_set(controld_globals.fsa_actions, A_DC_JOIN_FINAL)) { do_fsa_action(fsa_data, A_DC_JOIN_FINAL, do_dc_join_final); } else if (pcmk__is_set(controld_globals.fsa_actions, A_ELECTION_CHECK)) { do_fsa_action(fsa_data, A_ELECTION_CHECK, do_election_check); } else if (pcmk__is_set(controld_globals.fsa_actions, A_ELECTION_START)) { do_fsa_action(fsa_data, A_ELECTION_START, do_election_vote); } else if (pcmk__is_set(controld_globals.fsa_actions, A_DC_JOIN_OFFER_ALL)) { do_fsa_action(fsa_data, A_DC_JOIN_OFFER_ALL, do_dc_join_offer_all); } else if (pcmk__is_set(controld_globals.fsa_actions, A_DC_JOIN_OFFER_ONE)) { do_fsa_action(fsa_data, A_DC_JOIN_OFFER_ONE, do_dc_join_offer_one); } else if (pcmk__is_set(controld_globals.fsa_actions, A_DC_JOIN_PROCESS_REQ)) { do_fsa_action(fsa_data, A_DC_JOIN_PROCESS_REQ, do_dc_join_filter_offer); } else if (pcmk__is_set(controld_globals.fsa_actions, A_DC_JOIN_PROCESS_ACK)) { do_fsa_action(fsa_data, A_DC_JOIN_PROCESS_ACK, do_dc_join_ack); } else if (pcmk__is_set(controld_globals.fsa_actions, A_DC_JOIN_FINALIZE)) { do_fsa_action(fsa_data, A_DC_JOIN_FINALIZE, do_dc_join_finalize); } else if (pcmk__is_set(controld_globals.fsa_actions, A_CL_JOIN_ANNOUNCE)) { do_fsa_action(fsa_data, A_CL_JOIN_ANNOUNCE, do_cl_join_announce); /* * Low(er) priority actions * Make sure the CIB is always updated before invoking the * scheduler, and the scheduler before the transition engine. */ } else if (pcmk__is_set(controld_globals.fsa_actions, A_TE_HALT)) { do_fsa_action(fsa_data, A_TE_HALT, do_te_invoke); } else if (pcmk__is_set(controld_globals.fsa_actions, A_TE_CANCEL)) { do_fsa_action(fsa_data, A_TE_CANCEL, do_te_invoke); } else if (pcmk__is_set(controld_globals.fsa_actions, A_LRM_INVOKE)) { do_fsa_action(fsa_data, A_LRM_INVOKE, do_lrm_invoke); } else if (pcmk__is_set(controld_globals.fsa_actions, A_PE_INVOKE)) { do_fsa_action(fsa_data, A_PE_INVOKE, do_pe_invoke); } else if (pcmk__is_set(controld_globals.fsa_actions, A_TE_INVOKE)) { do_fsa_action(fsa_data, A_TE_INVOKE, do_te_invoke); /* Shutdown actions */ } else if (pcmk__is_set(controld_globals.fsa_actions, A_DC_RELEASED)) { do_fsa_action(fsa_data, A_DC_RELEASED, do_dc_release); } else if (pcmk__is_set(controld_globals.fsa_actions, A_PE_STOP)) { do_fsa_action(fsa_data, A_PE_STOP, do_pe_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_TE_STOP)) { do_fsa_action(fsa_data, A_TE_STOP, do_te_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_SHUTDOWN)) { do_fsa_action(fsa_data, A_SHUTDOWN, do_shutdown); } else if (pcmk__is_set(controld_globals.fsa_actions, A_LRM_DISCONNECT)) { do_fsa_action(fsa_data, A_LRM_DISCONNECT, do_lrm_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_HA_DISCONNECT)) { do_fsa_action(fsa_data, A_HA_DISCONNECT, do_ha_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_CIB_STOP)) { do_fsa_action(fsa_data, A_CIB_STOP, do_cib_control); } else if (pcmk__is_set(controld_globals.fsa_actions, A_STOP)) { do_fsa_action(fsa_data, A_STOP, do_stop); /* exit gracefully */ } else if (pcmk__is_set(controld_globals.fsa_actions, A_EXIT_0)) { do_fsa_action(fsa_data, A_EXIT_0, do_exit); /* Error checking and reporting */ } else { pcmk__err("Action %s not supported " QB_XS " %" PRIx64, fsa_action2string(controld_globals.fsa_actions), controld_globals.fsa_actions); register_fsa_error_adv(C_FSA_INTERNAL, I_ERROR, fsa_data, NULL, __func__); } } } void log_fsa_input(fsa_data_t * stored_msg) { pcmk__assert(stored_msg != NULL); pcmk__trace("Processing queued input %d", stored_msg->id); if (stored_msg->fsa_cause == C_LRM_OP_CALLBACK) { pcmk__trace("FSA processing LRM callback from %s", stored_msg->origin); } else if (stored_msg->data == NULL) { pcmk__trace("FSA processing input from %s", stored_msg->origin); } else { ha_msg_input_t *ha_input = fsa_typed_data_adv(stored_msg, fsa_dt_ha_msg, __func__); pcmk__trace("FSA processing XML message from %s", stored_msg->origin); crm_log_xml_trace(ha_input->xml, "FSA message data"); } } static void check_join_counts(fsa_data_t *msg_data) { int count; guint npeers; count = crmd_join_phase_count(controld_join_finalized); if (count > 0) { pcmk__err("%d cluster node%s failed to confirm join", count, pcmk__plural_s(count)); crmd_join_phase_log(LOG_NOTICE); return; } npeers = pcmk__cluster_num_active_nodes(); count = crmd_join_phase_count(controld_join_confirmed); if (count == npeers) { if (npeers == 1) { pcmk__debug("Sole active cluster node is fully joined"); } else { pcmk__debug("All %d active cluster nodes are fully joined", count); } } else if (count > npeers) { pcmk__err("New election needed because more nodes confirmed join " "than are in membership (%d > %u)", count, npeers); register_fsa_input(C_FSA_INTERNAL, I_ELECTION, NULL); } else if (controld_globals.membership_id != controld_globals.peer_seq) { pcmk__info("New join needed because membership changed (%llu -> %llu)", controld_globals.membership_id, controld_globals.peer_seq); register_fsa_input_before(C_FSA_INTERNAL, I_NODE_JOIN, NULL); } else { pcmk__warn("Only %d of %u active cluster nodes fully joined (%d did " "not respond to offer)", count, npeers, crmd_join_phase_count(controld_join_welcomed)); } } static void do_state_transition(enum crmd_fsa_state cur_state, enum crmd_fsa_state next_state, fsa_data_t *msg_data) { int level = LOG_INFO; int count = 0; gboolean clear_recovery_bit = TRUE; #if 0 uint64_t original_fsa_actions = controld_globals.fsa_actions; #endif enum crmd_fsa_cause cause = msg_data->fsa_cause; enum crmd_fsa_input current_input = msg_data->fsa_input; const char *state_from = fsa_state2string(cur_state); const char *state_to = fsa_state2string(next_state); const char *input = fsa_input2string(current_input); CRM_LOG_ASSERT(cur_state != next_state); if (cur_state == S_IDLE || next_state == S_IDLE) { level = LOG_NOTICE; } else if (cur_state == S_NOT_DC || next_state == S_NOT_DC) { level = LOG_NOTICE; } else if (cur_state == S_ELECTION) { level = LOG_NOTICE; } else if (cur_state == S_STARTING) { level = LOG_NOTICE; } else if (next_state == S_RECOVERY) { level = LOG_WARNING; } do_crm_log(level, "State transition %s -> %s " QB_XS " input=%s cause=%s origin=%s", state_from, state_to, input, fsa_cause2string(cause), msg_data->origin); if (next_state != S_ELECTION && cur_state != S_RELEASE_DC) { controld_stop_current_election_timeout(); } if (next_state == S_INTEGRATION) { controld_set_fsa_action_flags(A_INTEGRATE_TIMER_START); } else { controld_set_fsa_action_flags(A_INTEGRATE_TIMER_STOP); } if (next_state == S_FINALIZE_JOIN) { controld_set_fsa_action_flags(A_FINALIZE_TIMER_START); } else { controld_set_fsa_action_flags(A_FINALIZE_TIMER_STOP); } if (next_state != S_PENDING) { controld_set_fsa_action_flags(A_DC_TIMER_STOP); } if (next_state != S_IDLE) { controld_stop_recheck_timer(); } if (cur_state == S_FINALIZE_JOIN && next_state == S_POLICY_ENGINE) { populate_cib_nodes(node_update_quick|node_update_all, __func__); } switch (next_state) { case S_PENDING: { cib_t *cib_conn = controld_globals.cib_conn; cib_conn->cmds->set_secondary(cib_conn, cib_none); } update_dc(NULL); break; case S_ELECTION: update_dc(NULL); break; case S_NOT_DC: controld_reset_counter_election_timer(); purge_stonith_cleanup(); if (pcmk__is_set(controld_globals.fsa_input_register, R_SHUTDOWN)) { pcmk__info("(Re)Issuing shutdown request now" " that we have a " "new DC"); controld_set_fsa_action_flags(A_SHUTDOWN_REQ); } CRM_LOG_ASSERT(controld_globals.dc_name != NULL); if (controld_globals.dc_name == NULL) { pcmk__err("Reached S_NOT_DC without a DC" " being recorded"); } break; case S_RECOVERY: clear_recovery_bit = FALSE; break; case S_FINALIZE_JOIN: CRM_LOG_ASSERT(AM_I_DC); if (cause == C_TIMER_POPPED) { pcmk__warn("Progressed to state %s after %s", fsa_state2string(next_state), fsa_cause2string(cause)); } count = crmd_join_phase_count(controld_join_welcomed); if (count > 0) { pcmk__warn("%d cluster node%s failed to respond to join offer", count, pcmk__plural_s(count)); crmd_join_phase_log(LOG_NOTICE); } else { pcmk__debug("All cluster nodes (%d) responded to join offer", crmd_join_phase_count(controld_join_integrated)); } break; case S_POLICY_ENGINE: controld_reset_counter_election_timer(); CRM_LOG_ASSERT(AM_I_DC); if (cause == C_TIMER_POPPED) { pcmk__info("Progressed to state %s after %s", fsa_state2string(next_state), fsa_cause2string(cause)); } check_join_counts(msg_data); break; case S_STOPPING: case S_TERMINATE: /* possibly redundant */ controld_set_fsa_input_flags(R_SHUTDOWN); break; case S_IDLE: CRM_LOG_ASSERT(AM_I_DC); if (pcmk__is_set(controld_globals.fsa_input_register, R_SHUTDOWN)) { pcmk__info("(Re)Issuing shutdown request now" " that we are " "the DC"); controld_set_fsa_action_flags(A_SHUTDOWN_REQ); } controld_start_recheck_timer(); break; default: break; } if (clear_recovery_bit && next_state != S_PENDING) { controld_clear_fsa_action_flags(A_RECOVER); } else if (clear_recovery_bit == FALSE) { controld_set_fsa_action_flags(A_RECOVER); } #if 0 if (original_fsa_actions != controld_globals.fsa_actions) { fsa_dump_actions(original_fsa_actions ^ controld_globals.fsa_actions, "New actions"); } #endif } diff --git a/daemons/controld/controld_join_dc.c b/daemons/controld/controld_join_dc.c index a732e6ce36..a7a2351931 100644 --- a/daemons/controld/controld_join_dc.c +++ b/daemons/controld/controld_join_dc.c @@ -1,1108 +1,1108 @@ /* * 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 // PRIu32 #include // bool, true, false #include // NULL #include // free(), etc. #include // gboolean, etc. #include // xmlNode #include #include // PCMK_SCORE_INFINITY #include #include #include static char *max_generation_from = NULL; static xmlNodePtr max_generation_xml = NULL; /*! * \internal * \brief Nodes from which a CIB sync has failed since the peer joined * * This table is of the form (node_name -> join_id). \p node_name is * the name of a client node from which a CIB \p sync_from() call has failed in * \p do_dc_join_finalize() since the client joined the cluster as a peer. * \p join_id is the ID of the join round in which the \p sync_from() failed, * and is intended for use in nack log messages. */ static GHashTable *failed_sync_nodes = NULL; void finalize_join_for(gpointer key, gpointer value, gpointer user_data); void finalize_sync_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data); gboolean check_join_state(enum crmd_fsa_state cur_state, const char *source); /* Numeric counter used to identify join rounds (an unsigned int would be * appropriate, except we get and set it in XML as int) */ static int current_join_id = 0; /*! * \internal * \brief Get log-friendly string equivalent of a controller group join phase * * \param[in] phase Join phase * * \return Log-friendly string equivalent of \p phase */ static const char * join_phase_text(enum controld_join_phase phase) { switch (phase) { case controld_join_nack: return "nack"; case controld_join_none: return "none"; case controld_join_welcomed: return "welcomed"; case controld_join_integrated: return "integrated"; case controld_join_finalized: return "finalized"; case controld_join_confirmed: return "confirmed"; default: return "invalid"; } } /*! * \internal * \brief Destroy the hash table containing failed sync nodes */ void controld_destroy_failed_sync_table(void) { if (failed_sync_nodes != NULL) { g_hash_table_destroy(failed_sync_nodes); failed_sync_nodes = NULL; } } /*! * \internal * \brief Remove a node from the failed sync nodes table if present * * \param[in] node_name Node name to remove */ void controld_remove_failed_sync_node(const char *node_name) { if (failed_sync_nodes != NULL) { g_hash_table_remove(failed_sync_nodes, (gchar *) node_name); } } /*! * \internal * \brief Add to a hash table a node whose CIB failed to sync * * \param[in] node_name Name of node whose CIB failed to sync * \param[in] join_id Join round when the failure occurred */ static void record_failed_sync_node(const char *node_name, gint join_id) { if (failed_sync_nodes == NULL) { failed_sync_nodes = pcmk__strikey_table(g_free, NULL); } /* If the node is already in the table then we failed to nack it during the * filter offer step */ CRM_LOG_ASSERT(g_hash_table_insert(failed_sync_nodes, g_strdup(node_name), GINT_TO_POINTER(join_id))); } /*! * \internal * \brief Look up a node name in the failed sync table * * \param[in] node_name Name of node to look up * \param[out] join_id Where to store the join ID of when the sync failed * * \return Standard Pacemaker return code. Specifically, \p pcmk_rc_ok if the * node name was found, or \p pcmk_rc_node_unknown otherwise. * \note \p *join_id is set to -1 if the node is not found. */ static int lookup_failed_sync_node(const char *node_name, gint *join_id) { *join_id = -1; if (failed_sync_nodes != NULL) { gpointer result = g_hash_table_lookup(failed_sync_nodes, (gchar *) node_name); if (result != NULL) { *join_id = GPOINTER_TO_INT(result); return pcmk_rc_ok; } } return pcmk_rc_node_unknown; } void crm_update_peer_join(const char *source, pcmk__node_status_t *node, enum controld_join_phase phase) { enum controld_join_phase last = controld_get_join_phase(node); CRM_CHECK(node != NULL, return); /* Remote nodes do not participate in joins */ if (pcmk__is_set(node->flags, pcmk__node_status_remote)) { return; } if (phase == last) { pcmk__trace("Node %s join-%d phase is still %s " QB_XS " nodeid=%" PRIu32 " source=%s", node->name, current_join_id, join_phase_text(last), node->cluster_layer_id, source); return; } if ((phase <= controld_join_none) || (phase == (last + 1))) { struct controld_node_status_data *data = NULL; if (node->user_data == NULL) { node->user_data = pcmk__assert_alloc(1, sizeof(struct controld_node_status_data)); } data = node->user_data; data->join_phase = phase; pcmk__trace("Node %s join-%d phase is now %s (was %s) " QB_XS " nodeid=%" PRIu32 " source=%s", node->name, current_join_id, join_phase_text(phase), join_phase_text(last), node->cluster_layer_id, source); return; } pcmk__warn("Rejecting join-%d phase update for node %s because can't go " "from %s to %s " QB_XS " nodeid=%" PRIu32 " source=%s", current_join_id, node->name, join_phase_text(last), join_phase_text(phase), node->cluster_layer_id, source); } static void start_join_round(void) { GHashTableIter iter; pcmk__node_status_t *peer = NULL; pcmk__debug("Starting new join round join-%d", current_join_id); g_hash_table_iter_init(&iter, pcmk__peer_cache); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &peer)) { crm_update_peer_join(__func__, peer, controld_join_none); } if (max_generation_from != NULL) { free(max_generation_from); max_generation_from = NULL; } if (max_generation_xml != NULL) { pcmk__xml_free(max_generation_xml); max_generation_xml = NULL; } controld_clear_fsa_input_flags(R_HAVE_CIB); } /*! * \internal * \brief Create a join message from the DC * * \param[in] join_op Join operation name * \param[in] host_to Recipient of message */ static xmlNode * create_dc_message(const char *join_op, const char *host_to) { xmlNode *msg = pcmk__new_request(pcmk_ipc_controld, CRM_SYSTEM_DC, host_to, CRM_SYSTEM_CRMD, join_op, NULL); /* Identify which election this is a part of */ pcmk__xe_set_int(msg, PCMK__XA_JOIN_ID, current_join_id); /* Add a field specifying whether the DC is shutting down. This keeps the * joining node from fencing the old DC if it becomes the new DC. */ pcmk__xe_set_bool_attr(msg, PCMK__XA_DC_LEAVING, pcmk__is_set(controld_globals.fsa_input_register, R_SHUTDOWN)); return msg; } static void join_make_offer(gpointer key, gpointer value, gpointer user_data) { /* @TODO We don't use user_data except to distinguish one particular call * from others. Make this clearer. */ xmlNode *offer = NULL; pcmk__node_status_t *member = (pcmk__node_status_t *) value; pcmk__assert(member != NULL); if (!pcmk__cluster_is_node_active(member)) { pcmk__info("Not making join-%d offer to inactive node %s", current_join_id, pcmk__s(member->name, "with unknown name")); if ((member->expected == NULL) && pcmk__str_eq(member->state, PCMK__VALUE_LOST, pcmk__str_none)) { /* You would think this unsafe, but in fact this plus an * active resource is what causes it to be fenced. * * Yes, this does mean that any node that dies at the same * time as the old DC and is not running resource (still) * won't be fenced. * * I'm not happy about this either. */ pcmk__update_peer_expected(__func__, member, CRMD_JOINSTATE_DOWN); } return; } if (member->name == NULL) { pcmk__info("Not making join-%d offer to node uuid %s with unknown name", current_join_id, member->xml_id); return; } if (controld_globals.membership_id != controld_globals.peer_seq) { controld_globals.membership_id = controld_globals.peer_seq; pcmk__info("Making join-%d offers based on membership event %llu", current_join_id, controld_globals.peer_seq); } if (user_data != NULL) { enum controld_join_phase phase = controld_get_join_phase(member); if (phase > controld_join_none) { pcmk__info("Not making join-%d offer to already known node %s (%s)", current_join_id, member->name, join_phase_text(phase)); return; } } crm_update_peer_join(__func__, (pcmk__node_status_t*) member, controld_join_none); offer = create_dc_message(CRM_OP_JOIN_OFFER, member->name); // Advertise our feature set so the joining node can bail if not compatible pcmk__xe_set(offer, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET); pcmk__info("Sending join-%d offer to %s", current_join_id, member->name); pcmk__cluster_send_message(member, pcmk_ipc_controld, offer); pcmk__xml_free(offer); crm_update_peer_join(__func__, member, controld_join_welcomed); } /* A_DC_JOIN_OFFER_ALL */ void do_dc_join_offer_all(long long action, enum crmd_fsa_cause cause, enum crmd_fsa_state cur_state, enum crmd_fsa_input current_input, fsa_data_t * msg_data) { int count; /* Reset everyone's status back to down or in_ccm in the CIB. * Any nodes that are active in the CIB but not in the cluster membership * will be seen as offline by the scheduler anyway. */ current_join_id++; start_join_round(); update_dc(NULL); if (cause == C_HA_MESSAGE && current_input == I_NODE_JOIN) { pcmk__info("A new node joined the cluster"); } g_hash_table_foreach(pcmk__peer_cache, join_make_offer, NULL); count = crmd_join_phase_count(controld_join_welcomed); pcmk__info("Waiting on join-%d requests from %d outstanding node%s", current_join_id, count, pcmk__plural_s(count)); // Don't waste time by invoking the scheduler yet } /* A_DC_JOIN_OFFER_ONE */ void do_dc_join_offer_one(long long action, enum crmd_fsa_cause cause, enum crmd_fsa_state cur_state, enum crmd_fsa_input current_input, fsa_data_t * msg_data) { pcmk__node_status_t *member = NULL; ha_msg_input_t *welcome = NULL; int count; const char *join_to = NULL; if (msg_data->data == NULL) { pcmk__info("Making join-%d offers to any unconfirmed nodes because an " "unknown node joined", current_join_id); g_hash_table_foreach(pcmk__peer_cache, join_make_offer, &member); check_join_state(cur_state, __func__); return; } welcome = fsa_typed_data(fsa_dt_ha_msg); if (welcome == NULL) { // fsa_typed_data() already logged an error return; } join_to = pcmk__xe_get(welcome->msg, PCMK__XA_SRC); if (join_to == NULL) { pcmk__err("Can't make join-%d offer to unknown node", current_join_id); return; } member = pcmk__get_node(0, join_to, NULL, pcmk__node_search_cluster_member); /* It is possible that a node will have been sick or starting up when the * original offer was made. However, it will either re-announce itself in * due course, or we can re-store the original offer on the client. */ crm_update_peer_join(__func__, member, controld_join_none); join_make_offer(NULL, member, NULL); /* If the offer isn't to the local node, make an offer to the local node as * well, to ensure the correct value for max_generation_from. */ if (!controld_is_local_node(join_to)) { member = controld_get_local_node_status(); join_make_offer(NULL, member, NULL); } /* This was a genuine join request; cancel any existing transition and * invoke the scheduler. */ abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, "Node join", NULL); count = crmd_join_phase_count(controld_join_welcomed); pcmk__info("Waiting on join-%d requests from %d outstanding node%s", current_join_id, count, pcmk__plural_s(count)); // Don't waste time by invoking the scheduler yet } static int compare_int_fields(xmlNode * left, xmlNode * right, const char *field) { const char *elem_l = pcmk__xe_get(left, field); const char *elem_r = pcmk__xe_get(right, field); long long int_elem_l; long long int_elem_r; int rc = pcmk_rc_ok; rc = pcmk__scan_ll(elem_l, &int_elem_l, -1LL); if (rc != pcmk_rc_ok) { // Shouldn't be possible pcmk__warn("Comparing current CIB %s as -1 because '%s' is not an " "integer", field, elem_l); } rc = pcmk__scan_ll(elem_r, &int_elem_r, -1LL); if (rc != pcmk_rc_ok) { // Shouldn't be possible pcmk__warn("Comparing joining node's CIB %s as -1 because '%s' is not " "an integer", field, elem_r); } if (int_elem_l < int_elem_r) { return -1; } else if (int_elem_l > int_elem_r) { return 1; } return 0; } /* A_DC_JOIN_PROCESS_REQ */ void do_dc_join_filter_offer(long long action, enum crmd_fsa_cause cause, enum crmd_fsa_state cur_state, enum crmd_fsa_input current_input, fsa_data_t * msg_data) { xmlNode *generation = NULL; int cmp = 0; int join_id = -1; int count = 0; gint value = 0; gboolean ack_nack_bool = TRUE; ha_msg_input_t *join_ack = fsa_typed_data(fsa_dt_ha_msg); const char *join_from = pcmk__xe_get(join_ack->msg, PCMK__XA_SRC); const char *ref = pcmk__xe_get(join_ack->msg, PCMK_XA_REFERENCE); const char *join_version = pcmk__xe_get(join_ack->msg, PCMK_XA_CRM_FEATURE_SET); pcmk__node_status_t *join_node = NULL; if (join_from == NULL) { pcmk__err("Ignoring invalid join request without node name"); return; } join_node = pcmk__get_node(0, join_from, NULL, pcmk__node_search_cluster_member); pcmk__xe_get_int(join_ack->msg, PCMK__XA_JOIN_ID, &join_id); if (join_id != current_join_id) { pcmk__debug("Ignoring join-%d request from %s because we are on " "join-%d", join_id, join_from, current_join_id); check_join_state(cur_state, __func__); return; } generation = join_ack->xml; if (max_generation_xml != NULL && generation != NULL) { int lpc = 0; const char *attributes[] = { PCMK_XA_ADMIN_EPOCH, PCMK_XA_EPOCH, PCMK_XA_NUM_UPDATES, }; /* It's not obvious that join_ack->xml is the PCMK__XE_GENERATION_TUPLE * element from the join client. The "if" guard is for clarity. */ if (pcmk__xe_is(generation, PCMK__XE_GENERATION_TUPLE)) { for (lpc = 0; cmp == 0 && lpc < PCMK__NELEM(attributes); lpc++) { cmp = compare_int_fields(max_generation_xml, generation, attributes[lpc]); } } else { // Should always be PCMK__XE_GENERATION_TUPLE CRM_LOG_ASSERT(false); } } if (ref == NULL) { ref = "none"; // for logging only } if (lookup_failed_sync_node(join_from, &value) == pcmk_rc_ok) { pcmk__err("Rejecting join-%d request from node %s because we failed to " "sync its CIB in join-%d " QB_XS " ref=%s", join_id, join_from, value, ref); ack_nack_bool = FALSE; } else if (!pcmk__cluster_is_node_active(join_node)) { if (match_down_event(join_from) != NULL) { /* The join request was received after the node was fenced or * otherwise shutdown in a way that we're aware of. No need to log * an error in this rare occurrence; we know the client was recently * shut down, and receiving a lingering in-flight request is not * cause for alarm. */ pcmk__debug("Rejecting join-%d request from inactive node %s " QB_XS " ref=%s", join_id, join_from, ref); } else { pcmk__err("Rejecting join-%d request from inactive node %s " QB_XS " ref=%s", join_id, join_from, ref); } ack_nack_bool = FALSE; } else if (generation == NULL) { pcmk__err("Rejecting invalid join-%d request from node %s missing CIB " "generation " QB_XS " ref=%s", join_id, join_from, ref); ack_nack_bool = FALSE; } else if ((join_version == NULL) || !feature_set_compatible(CRM_FEATURE_SET, join_version)) { pcmk__err("Rejecting join-%d request from node %s because feature set " "%s is incompatible with ours (%s) " QB_XS " ref=%s", join_id, join_from, pcmk__s(join_version, "pre-3.1.0"), CRM_FEATURE_SET, ref); ack_nack_bool = FALSE; } else if (max_generation_xml == NULL) { const char *validation = pcmk__xe_get(generation, PCMK_XA_VALIDATE_WITH); if (pcmk__get_schema(validation) == NULL) { pcmk__err("Rejecting join-%d request from %s (with first CIB " "generation) due to %s schema version %s " QB_XS " ref=%s", join_id, join_from, ((validation == NULL)? "missing" : "unknown"), pcmk__s(validation, ""), ref); ack_nack_bool = FALSE; } else { pcmk__debug("Accepting join-%d request from %s (with first CIB " "generation) " QB_XS " ref=%s", join_id, join_from, ref); max_generation_xml = pcmk__xml_copy(NULL, generation); pcmk__str_update(&max_generation_from, join_from); } } else if ((cmp < 0) || ((cmp == 0) && controld_is_local_node(join_from))) { const char *validation = pcmk__xe_get(generation, PCMK_XA_VALIDATE_WITH); if (pcmk__get_schema(validation) == NULL) { pcmk__err("Rejecting join-%d request from %s (with better CIB " "generation than current best from %s) due to %s schema " "version %s " QB_XS " ref=%s", join_id, join_from, max_generation_from, ((validation == NULL)? "missing" : "unknown"), pcmk__s(validation, ""), ref); ack_nack_bool = FALSE; } else { pcmk__debug("Accepting join-%d request from %s (with better CIB " "generation than current best from %s) " QB_XS " ref=%s", join_id, join_from, max_generation_from, ref); - crm_log_xml_debug(max_generation_xml, "Old max generation"); - crm_log_xml_debug(generation, "New max generation"); + pcmk__log_xml_debug(max_generation_xml, "Old max generation"); + pcmk__log_xml_debug(generation, "New max generation"); pcmk__xml_free(max_generation_xml); max_generation_xml = pcmk__xml_copy(NULL, join_ack->xml); pcmk__str_update(&max_generation_from, join_from); } } else { pcmk__debug("Accepting join-%d request from %s " QB_XS " ref=%s", join_id, join_from, ref); } if (!ack_nack_bool) { crm_update_peer_join(__func__, join_node, controld_join_nack); pcmk__update_peer_expected(__func__, join_node, CRMD_JOINSTATE_NACK); } else { crm_update_peer_join(__func__, join_node, controld_join_integrated); pcmk__update_peer_expected(__func__, join_node, CRMD_JOINSTATE_MEMBER); } count = crmd_join_phase_count(controld_join_integrated); pcmk__debug("%d node%s currently integrated in join-%d", count, pcmk__plural_s(count), join_id); if (check_join_state(cur_state, __func__) == FALSE) { // Don't waste time by invoking the scheduler yet count = crmd_join_phase_count(controld_join_welcomed); pcmk__debug("Waiting on join-%d requests from %d outstanding node%s", join_id, count, pcmk__plural_s(count)); } } /* A_DC_JOIN_FINALIZE */ void do_dc_join_finalize(long long action, enum crmd_fsa_cause cause, enum crmd_fsa_state cur_state, enum crmd_fsa_input current_input, fsa_data_t * msg_data) { char *sync_from = NULL; int rc = pcmk_ok; int count_welcomed = crmd_join_phase_count(controld_join_welcomed); int count_finalizable = crmd_join_phase_count(controld_join_integrated) + crmd_join_phase_count(controld_join_nack); /* This we can do straight away and avoid clients timing us out * while we compute the latest CIB */ if (count_welcomed != 0) { pcmk__debug("Waiting on join-%d requests from %d outstanding node%s " "before finalizing join", current_join_id, count_welcomed, pcmk__plural_s(count_welcomed)); crmd_join_phase_log(LOG_DEBUG); /* crmd_fsa_stall(FALSE); Needed? */ return; } else if (count_finalizable == 0) { pcmk__debug("Finalization not needed for join-%d at the current time", current_join_id); crmd_join_phase_log(LOG_DEBUG); check_join_state(controld_globals.fsa_state, __func__); return; } controld_clear_fsa_input_flags(R_HAVE_CIB); if ((max_generation_from == NULL) || controld_is_local_node(max_generation_from)) { controld_set_fsa_input_flags(R_HAVE_CIB); } if (!controld_globals.transition_graph->complete) { pcmk__warn("Delaying join-%d finalization while transition in progress", current_join_id); crmd_join_phase_log(LOG_DEBUG); crmd_fsa_stall(FALSE); return; } if (pcmk__is_set(controld_globals.fsa_input_register, R_HAVE_CIB)) { // Send our CIB out to everyone sync_from = pcmk__str_copy(controld_globals.cluster->priv->node_name); } else { // Ask for the agreed best CIB sync_from = pcmk__str_copy(max_generation_from); } pcmk__notice("Finalizing join-%d for %d node%s (sync'ing CIB %s.%s.%s with " "schema %s and feature set %s from %s)", current_join_id, count_finalizable, pcmk__plural_s(count_finalizable), pcmk__xe_get(max_generation_xml, PCMK_XA_ADMIN_EPOCH), pcmk__xe_get(max_generation_xml, PCMK_XA_EPOCH), pcmk__xe_get(max_generation_xml, PCMK_XA_NUM_UPDATES), pcmk__xe_get(max_generation_xml, PCMK_XA_VALIDATE_WITH), pcmk__xe_get(max_generation_xml, PCMK_XA_CRM_FEATURE_SET), sync_from); crmd_join_phase_log(LOG_DEBUG); rc = controld_globals.cib_conn->cmds->sync_from(controld_globals.cib_conn, sync_from, NULL, cib_none); fsa_register_cib_callback(rc, sync_from, finalize_sync_callback); } void free_max_generation(void) { free(max_generation_from); max_generation_from = NULL; pcmk__xml_free(max_generation_xml); max_generation_xml = NULL; } void finalize_sync_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data) { CRM_LOG_ASSERT(-EPERM != rc); if (rc != pcmk_ok) { const char *sync_from = (const char *) user_data; do_crm_log(((rc == -pcmk_err_old_data)? LOG_WARNING : LOG_ERR), "Could not sync CIB from %s in join-%d: %s", sync_from, current_join_id, pcmk_strerror(rc)); if (rc != -pcmk_err_old_data) { record_failed_sync_node(sync_from, current_join_id); } /* restart the whole join process */ register_fsa_error_adv(C_FSA_INTERNAL, I_ELECTION_DC, NULL, NULL, __func__); } else if (!AM_I_DC) { pcmk__debug("Sync'ed CIB for join-%d but no longer DC", current_join_id); } else if (controld_globals.fsa_state != S_FINALIZE_JOIN) { pcmk__debug("Sync'ed CIB for join-%d but no longer in S_FINALIZE_JOIN " "(%s)", current_join_id, fsa_state2string(controld_globals.fsa_state)); } else { controld_set_fsa_input_flags(R_HAVE_CIB); /* make sure dc_uuid is re-set to us */ if (!check_join_state(controld_globals.fsa_state, __func__)) { int count_finalizable = 0; count_finalizable = crmd_join_phase_count(controld_join_integrated) + crmd_join_phase_count(controld_join_nack); pcmk__debug("Notifying %d node%s of join-%d results", count_finalizable, pcmk__plural_s(count_finalizable), current_join_id); g_hash_table_foreach(pcmk__peer_cache, finalize_join_for, NULL); } } } static void join_node_state_commit_callback(xmlNode *msg, int call_id, int rc, xmlNode *output, void *user_data) { const char *node = user_data; if (rc != pcmk_ok) { fsa_data_t *msg_data = NULL; // for register_fsa_error() macro pcmk__crit("join-%d node history update (via CIB call %d) for node %s " "failed: %s", current_join_id, call_id, node, pcmk_strerror(rc)); - crm_log_xml_debug(msg, "failed"); + pcmk__log_xml_debug(msg, "failed"); register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL); } pcmk__debug("join-%d node history update (via CIB call %d) for node %s " "complete", current_join_id, call_id, node); check_join_state(controld_globals.fsa_state, __func__); } /* A_DC_JOIN_PROCESS_ACK */ void do_dc_join_ack(long long action, enum crmd_fsa_cause cause, enum crmd_fsa_state cur_state, enum crmd_fsa_input current_input, fsa_data_t * msg_data) { int join_id = -1; ha_msg_input_t *join_ack = fsa_typed_data(fsa_dt_ha_msg); const char *op = pcmk__xe_get(join_ack->msg, PCMK__XA_CRM_TASK); char *join_from = pcmk__xe_get_copy(join_ack->msg, PCMK__XA_SRC); pcmk__node_status_t *peer = NULL; enum controld_join_phase phase = controld_join_none; enum controld_section_e section = controld_section_lrm; char *xpath = NULL; xmlNode *state = join_ack->xml; xmlNode *execd_state = NULL; cib_t *cib = controld_globals.cib_conn; int rc = pcmk_ok; // Sanity checks if (join_from == NULL) { pcmk__warn("Ignoring message received without node identification"); goto done; } if (op == NULL) { pcmk__warn("Ignoring message received from %s without task", join_from); goto done; } if (strcmp(op, CRM_OP_JOIN_CONFIRM)) { pcmk__debug("Ignoring '%s' message from %s while waiting for '%s'", op, join_from, CRM_OP_JOIN_CONFIRM); goto done; } if (pcmk__xe_get_int(join_ack->msg, PCMK__XA_JOIN_ID, &join_id) != pcmk_rc_ok) { pcmk__warn("Ignoring join confirmation from %s without valid join ID", join_from); goto done; } peer = pcmk__get_node(0, join_from, NULL, pcmk__node_search_cluster_member); phase = controld_get_join_phase(peer); if (phase != controld_join_finalized) { pcmk__info("Ignoring out-of-sequence join-%d confirmation from %s " "(currently %s not %s)", join_id, join_from, join_phase_text(phase), join_phase_text(controld_join_finalized)); goto done; } if (join_id != current_join_id) { pcmk__err("Rejecting join-%d confirmation from %s because currently on " "join-%d", join_id, join_from, current_join_id); crm_update_peer_join(__func__, peer, controld_join_nack); goto done; } crm_update_peer_join(__func__, peer, controld_join_confirmed); /* Update CIB with node's current executor state. A new transition will be * triggered later, when the CIB manager notifies us of the change. * * The delete and modify requests are part of an atomic transaction. */ rc = cib->cmds->init_transaction(cib); if (rc != pcmk_ok) { goto done; } // Delete relevant parts of node's current executor state from CIB if (pcmk__is_set(controld_globals.flags, controld_shutdown_lock_enabled)) { section = controld_section_lrm_unlocked; } controld_node_state_deletion_strings(join_from, section, &xpath, NULL); rc = cib->cmds->remove(cib, xpath, NULL, cib_xpath|cib_multiple|cib_transaction); if (rc != pcmk_ok) { goto done; } // Update CIB with node's latest known executor state if (controld_is_local_node(join_from)) { // Use the latest possible state if processing our own join ack execd_state = controld_query_executor_state(); if (execd_state != NULL) { pcmk__debug("Updating local node history for join-%d from query " "result", current_join_id); state = execd_state; } else { pcmk__warn("Updating local node history from join-%d confirmation " "because query failed", current_join_id); } } else { pcmk__debug("Updating node history for %s from join-%d confirmation", join_from, current_join_id); } rc = cib->cmds->modify(cib, PCMK_XE_STATUS, state, cib_can_create|cib_transaction); pcmk__xml_free(execd_state); if (rc != pcmk_ok) { goto done; } // Commit the transaction rc = cib->cmds->end_transaction(cib, true, cib_none); fsa_register_cib_callback(rc, join_from, join_node_state_commit_callback); if (rc > 0) { // join_from will be freed after callback join_from = NULL; rc = pcmk_ok; } done: if (rc != pcmk_ok) { pcmk__crit("join-%d node history update for node %s failed: %s", current_join_id, join_from, pcmk_strerror(rc)); register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL); } free(join_from); free(xpath); } void finalize_join_for(gpointer key, gpointer value, gpointer user_data) { xmlNode *acknak = NULL; xmlNode *tmp1 = NULL; pcmk__node_status_t *join_node = value; const char *join_to = join_node->name; enum controld_join_phase phase = controld_get_join_phase(join_node); bool integrated = false; switch (phase) { case controld_join_integrated: integrated = true; break; case controld_join_nack: break; default: pcmk__trace("Not updating non-integrated and non-nacked node %s " "(%s) for join-%d", join_to, join_phase_text(phase), current_join_id); return; } /* Update the element with the node's name and UUID, in case they * weren't known before */ pcmk__trace("Updating node name and UUID in CIB for %s", join_to); tmp1 = pcmk__xe_create(NULL, PCMK_XE_NODE); pcmk__xe_set(tmp1, PCMK_XA_ID, pcmk__cluster_get_xml_id(join_node)); pcmk__xe_set(tmp1, PCMK_XA_UNAME, join_to); fsa_cib_anon_update(PCMK_XE_NODES, tmp1); pcmk__xml_free(tmp1); join_node = pcmk__get_node(0, join_to, NULL, pcmk__node_search_cluster_member); if (!pcmk__cluster_is_node_active(join_node)) { /* * NACK'ing nodes that the membership layer doesn't know about yet * simply creates more churn * * Better to leave them waiting and let the join restart when * the new membership event comes in * * All other NACKs (due to versions etc) should still be processed */ pcmk__update_peer_expected(__func__, join_node, CRMD_JOINSTATE_PENDING); return; } // Acknowledge or nack node's join request pcmk__debug("%sing join-%d request from %s", (integrated? "Acknowledg" : "Nack"), current_join_id, join_to); acknak = create_dc_message(CRM_OP_JOIN_ACKNAK, join_to); pcmk__xe_set_bool_attr(acknak, CRM_OP_JOIN_ACKNAK, integrated); if (integrated) { // No change needed for a nacked node crm_update_peer_join(__func__, join_node, controld_join_finalized); pcmk__update_peer_expected(__func__, join_node, CRMD_JOINSTATE_MEMBER); /* Iterate through the remote peer cache and add information on which * node hosts each to the ACK message. This keeps new controllers in * sync with what has already happened. */ if (pcmk__cluster_num_remote_nodes() > 0) { GHashTableIter iter; pcmk__node_status_t *node = NULL; xmlNode *remotes = pcmk__xe_create(acknak, PCMK_XE_NODES); g_hash_table_iter_init(&iter, pcmk__remote_peer_cache); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { xmlNode *remote = NULL; if (!node->conn_host) { continue; } remote = pcmk__xe_create(remotes, PCMK_XE_NODE); pcmk__xe_set_props(remote, PCMK_XA_ID, node->name, PCMK__XA_NODE_STATE, node->state, PCMK__XA_CONNECTION_HOST, node->conn_host, NULL); } } } pcmk__cluster_send_message(join_node, pcmk_ipc_controld, acknak); pcmk__xml_free(acknak); return; } gboolean check_join_state(enum crmd_fsa_state cur_state, const char *source) { static unsigned long long highest_seq = 0; if (controld_globals.membership_id != controld_globals.peer_seq) { pcmk__debug("join-%d: Membership changed from %llu to %llu " QB_XS " highest=%llu state=%s for=%s", current_join_id, controld_globals.membership_id, controld_globals.peer_seq, highest_seq, fsa_state2string(cur_state), source); if (highest_seq < controld_globals.peer_seq) { /* Don't spam the FSA with duplicates */ highest_seq = controld_globals.peer_seq; register_fsa_input_before(C_FSA_INTERNAL, I_NODE_JOIN, NULL); } } else if (cur_state == S_INTEGRATION) { if (crmd_join_phase_count(controld_join_welcomed) == 0) { int count = crmd_join_phase_count(controld_join_integrated); pcmk__debug("join-%d: Integration of %d peer%s complete " QB_XS " state=%s for=%s", current_join_id, count, pcmk__plural_s(count), fsa_state2string(cur_state), source); register_fsa_input_before(C_FSA_INTERNAL, I_INTEGRATED, NULL); return TRUE; } } else if (cur_state == S_FINALIZE_JOIN) { if (!pcmk__is_set(controld_globals.fsa_input_register, R_HAVE_CIB)) { pcmk__debug("join-%d: Delaying finalization until we have CIB " QB_XS " state=%s for=%s", current_join_id, fsa_state2string(cur_state), source); return TRUE; } else if (crmd_join_phase_count(controld_join_welcomed) != 0) { int count = crmd_join_phase_count(controld_join_welcomed); pcmk__debug("join-%d: Still waiting on %d welcomed node%s " QB_XS " state=%s for=%s", current_join_id, count, pcmk__plural_s(count), fsa_state2string(cur_state), source); crmd_join_phase_log(LOG_DEBUG); } else if (crmd_join_phase_count(controld_join_integrated) != 0) { int count = crmd_join_phase_count(controld_join_integrated); pcmk__debug("join-%d: Still waiting on %d integrated node%s " QB_XS " state=%s for=%s", current_join_id, count, pcmk__plural_s(count), fsa_state2string(cur_state), source); crmd_join_phase_log(LOG_DEBUG); } else if (crmd_join_phase_count(controld_join_finalized) != 0) { int count = crmd_join_phase_count(controld_join_finalized); pcmk__debug("join-%d: Still waiting on %d finalized node%s " QB_XS " state=%s for=%s", current_join_id, count, pcmk__plural_s(count), fsa_state2string(cur_state), source); crmd_join_phase_log(LOG_DEBUG); } else { pcmk__debug("join-%d: Complete " QB_XS " state=%s for=%s", current_join_id, fsa_state2string(cur_state), source); register_fsa_input_later(C_FSA_INTERNAL, I_FINALIZED, NULL); return TRUE; } } return FALSE; } void do_dc_join_final(long long action, enum crmd_fsa_cause cause, enum crmd_fsa_state cur_state, enum crmd_fsa_input current_input, fsa_data_t * msg_data) { pcmk__debug("Ensuring DC, quorum and node attributes are up-to-date"); crm_update_quorum(pcmk__cluster_has_quorum(), TRUE); } int crmd_join_phase_count(enum controld_join_phase phase) { int count = 0; pcmk__node_status_t *peer; GHashTableIter iter; g_hash_table_iter_init(&iter, pcmk__peer_cache); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &peer)) { if (controld_get_join_phase(peer) == phase) { count++; } } return count; } void crmd_join_phase_log(int level) { pcmk__node_status_t *peer; GHashTableIter iter; g_hash_table_iter_init(&iter, pcmk__peer_cache); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &peer)) { do_crm_log(level, "join-%d: %s=%s", current_join_id, peer->name, join_phase_text(controld_get_join_phase(peer))); } } diff --git a/daemons/controld/controld_membership.c b/daemons/controld/controld_membership.c index 49bdc42b0b..ccc37c97fa 100644 --- a/daemons/controld/controld_membership.c +++ b/daemons/controld/controld_membership.c @@ -1,481 +1,481 @@ /* * 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. */ /* put these first so that uuid_t is defined without conflicts */ #include #include #include #include // PCMK_SCORE_INFINITY #include #include #include #include void post_cache_update(int instance); extern gboolean check_join_state(enum crmd_fsa_state cur_state, const char *source); static void reap_dead_nodes(gpointer key, gpointer value, gpointer user_data) { pcmk__node_status_t *node = value; if (pcmk__cluster_is_node_active(node)) { return; } crm_update_peer_join(__func__, node, controld_join_none); if ((node != NULL) && (node->name != NULL)) { if (controld_is_local_node(node->name)) { pcmk__err("We're not part of the cluster anymore"); register_fsa_input(C_FSA_INTERNAL, I_ERROR, NULL); } else if (!AM_I_DC && pcmk__str_eq(node->name, controld_globals.dc_name, pcmk__str_casei)) { pcmk__warn("Our DC node (%s) left the cluster", node->name); register_fsa_input(C_FSA_INTERNAL, I_ELECTION, NULL); } } if ((controld_globals.fsa_state == S_INTEGRATION) || (controld_globals.fsa_state == S_FINALIZE_JOIN)) { check_join_state(controld_globals.fsa_state, __func__); } if ((node != NULL) && (node->xml_id != NULL)) { fail_incompletable_actions(controld_globals.transition_graph, node->xml_id); } } void post_cache_update(int instance) { xmlNode *no_op = NULL; controld_globals.peer_seq = instance; pcmk__debug("Updated cache after membership event %d", instance); g_hash_table_foreach(pcmk__peer_cache, reap_dead_nodes, NULL); controld_set_fsa_input_flags(R_MEMBERSHIP); if (AM_I_DC) { populate_cib_nodes(node_update_quick | node_update_cluster | node_update_peer | node_update_expected, __func__); } /* * If we lost nodes, we should re-check the election status * Safe to call outside of an election */ controld_set_fsa_action_flags(A_ELECTION_CHECK); controld_trigger_fsa(); /* Membership changed, remind everyone we're here. * This will aid detection of duplicate DCs */ no_op = pcmk__new_request(pcmk_ipc_controld, (AM_I_DC? CRM_SYSTEM_DC : CRM_SYSTEM_CRMD), NULL, CRM_SYSTEM_CRMD, CRM_OP_NOOP, NULL); pcmk__cluster_send_message(NULL, pcmk_ipc_controld, no_op); pcmk__xml_free(no_op); } static void crmd_node_update_complete(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data) { fsa_data_t *msg_data = NULL; if (rc == pcmk_ok) { pcmk__trace("Node update %d complete", call_id); } else if(call_id < pcmk_ok) { pcmk__err("Node update failed: %s (%d)", pcmk_strerror(call_id), call_id); - crm_log_xml_debug(msg, "failed"); + pcmk__log_xml_debug(msg, "failed"); register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL); } else { pcmk__err("Node update %d failed: %s (%d)", call_id, pcmk_strerror(rc), rc); - crm_log_xml_debug(msg, "failed"); + pcmk__log_xml_debug(msg, "failed"); register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL); } } /*! * \internal * \brief Create an XML node state tag with updates * * \param[in,out] node Node whose state will be used for update * \param[in] flags Bitmask of node_update_flags indicating what to update * \param[in,out] parent XML node to contain update (or NULL) * \param[in] source Who requested the update (only used for logging) * * \return Pointer to created node state tag */ xmlNode * create_node_state_update(pcmk__node_status_t *node, int flags, xmlNode *parent, const char *source) { // @TODO Ensure all callers handle NULL returns const char *id = NULL; const char *value = NULL; xmlNode *node_state; if (!node->state) { pcmk__info("Node update for %s cancelled: no state, not seen yet", node->name); return NULL; } node_state = pcmk__xe_create(parent, PCMK__XE_NODE_STATE); if (pcmk__is_set(node->flags, pcmk__node_status_remote)) { pcmk__xe_set_bool_attr(node_state, PCMK_XA_REMOTE_NODE, true); } id = pcmk__cluster_get_xml_id(node); if ((id == NULL) || (pcmk__xe_set(node_state, PCMK_XA_ID, id) != pcmk_rc_ok)) { pcmk__info("Node update for %s cancelled: no ID", node->name); pcmk__xml_free(node_state); return NULL; } pcmk__xe_set(node_state, PCMK_XA_UNAME, node->name); if ((flags & node_update_cluster) && node->state) { if (pcmk__compare_versions(controld_globals.dc_version, "3.18.0") >= 0) { // A value 0 means the node is not a cluster member. pcmk__xe_set_ll(node_state, PCMK__XA_IN_CCM, node->when_member); } else { pcmk__xe_set_bool_attr(node_state, PCMK__XA_IN_CCM, pcmk__str_eq(node->state, PCMK_VALUE_MEMBER, pcmk__str_none)); } } if (!pcmk__is_set(node->flags, pcmk__node_status_remote)) { if (flags & node_update_peer) { if (pcmk__compare_versions(controld_globals.dc_version, "3.18.0") >= 0) { // A value 0 means the peer is offline in CPG. pcmk__xe_set_ll(node_state, PCMK_XA_CRMD, node->when_online); } else { // @COMPAT DCs < 2.1.7 use online/offline rather than timestamp value = PCMK_VALUE_OFFLINE; if (pcmk__is_set(node->processes, crm_get_cluster_proc())) { value = PCMK_VALUE_ONLINE; } pcmk__xe_set(node_state, PCMK_XA_CRMD, value); } } if (flags & node_update_join) { if (controld_get_join_phase(node) <= controld_join_none) { value = CRMD_JOINSTATE_DOWN; } else { value = CRMD_JOINSTATE_MEMBER; } pcmk__xe_set(node_state, PCMK__XA_JOIN, value); } if (flags & node_update_expected) { pcmk__xe_set(node_state, PCMK_XA_EXPECTED, node->expected); } } pcmk__xe_set(node_state, PCMK_XA_CRM_DEBUG_ORIGIN, source); return node_state; } static void remove_conflicting_node_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data) { char *node_uuid = user_data; do_crm_log_unlikely(rc == 0 ? LOG_DEBUG : LOG_NOTICE, "Deletion of the unknown conflicting node \"%s\": %s (rc=%d)", node_uuid, pcmk_strerror(rc), rc); } static void search_conflicting_node_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data) { char *new_node_uuid = user_data; xmlNode *node_xml = NULL; if (rc != pcmk_ok) { if (rc != -ENXIO) { pcmk__notice("Searching conflicting nodes for %s failed: %s (%d)", new_node_uuid, pcmk_strerror(rc), rc); } return; } else if (output == NULL) { return; } if (pcmk__xe_is(output, PCMK_XE_NODE)) { node_xml = output; } else { node_xml = pcmk__xe_first_child(output, PCMK_XE_NODE, NULL, NULL); } for (; node_xml != NULL; node_xml = pcmk__xe_next(node_xml, PCMK_XE_NODE)) { const char *node_uuid = NULL; const char *node_uname = NULL; GHashTableIter iter; pcmk__node_status_t *node = NULL; gboolean known = FALSE; node_uuid = pcmk__xe_get(node_xml, PCMK_XA_ID); node_uname = pcmk__xe_get(node_xml, PCMK_XA_UNAME); if (node_uuid == NULL || node_uname == NULL) { continue; } g_hash_table_iter_init(&iter, pcmk__peer_cache); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { if ((node != NULL) && pcmk__str_eq(node->xml_id, node_uuid, pcmk__str_casei) && pcmk__str_eq(node->name, node_uname, pcmk__str_casei)) { known = TRUE; break; } } if (known == FALSE) { cib_t *cib_conn = controld_globals.cib_conn; int delete_call_id = 0; xmlNode *node_state_xml = NULL; pcmk__notice("Deleting unknown node %s/%s which has conflicting " "uname with %s", node_uuid, node_uname, new_node_uuid); delete_call_id = cib_conn->cmds->remove(cib_conn, PCMK_XE_NODES, node_xml, cib_none); fsa_register_cib_callback(delete_call_id, pcmk__str_copy(node_uuid), remove_conflicting_node_callback); node_state_xml = pcmk__xe_create(NULL, PCMK__XE_NODE_STATE); pcmk__xe_set(node_state_xml, PCMK_XA_ID, node_uuid); pcmk__xe_set(node_state_xml, PCMK_XA_UNAME, node_uname); delete_call_id = cib_conn->cmds->remove(cib_conn, PCMK_XE_STATUS, node_state_xml, cib_none); fsa_register_cib_callback(delete_call_id, pcmk__str_copy(node_uuid), remove_conflicting_node_callback); pcmk__xml_free(node_state_xml); } } } static void node_list_update_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data) { fsa_data_t *msg_data = NULL; if(call_id < pcmk_ok) { pcmk__err("Node list update failed: %s (%d)", pcmk_strerror(call_id), call_id); - crm_log_xml_debug(msg, "update:failed"); + pcmk__log_xml_debug(msg, "update:failed"); register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL); } else if(rc < pcmk_ok) { pcmk__err("Node update %d failed: %s (%d)", call_id, pcmk_strerror(rc), rc); - crm_log_xml_debug(msg, "update:failed"); + pcmk__log_xml_debug(msg, "update:failed"); register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL); } } void populate_cib_nodes(enum node_update_flags flags, const char *source) { cib_t *cib_conn = controld_globals.cib_conn; int call_id = 0; gboolean from_hashtable = TRUE; xmlNode *node_list = pcmk__xe_create(NULL, PCMK_XE_NODES); #if SUPPORT_COROSYNC if (!pcmk__is_set(flags, node_update_quick) && (pcmk_get_cluster_layer() == pcmk_cluster_layer_corosync)) { from_hashtable = pcmk__corosync_add_nodes(node_list); } #endif if (from_hashtable) { GHashTableIter iter; pcmk__node_status_t *node = NULL; GString *xpath = NULL; g_hash_table_iter_init(&iter, pcmk__peer_cache); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { xmlNode *new_node = NULL; if ((node->xml_id != NULL) && (node->name != NULL)) { pcmk__trace("Creating node entry for %s/%s", node->name, node->xml_id); if (xpath == NULL) { xpath = g_string_sized_new(512); } else { g_string_truncate(xpath, 0); } /* We need both to be valid */ new_node = pcmk__xe_create(node_list, PCMK_XE_NODE); pcmk__xe_set(new_node, PCMK_XA_ID, node->xml_id); pcmk__xe_set(new_node, PCMK_XA_UNAME, node->name); /* Search and remove unknown nodes with the conflicting uname from CIB */ pcmk__g_strcat(xpath, "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_NODES "/" PCMK_XE_NODE "[@" PCMK_XA_UNAME "='", node->name, "']" "[@" PCMK_XA_ID "!='", node->xml_id, "']", NULL); call_id = cib_conn->cmds->query(cib_conn, (const char *) xpath->str, NULL, cib_xpath); fsa_register_cib_callback(call_id, pcmk__str_copy(node->xml_id), search_conflicting_node_callback); } } if (xpath != NULL) { g_string_free(xpath, TRUE); } } pcmk__trace("Populating section from %s", (from_hashtable? "hashtable" : "cluster")); if ((controld_update_cib(PCMK_XE_NODES, node_list, cib_none, node_list_update_callback) == pcmk_rc_ok) && (pcmk__peer_cache != NULL) && AM_I_DC) { /* * There is no need to update the local CIB with our values if * we've not seen valid membership data */ GHashTableIter iter; pcmk__node_status_t *node = NULL; pcmk__xml_free(node_list); node_list = pcmk__xe_create(NULL, PCMK_XE_STATUS); g_hash_table_iter_init(&iter, pcmk__peer_cache); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { create_node_state_update(node, flags, node_list, source); } if (pcmk__remote_peer_cache != NULL) { g_hash_table_iter_init(&iter, pcmk__remote_peer_cache); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { create_node_state_update(node, flags, node_list, source); } } controld_update_cib(PCMK_XE_STATUS, node_list, cib_none, crmd_node_update_complete); } pcmk__xml_free(node_list); } static void cib_quorum_update_complete(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data) { fsa_data_t *msg_data = NULL; if (rc == pcmk_ok) { pcmk__trace("Quorum update %d complete", call_id); } else { pcmk__err("Quorum update %d failed: %s (%d)", call_id, pcmk_strerror(rc), rc); - crm_log_xml_debug(msg, "failed"); + pcmk__log_xml_debug(msg, "failed"); register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL); } } void crm_update_quorum(gboolean quorum, gboolean force_update) { bool has_quorum = pcmk__is_set(controld_globals.flags, controld_has_quorum); if (quorum) { controld_set_global_flags(controld_ever_had_quorum); } else if (pcmk__all_flags_set(controld_globals.flags, controld_ever_had_quorum |controld_no_quorum_panic)) { pcmk__panic("Quorum lost"); } if (AM_I_DC && ((has_quorum && !quorum) || (!has_quorum && quorum) || force_update)) { xmlNode *update = NULL; update = pcmk__xe_create(NULL, PCMK_XE_CIB); pcmk__xe_set_int(update, PCMK_XA_HAVE_QUORUM, quorum); pcmk__xe_set(update, PCMK_XA_DC_UUID, controld_globals.our_uuid); pcmk__debug("Updating quorum status to %s", pcmk__btoa(quorum)); controld_update_cib(PCMK_XE_CIB, update, cib_none, cib_quorum_update_complete); pcmk__xml_free(update); /* Quorum changes usually cause a new transition via other activity: * quorum gained via a node joining will abort via the node join, * and quorum lost via a node leaving will usually abort via resource * activity and/or fencing. * * However, it is possible that nothing else causes a transition (e.g. * someone forces quorum via corosync-cmaptcl, or quorum is lost due to * a node in standby shutting down cleanly), so here ensure a new * transition is triggered. */ if (quorum) { /* If quorum was gained, abort after a short delay, in case multiple * nodes are joining around the same time, so the one that brings us * to quorum doesn't cause all the remaining ones to be fenced. */ abort_after_delay(PCMK_SCORE_INFINITY, pcmk__graph_restart, "Quorum gained", 5000); } else { abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, "Quorum lost", NULL); } } if (quorum) { controld_set_global_flags(controld_has_quorum); } else { controld_clear_global_flags(controld_has_quorum); } } diff --git a/daemons/controld/controld_te_events.c b/daemons/controld/controld_te_events.c index 7e939edfef..ad0e04d5a0 100644 --- a/daemons/controld/controld_te_events.c +++ b/daemons/controld/controld_te_events.c @@ -1,618 +1,618 @@ /* * 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 // xmlXPathObject, etc. #include #include #include // crm_meta_value() #include // PCMK_SCORE_INFINITY #include #include #include #include /*! * \internal * \brief Action numbers of outside events processed in current update diff * * This table is to be used as a set. It should be empty when the transitioner * begins processing a CIB update diff. It ensures that if there are multiple * events (for example, "_last_0" and "_last_failure_0") for the same action, * only one of them updates the failcount. Events that originate outside the * cluster can't be confirmed, since they're not in the transition graph. */ static GHashTable *outside_events = NULL; /*! * \internal * \brief Empty the hash table containing action numbers of outside events */ void controld_remove_all_outside_events(void) { if (outside_events != NULL) { g_hash_table_remove_all(outside_events); } } /*! * \internal * \brief Destroy the hash table containing action numbers of outside events */ void controld_destroy_outside_events_table(void) { if (outside_events != NULL) { g_hash_table_destroy(outside_events); outside_events = NULL; } } /*! * \internal * \brief Add an outside event's action number to a set * * \return Standard Pacemaker return code. Specifically, \p pcmk_rc_ok if the * event was not already in the set, or \p pcmk_rc_already otherwise. */ static int record_outside_event(gint action_num) { if (outside_events == NULL) { outside_events = g_hash_table_new(NULL, NULL); } if (g_hash_table_add(outside_events, GINT_TO_POINTER(action_num))) { return pcmk_rc_ok; } return pcmk_rc_already; } gboolean fail_incompletable_actions(pcmk__graph_t *graph, const char *down_node) { const char *target_uuid = NULL; const char *router = NULL; const char *router_uuid = NULL; xmlNode *last_action = NULL; GList *gIter = NULL; GList *gIter2 = NULL; if (graph == NULL || graph->complete) { return FALSE; } gIter = graph->synapses; for (; gIter != NULL; gIter = gIter->next) { pcmk__graph_synapse_t *synapse = (pcmk__graph_synapse_t *) gIter->data; if (pcmk__any_flags_set(synapse->flags, pcmk__synapse_confirmed|pcmk__synapse_failed)) { /* We've already been here */ continue; } gIter2 = synapse->actions; for (; gIter2 != NULL; gIter2 = gIter2->next) { pcmk__graph_action_t *action = (pcmk__graph_action_t *) gIter2->data; if ((action->type == pcmk__pseudo_graph_action) || pcmk__is_set(action->flags, pcmk__graph_action_confirmed)) { continue; } else if (action->type == pcmk__cluster_graph_action) { const char *task = pcmk__xe_get(action->xml, PCMK_XA_OPERATION); if (pcmk__str_eq(task, PCMK_ACTION_STONITH, pcmk__str_casei)) { continue; } } target_uuid = pcmk__xe_get(action->xml, PCMK__META_ON_NODE_UUID); router = pcmk__xe_get(action->xml, PCMK__XA_ROUTER_NODE); if (router) { const pcmk__node_status_t *node = pcmk__get_node(0, router, NULL, pcmk__node_search_cluster_member); if (node != NULL) { router_uuid = node->xml_id; } } if (pcmk__str_eq(target_uuid, down_node, pcmk__str_casei) || pcmk__str_eq(router_uuid, down_node, pcmk__str_casei)) { pcmk__set_graph_action_flags(action, pcmk__graph_action_failed); pcmk__set_synapse_flags(synapse, pcmk__synapse_failed); last_action = action->xml; stop_te_timer(action); pcmk__update_graph(graph, action); if (pcmk__is_set(synapse->flags, pcmk__synapse_executed)) { pcmk__notice("Action %d (%s) was pending on %s (offline)", action->id, pcmk__xe_get(action->xml, PCMK__XA_OPERATION_KEY), down_node); } else { pcmk__info("Action %d (%s) is scheduled for %s (offline)", action->id, pcmk__xe_get(action->xml, PCMK__XA_OPERATION_KEY), down_node); } } } } if (last_action != NULL) { pcmk__info("Node %s shutdown resulted in un-runnable actions", down_node); abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, "Node failure", last_action); return TRUE; } return FALSE; } /*! * \internal * \brief Update failure-related node attributes if warranted * * \param[in] event XML describing operation that (maybe) failed * \param[in] event_node_uuid Node that event occurred on * \param[in] rc Actual operation return code * \param[in] target_rc Expected operation return code * \param[in] do_update If TRUE, do update regardless of operation type * \param[in] ignore_failures If TRUE, update last failure but not fail count * * \return TRUE if this was not a direct nack, success or lrm status refresh */ static gboolean update_failcount(const xmlNode *event, const char *event_node_uuid, int rc, int target_rc, gboolean do_update, gboolean ignore_failures) { guint interval_ms = 0; char *task = NULL; char *rsc_id = NULL; const char *value = NULL; const char *id = pcmk__xe_get(event, PCMK__XA_OPERATION_KEY); const char *on_uname = pcmk__node_name_from_uuid(event_node_uuid); const char *origin = pcmk__xe_get(event, PCMK_XA_CRM_DEBUG_ORIGIN); // Nothing needs to be done for success or status refresh if (rc == target_rc) { return FALSE; } else if (pcmk__str_eq(origin, "build_active_RAs", pcmk__str_casei)) { pcmk__debug("No update for %s (rc=%d) on %s: Old failure from lrm " "status refresh", id, rc, on_uname); return FALSE; } /* Sanity check */ CRM_CHECK(on_uname != NULL, return TRUE); CRM_CHECK(parse_op_key(id, &rsc_id, &task, &interval_ms), pcmk__err("Couldn't parse: %s", pcmk__xe_id(event)); goto bail); /* Decide whether update is necessary and what value to use */ if ((interval_ms > 0) || pcmk__str_eq(task, PCMK_ACTION_PROMOTE, pcmk__str_none) || pcmk__str_eq(task, PCMK_ACTION_DEMOTE, pcmk__str_none)) { do_update = TRUE; } else if (pcmk__str_eq(task, PCMK_ACTION_START, pcmk__str_none)) { do_update = TRUE; value = pcmk__s(controld_globals.transition_graph->failed_start_offset, PCMK_VALUE_INFINITY); } else if (pcmk__str_eq(task, PCMK_ACTION_STOP, pcmk__str_none)) { do_update = TRUE; value = pcmk__s(controld_globals.transition_graph->failed_stop_offset, PCMK_VALUE_INFINITY); } if (do_update) { pcmk__attrd_query_pair_t *fail_pair = NULL; pcmk__attrd_query_pair_t *last_pair = NULL; char *fail_name = NULL; char *last_name = NULL; GList *attrs = NULL; uint32_t opts = pcmk__node_attr_none; char *now = pcmk__ttoa(time(NULL)); // Fail count will be either incremented or set to infinity if (!pcmk_str_is_infinity(value)) { value = PCMK_XA_VALUE "++"; } if (g_hash_table_lookup(pcmk__remote_peer_cache, event_node_uuid)) { opts |= pcmk__node_attr_remote; } pcmk__info("Updating %s for %s on %s after failed %s: rc=%d " "(update=%s, time=%s)", (ignore_failures? "last failure" : "failcount"), rsc_id, on_uname, task, rc, value, now); /* Update the fail count, if we're not ignoring failures */ if (!ignore_failures) { fail_pair = pcmk__assert_alloc(1, sizeof(pcmk__attrd_query_pair_t)); fail_name = pcmk__failcount_name(rsc_id, task, interval_ms); fail_pair->name = fail_name; fail_pair->value = value; fail_pair->node = on_uname; attrs = g_list_prepend(attrs, fail_pair); } /* Update the last failure time (even if we're ignoring failures, * so that failure can still be detected and shown, e.g. by crm_mon) */ last_pair = pcmk__assert_alloc(1, sizeof(pcmk__attrd_query_pair_t)); last_name = pcmk__lastfailure_name(rsc_id, task, interval_ms); last_pair->name = last_name; last_pair->value = now; last_pair->node = on_uname; attrs = g_list_prepend(attrs, last_pair); update_attrd_list(attrs, opts); free(fail_name); free(fail_pair); free(last_name); free(last_pair); g_list_free(attrs); free(now); } bail: free(rsc_id); free(task); return TRUE; } pcmk__graph_action_t * controld_get_action(int id) { for (GList *item = controld_globals.transition_graph->synapses; item != NULL; item = item->next) { pcmk__graph_synapse_t *synapse = (pcmk__graph_synapse_t *) item->data; for (GList *item2 = synapse->actions; item2; item2 = item2->next) { pcmk__graph_action_t *action = (pcmk__graph_action_t *) item2->data; if (action->id == id) { return action; } } } return NULL; } pcmk__graph_action_t * get_cancel_action(const char *id, const char *node) { GList *gIter = NULL; GList *gIter2 = NULL; gIter = controld_globals.transition_graph->synapses; for (; gIter != NULL; gIter = gIter->next) { pcmk__graph_synapse_t *synapse = (pcmk__graph_synapse_t *) gIter->data; gIter2 = synapse->actions; for (; gIter2 != NULL; gIter2 = gIter2->next) { const char *task = NULL; const char *target = NULL; pcmk__graph_action_t *action = (pcmk__graph_action_t *) gIter2->data; task = pcmk__xe_get(action->xml, PCMK_XA_OPERATION); if (!pcmk__str_eq(PCMK_ACTION_CANCEL, task, pcmk__str_casei)) { continue; } task = pcmk__xe_get(action->xml, PCMK__XA_OPERATION_KEY); if (!pcmk__str_eq(task, id, pcmk__str_casei)) { continue; } target = pcmk__xe_get(action->xml, PCMK__META_ON_NODE_UUID); if (node && !pcmk__str_eq(target, node, pcmk__str_casei)) { pcmk__trace("Wrong node %s for %s on %s", target, id, node); continue; } pcmk__trace("Found %s on %s", id, node); return action; } } return NULL; } bool confirm_cancel_action(const char *id, const char *node_id) { const char *op_key = NULL; const char *node_name = NULL; pcmk__graph_action_t *cancel = get_cancel_action(id, node_id); if (cancel == NULL) { return FALSE; } op_key = pcmk__xe_get(cancel->xml, PCMK__XA_OPERATION_KEY); node_name = pcmk__xe_get(cancel->xml, PCMK__META_ON_NODE); stop_te_timer(cancel); te_action_confirmed(cancel, controld_globals.transition_graph); pcmk__info("Cancellation of %s on %s confirmed (action %d)", op_key, node_name, cancel->id); return TRUE; } /* downed nodes are listed like: ... */ #define XPATH_DOWNED "//" PCMK__XE_DOWNED \ "/" PCMK_XE_NODE "[@" PCMK_XA_ID "='%s']" /*! * \brief Find a transition event that would have made a specified node down * * \param[in] target UUID of node to match * * \return Matching event if found, NULL otherwise */ pcmk__graph_action_t * match_down_event(const char *target) { pcmk__graph_action_t *match = NULL; xmlXPathObject *xpath_ret = NULL; GList *gIter, *gIter2; char *xpath = pcmk__assert_asprintf(XPATH_DOWNED, target); for (gIter = controld_globals.transition_graph->synapses; gIter != NULL && match == NULL; gIter = gIter->next) { for (gIter2 = ((pcmk__graph_synapse_t * ) gIter->data)->actions; gIter2 != NULL && match == NULL; gIter2 = gIter2->next) { match = (pcmk__graph_action_t *) gIter2->data; if (pcmk__is_set(match->flags, pcmk__graph_action_executed)) { xpath_ret = pcmk__xpath_search(match->xml->doc, xpath); if (pcmk__xpath_num_results(xpath_ret) == 0) { match = NULL; } xmlXPathFreeObject(xpath_ret); } else { // Only actions that were actually started can match match = NULL; } } } free(xpath); if (match != NULL) { pcmk__debug("Shutdown action %d (%s) found for node %s", match->id, pcmk__xe_get(match->xml, PCMK__XA_OPERATION_KEY), target); } else { pcmk__debug("No reason to expect node %s to be down", target); } return match; } void process_graph_event(xmlNode *event, const char *event_node) { int rc = -1; // Actual result int target_rc = -1; // Expected result int status = -1; // Executor status int callid = -1; // Executor call ID int transition_num = -1; // Transition number int action_num = -1; // Action number within transition char *update_te_uuid = NULL; bool ignore_failures = FALSE; const char *id = NULL; const char *desc = NULL; const char *magic = NULL; const char *uname = NULL; pcmk__assert(event != NULL); /* */ magic = pcmk__xe_get(event, PCMK__XA_TRANSITION_KEY); if (magic == NULL) { /* non-change */ return; } pcmk__xe_get_int(event, PCMK__XA_OP_STATUS, &status); if (status == PCMK_EXEC_PENDING) { return; } id = pcmk__xe_get(event, PCMK__XA_OPERATION_KEY); pcmk__xe_get_int(event, PCMK__XA_RC_CODE, &rc); pcmk__xe_get_int(event, PCMK__XA_CALL_ID, &callid); rc = pcmk__effective_rc(rc); if (decode_transition_key(magic, &update_te_uuid, &transition_num, &action_num, &target_rc) == FALSE) { // decode_transition_key() already logged the bad key pcmk__err("Can't process action %s result: Incompatible versions? " QB_XS " call-id=%d", id, callid); abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, "Bad event", event); return; } if (transition_num == -1) { // E.g. crm_resource --fail if (record_outside_event(action_num) != pcmk_rc_ok) { pcmk__debug("Outside event with transition key '%s' has already " "been processed", magic); goto bail; } desc = "initiated outside of the cluster"; abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, "Unexpected event", event); } else if ((action_num < 0) || !pcmk__str_eq(update_te_uuid, controld_globals.te_uuid, pcmk__str_none)) { desc = "initiated by a different DC"; abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, "Foreign event", event); } else if ((controld_globals.transition_graph->id != transition_num) || controld_globals.transition_graph->complete) { // Action is not from currently active transition guint interval_ms = 0; if (parse_op_key(id, NULL, NULL, &interval_ms) && (interval_ms != 0)) { /* Recurring actions have the transition number they were first * scheduled in. */ if (status == PCMK_EXEC_CANCELLED) { confirm_cancel_action(id, get_node_id(event)); goto bail; } desc = "arrived after initial scheduling"; abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, "Change in recurring result", event); } else if (controld_globals.transition_graph->id != transition_num) { desc = "arrived really late"; abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, "Old event", event); } else { desc = "arrived late"; abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, "Inactive graph", event); } } else { // Event is result of an action from currently active transition pcmk__graph_action_t *action = controld_get_action(action_num); if (action == NULL) { // Should never happen desc = "unknown"; abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart, "Unknown event", event); } else if (pcmk__is_set(action->flags, pcmk__graph_action_confirmed)) { /* Nothing further needs to be done if the action has already been * confirmed. This can happen e.g. when processing both an * "xxx_last_0" or "xxx_last_failure_0" record as well as the main * history record, which would otherwise result in incorrectly * bumping the fail count twice. */ - crm_log_xml_debug(event, "Event already confirmed:"); + pcmk__log_xml_debug(event, "Event already confirmed:"); goto bail; } else { /* An action result needs to be confirmed. * (This is the only case where desc == NULL.) */ if (pcmk__str_eq(crm_meta_value(action->params, PCMK_META_ON_FAIL), PCMK_VALUE_IGNORE, pcmk__str_casei)) { ignore_failures = TRUE; } else if (rc != target_rc) { pcmk__set_graph_action_flags(action, pcmk__graph_action_failed); } stop_te_timer(action); te_action_confirmed(action, controld_globals.transition_graph); if (pcmk__is_set(action->flags, pcmk__graph_action_failed)) { abort_transition(action->synapse->priority + 1, pcmk__graph_restart, "Event failed", event); } } } if (id == NULL) { id = "unknown action"; } uname = pcmk__xe_get(event, PCMK__META_ON_NODE); if (uname == NULL) { uname = "unknown node"; } if (status == PCMK_EXEC_INVALID) { // We couldn't attempt the action pcmk__info("Transition %d action %d (%s on %s): %s", transition_num, action_num, id, uname, pcmk_exec_status_str(status)); } else if (desc && update_failcount(event, event_node, rc, target_rc, (transition_num == -1), FALSE)) { pcmk__notice("Transition %d action %d (%s on %s): expected '%s' but " "got '%s' " QB_XS " target-rc=%d rc=%d call-id=%d event='%s'", transition_num, action_num, id, uname, crm_exit_str(target_rc), crm_exit_str(rc), target_rc, rc, callid, desc); } else if (desc) { pcmk__info("Transition %d action %d (%s on %s): %s " QB_XS " rc=%d target-rc=%d call-id=%d", transition_num, action_num, id, uname, desc, rc, target_rc, callid); } else if (rc == target_rc) { pcmk__info("Transition %d action %d (%s on %s) confirmed: %s " QB_XS " rc=%d call-id=%d", transition_num, action_num, id, uname, crm_exit_str(rc), rc, callid); } else { update_failcount(event, event_node, rc, target_rc, (transition_num == -1), ignore_failures); pcmk__notice("Transition %d action %d (%s on %s): expected '%s' but " "got '%s' " QB_XS " target-rc=%d rc=%d call-id=%d", transition_num, action_num, id, uname, crm_exit_str(target_rc), crm_exit_str(rc), target_rc, rc, callid); } bail: free(update_te_uuid); } diff --git a/daemons/fenced/fenced_remote.c b/daemons/fenced/fenced_remote.c index 0827105dd5..de195daa6f 100644 --- a/daemons/fenced/fenced_remote.c +++ b/daemons/fenced/fenced_remote.c @@ -1,2647 +1,2647 @@ /* * 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 #include #include #include #include #include #include #include #include #include #include #include // xmlNode #include #include #include #include #include #include #include #include #include #include #define TIMEOUT_MULTIPLY_FACTOR 1.2 /* When one fencer queries its peers for devices able to handle a fencing * request, each peer will reply with a list of such devices available to it. * Each reply will be parsed into a peer_device_info_t, with each device's * information kept in a device_properties_t. */ typedef struct device_properties_s { /* Whether access to this device has been verified */ gboolean verified; /* The remaining members are indexed by the operation's "phase" */ /* Whether this device has been executed in each phase */ gboolean executed[st_phase_max]; /* Whether this device is disallowed from executing in each phase */ gboolean disallowed[st_phase_max]; /* Action-specific timeout for each phase */ int custom_action_timeout[st_phase_max]; /* Action-specific maximum random delay for each phase */ int delay_max[st_phase_max]; /* Action-specific base delay for each phase */ int delay_base[st_phase_max]; /* Group of enum st_device_flags */ uint32_t device_support_flags; } device_properties_t; typedef struct { /* Name of peer that sent this result */ char *host; /* Only try peers for non-topology based operations once */ gboolean tried; /* Number of entries in the devices table */ int ndevices; /* Devices available to this host that are capable of fencing the target */ GHashTable *devices; } peer_device_info_t; GHashTable *stonith_remote_op_list = NULL; extern xmlNode *stonith_create_op(int call_id, const char *token, const char *op, xmlNode * data, int call_options); static void request_peer_fencing(remote_fencing_op_t *op, peer_device_info_t *peer); static void finalize_op(remote_fencing_op_t *op, xmlNode *data, bool dup); static void report_timeout_period(remote_fencing_op_t * op, int op_timeout); static int get_op_total_timeout(const remote_fencing_op_t *op, const peer_device_info_t *chosen_peer); static gint sort_strings(gconstpointer a, gconstpointer b) { return strcmp(a, b); } static void free_remote_query(gpointer data) { if (data != NULL) { peer_device_info_t *peer = data; g_hash_table_destroy(peer->devices); free(peer->host); free(peer); } } void free_stonith_remote_op_list(void) { if (stonith_remote_op_list != NULL) { g_hash_table_destroy(stonith_remote_op_list); stonith_remote_op_list = NULL; } } struct peer_count_data { const remote_fencing_op_t *op; gboolean verified_only; uint32_t support_action_only; int count; }; /*! * \internal * \brief Increment a counter if a device has not been executed yet * * \param[in] key Device ID (ignored) * \param[in] value Device properties * \param[in,out] user_data Peer count data */ static void count_peer_device(gpointer key, gpointer value, gpointer user_data) { device_properties_t *props = (device_properties_t*)value; struct peer_count_data *data = user_data; if (!props->executed[data->op->phase] && (!data->verified_only || props->verified) && ((data->support_action_only == st_device_supports_none) || pcmk__is_set(props->device_support_flags, data->support_action_only))) { ++(data->count); } } /*! * \internal * \brief Check the number of available devices in a peer's query results * * \param[in] op Operation that results are for * \param[in] peer Peer to count * \param[in] verified_only Whether to count only verified devices * \param[in] support_action_only Whether to count only devices that support action * * \return Number of devices available to peer that were not already executed */ static int count_peer_devices(const remote_fencing_op_t *op, const peer_device_info_t *peer, gboolean verified_only, uint32_t support_on_action_only) { struct peer_count_data data; data.op = op; data.verified_only = verified_only; data.support_action_only = support_on_action_only; data.count = 0; if (peer) { g_hash_table_foreach(peer->devices, count_peer_device, &data); } return data.count; } /*! * \internal * \brief Search for a device in a query result * * \param[in] op Operation that result is for * \param[in] peer Query result for a peer * \param[in] device Device ID to search for * * \return Device properties if found, NULL otherwise */ static device_properties_t * find_peer_device(const remote_fencing_op_t *op, const peer_device_info_t *peer, const char *device, uint32_t support_action_only) { device_properties_t *props = g_hash_table_lookup(peer->devices, device); if ((props != NULL) && (support_action_only != st_device_supports_none) && !pcmk__is_set(props->device_support_flags, support_action_only)) { return NULL; } return (props && !props->executed[op->phase] && !props->disallowed[op->phase])? props : NULL; } /*! * \internal * \brief Find a device in a peer's device list and mark it as executed * * \param[in] op Operation that peer result is for * \param[in,out] peer Peer with results to search * \param[in] device ID of device to mark as done * \param[in] verified_devices_only Only consider verified devices * * \return TRUE if device was found and marked, FALSE otherwise */ static gboolean grab_peer_device(const remote_fencing_op_t *op, peer_device_info_t *peer, const char *device, gboolean verified_devices_only) { device_properties_t *props = find_peer_device(op, peer, device, fenced_support_flag(op->action)); if ((props == NULL) || (verified_devices_only && !props->verified)) { return FALSE; } pcmk__trace("Removing %s from %s (%d remaining)", device, peer->host, count_peer_devices(op, peer, FALSE, st_device_supports_none)); props->executed[op->phase] = TRUE; return TRUE; } static void clear_remote_op_timers(remote_fencing_op_t * op) { if (op->query_timer) { g_source_remove(op->query_timer); op->query_timer = 0; } if (op->op_timer_total) { g_source_remove(op->op_timer_total); op->op_timer_total = 0; } if (op->op_timer_one) { g_source_remove(op->op_timer_one); op->op_timer_one = 0; } } static void free_remote_op(gpointer data) { remote_fencing_op_t *op = data; - crm_log_xml_debug(op->request, "Destroying"); + pcmk__log_xml_debug(op->request, "Destroying"); clear_remote_op_timers(op); free(op->id); free(op->action); free(op->delegate); free(op->target); free(op->client_id); free(op->client_name); free(op->originator); if (op->query_results) { g_list_free_full(op->query_results, free_remote_query); } if (op->request) { pcmk__xml_free(op->request); op->request = NULL; } if (op->devices_list) { g_list_free_full(op->devices_list, free); op->devices_list = NULL; } g_list_free_full(op->automatic_list, free); g_list_free(op->duplicates); pcmk__reset_result(&op->result); free(op); } void init_stonith_remote_op_hash_table(GHashTable **table) { if (*table == NULL) { *table = pcmk__strkey_table(NULL, free_remote_op); } } /*! * \internal * \brief Return an operation's originally requested action (before any remap) * * \param[in] op Operation to check * * \return Operation's original action */ static const char * op_requested_action(const remote_fencing_op_t *op) { return ((op->phase > st_phase_requested)? PCMK_ACTION_REBOOT : op->action); } /*! * \internal * \brief Remap a "reboot" operation to the "off" phase * * \param[in,out] op Operation to remap */ static void op_phase_off(remote_fencing_op_t *op) { pcmk__info("Remapping multiple-device reboot targeting %s to 'off' " QB_XS " id=%.8s", op->target, op->id); op->phase = st_phase_off; /* Happily, "off" and "on" are shorter than "reboot", so we can reuse the * memory allocation at each phase. */ strcpy(op->action, PCMK_ACTION_OFF); } /*! * \internal * \brief Advance a remapped reboot operation to the "on" phase * * \param[in,out] op Operation to remap */ static void op_phase_on(remote_fencing_op_t *op) { GList *iter = NULL; pcmk__info("Remapped 'off' targeting %s complete, remapping to 'on' for " "%s " QB_XS " id=%.8s", op->target, op->client_name, op->id); op->phase = st_phase_on; strcpy(op->action, PCMK_ACTION_ON); /* Skip devices with automatic unfencing, because the cluster will handle it * when the node rejoins. */ for (iter = op->automatic_list; iter != NULL; iter = iter->next) { GList *match = g_list_find_custom(op->devices_list, iter->data, sort_strings); if (match) { op->devices_list = g_list_remove(op->devices_list, match->data); } } g_list_free_full(op->automatic_list, free); op->automatic_list = NULL; /* Rewind device list pointer */ op->devices = op->devices_list; } /*! * \internal * \brief Reset a remapped reboot operation * * \param[in,out] op Operation to reset */ static void undo_op_remap(remote_fencing_op_t *op) { if (op->phase > 0) { pcmk__info("Undoing remap of reboot targeting %s for %s " QB_XS " id=%.8s", op->target, op->client_name, op->id); op->phase = st_phase_requested; strcpy(op->action, PCMK_ACTION_REBOOT); } } /*! * \internal * \brief Create notification data XML for a fencing operation result * * \param[in,out] parent Parent XML element for newly created element * \param[in] op Fencer operation that completed * * \return Newly created XML to add as notification data * \note The caller is responsible for freeing the result. */ static xmlNode * fencing_result2xml(xmlNode *parent, const remote_fencing_op_t *op) { xmlNode *notify_data = pcmk__xe_create(parent, PCMK__XE_ST_NOTIFY_FENCE); pcmk__xe_set_int(notify_data, PCMK_XA_STATE, op->state); pcmk__xe_set(notify_data, PCMK__XA_ST_TARGET, op->target); pcmk__xe_set(notify_data, PCMK__XA_ST_DEVICE_ACTION, op->action); pcmk__xe_set(notify_data, PCMK__XA_ST_DELEGATE, op->delegate); pcmk__xe_set(notify_data, PCMK__XA_ST_REMOTE_OP, op->id); pcmk__xe_set(notify_data, PCMK__XA_ST_ORIGIN, op->originator); pcmk__xe_set(notify_data, PCMK__XA_ST_CLIENTID, op->client_id); pcmk__xe_set(notify_data, PCMK__XA_ST_CLIENTNAME, op->client_name); return notify_data; } /*! * \internal * \brief Broadcast a fence result notification to all CPG peers * * \param[in] op Fencer operation that completed * \param[in] op_merged Whether this operation is a duplicate of another */ void fenced_broadcast_op_result(const remote_fencing_op_t *op, bool op_merged) { static int count = 0; xmlNode *bcast = pcmk__xe_create(NULL, PCMK__XE_ST_REPLY); xmlNode *wrapper = NULL; xmlNode *notify_data = NULL; count++; pcmk__trace("Broadcasting result to peers"); pcmk__xe_set(bcast, PCMK__XA_T, PCMK__VALUE_ST_NOTIFY); pcmk__xe_set(bcast, PCMK__XA_SUBT, PCMK__VALUE_BROADCAST); pcmk__xe_set(bcast, PCMK__XA_ST_OP, STONITH_OP_NOTIFY); pcmk__xe_set_int(bcast, PCMK_XA_COUNT, count); if (op_merged) { pcmk__xe_set_bool_attr(bcast, PCMK__XA_ST_OP_MERGED, true); } wrapper = pcmk__xe_create(bcast, PCMK__XE_ST_CALLDATA); notify_data = fencing_result2xml(wrapper, op); stonith__xe_set_result(notify_data, &op->result); pcmk__cluster_send_message(NULL, pcmk_ipc_fenced, bcast); pcmk__xml_free(bcast); return; } /*! * \internal * \brief Reply to a local request originator and notify all subscribed clients * * \param[in,out] op Fencer operation that completed * \param[in,out] data Top-level XML to add notification to */ static void handle_local_reply_and_notify(remote_fencing_op_t *op, xmlNode *data) { xmlNode *notify_data = NULL; xmlNode *reply = NULL; pcmk__client_t *client = NULL; if (op->notify_sent == TRUE) { /* nothing to do */ return; } /* Do notification with a clean data object */ pcmk__xe_set_int(data, PCMK_XA_STATE, op->state); pcmk__xe_set(data, PCMK__XA_ST_TARGET, op->target); pcmk__xe_set(data, PCMK__XA_ST_OP, op->action); reply = fenced_construct_reply(op->request, data, &op->result); pcmk__xe_set(reply, PCMK__XA_ST_DELEGATE, op->delegate); /* Send fencing OP reply to local client that initiated fencing */ client = pcmk__find_client_by_id(op->client_id); if (client == NULL) { pcmk__trace("Skipping reply to %s: no longer a client", op->client_id); } else { do_local_reply(reply, client, op->call_options); } /* bcast to all local clients that the fencing operation happend */ notify_data = fencing_result2xml(NULL, op); fenced_send_notification(PCMK__VALUE_ST_NOTIFY_FENCE, &op->result, notify_data); pcmk__xml_free(notify_data); fenced_send_notification(PCMK__VALUE_ST_NOTIFY_HISTORY, NULL, NULL); /* mark this op as having notify's already sent */ op->notify_sent = TRUE; pcmk__xml_free(reply); } /*! * \internal * \brief Finalize all duplicates of a given fencer operation * * \param[in,out] op Fencer operation that completed * \param[in,out] data Top-level XML to add notification to */ static void finalize_op_duplicates(remote_fencing_op_t *op, xmlNode *data) { for (GList *iter = op->duplicates; iter != NULL; iter = iter->next) { remote_fencing_op_t *other = iter->data; if (other->state == st_duplicate) { other->state = op->state; pcmk__debug("Performing duplicate notification for %s@%s: %s " QB_XS " id=%.8s", other->client_name, other->originator, pcmk_exec_status_str(op->result.execution_status), other->id); pcmk__copy_result(&op->result, &other->result); finalize_op(other, data, true); } else { // Possible if (for example) it timed out already pcmk__err("Skipping duplicate notification for %s@%s " QB_XS " state=%s id=%.8s", other->client_name, other->originator, stonith_op_state_str(other->state), other->id); } } } static char * delegate_from_xml(xmlNode *xml) { xmlNode *match = pcmk__xpath_find_one(xml->doc, "//*[@" PCMK__XA_ST_DELEGATE "]", PCMK__LOG_NEVER); if (match == NULL) { return pcmk__xe_get_copy(xml, PCMK__XA_SRC); } else { return pcmk__xe_get_copy(match, PCMK__XA_ST_DELEGATE); } } /*! * \internal * \brief Finalize a peer fencing operation * * Clean up after a fencing operation completes. This function has two code * paths: the executioner uses it to broadcast the result to CPG peers, and then * each peer (including the executioner) uses it to process that broadcast and * notify its IPC clients of the result. * * \param[in,out] op Fencer operation that completed * \param[in,out] data If not NULL, XML reply of last delegated operation * \param[in] dup Whether this operation is a duplicate of another * (in which case, do not broadcast the result) * * \note The operation result should be set before calling this function. */ static void finalize_op(remote_fencing_op_t *op, xmlNode *data, bool dup) { int level = LOG_ERR; const char *subt = NULL; xmlNode *local_data = NULL; gboolean op_merged = FALSE; CRM_CHECK((op != NULL), return); // This is a no-op if timers have already been cleared clear_remote_op_timers(op); if (op->notify_sent) { // Most likely, this is a timed-out action that eventually completed pcmk__notice("Operation '%s'%s%s by %s for %s@%s%s: Result arrive too " "late " QB_XS " id=%.8s", op->action, (op->target? " targeting " : ""), pcmk__s(op->target, ""), pcmk__s(op->delegate, "unknown node"), op->client_name, op->originator, (op_merged? " (merged)" : ""), op->id); return; } set_fencing_completed(op); undo_op_remap(op); if (data == NULL) { data = pcmk__xe_create(NULL, "remote-op"); local_data = data; } else if (op->delegate == NULL) { switch (op->result.execution_status) { case PCMK_EXEC_NO_FENCE_DEVICE: break; case PCMK_EXEC_INVALID: if (op->result.exit_status != CRM_EX_EXPIRED) { op->delegate = delegate_from_xml(data); } break; default: op->delegate = delegate_from_xml(data); break; } } if (dup || (pcmk__xe_get(data, PCMK__XA_ST_OP_MERGED) != NULL)) { op_merged = true; } /* Tell everyone the operation is done, we will continue * with doing the local notifications once we receive * the broadcast back. */ subt = pcmk__xe_get(data, PCMK__XA_SUBT); if (!dup && !pcmk__str_eq(subt, PCMK__VALUE_BROADCAST, pcmk__str_none)) { /* Defer notification until the bcast message arrives */ fenced_broadcast_op_result(op, op_merged); pcmk__xml_free(local_data); return; } if (pcmk__result_ok(&op->result) || dup || !pcmk__str_eq(op->originator, fenced_get_local_node(), pcmk__str_casei)) { level = LOG_NOTICE; } do_crm_log(level, "Operation '%s'%s%s by %s for %s@%s%s: %s (%s%s%s) " QB_XS " id=%.8s", op->action, (op->target? " targeting " : ""), (op->target? op->target : ""), (op->delegate? op->delegate : "unknown node"), op->client_name, op->originator, (op_merged? " (merged)" : ""), crm_exit_str(op->result.exit_status), pcmk_exec_status_str(op->result.execution_status), ((op->result.exit_reason == NULL)? "" : ": "), ((op->result.exit_reason == NULL)? "" : op->result.exit_reason), op->id); handle_local_reply_and_notify(op, data); if (!dup) { finalize_op_duplicates(op, data); } /* Free non-essential parts of the record * Keep the record around so we can query the history */ if (op->query_results) { g_list_free_full(op->query_results, free_remote_query); op->query_results = NULL; } if (op->request) { pcmk__xml_free(op->request); op->request = NULL; } pcmk__xml_free(local_data); } /*! * \internal * \brief Finalize a watchdog fencer op after the waiting time expires * * \param[in,out] userdata Fencer operation that completed * * \return G_SOURCE_REMOVE (which tells glib not to restart timer) */ static gboolean remote_op_watchdog_done(gpointer userdata) { remote_fencing_op_t *op = userdata; op->op_timer_one = 0; pcmk__notice("Self-fencing (%s) by %s for %s assumed complete " QB_XS " id=%.8s", op->action, op->target, op->client_name, op->id); op->state = st_done; pcmk__set_result(&op->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); finalize_op(op, NULL, false); return G_SOURCE_REMOVE; } static gboolean remote_op_timeout_one(gpointer userdata) { remote_fencing_op_t *op = userdata; op->op_timer_one = 0; pcmk__notice("Peer's '%s' action targeting %s for client %s timed out " QB_XS " id=%.8s", op->action, op->target, op->client_name, op->id); pcmk__set_result(&op->result, CRM_EX_ERROR, PCMK_EXEC_TIMEOUT, "Peer did not return fence result within timeout"); // The requested delay has been applied for the first device if (op->client_delay > 0) { op->client_delay = 0; pcmk__trace("Try another device for '%s' action targeting %s for " "client %s without delay " QB_XS " id=%.8s", op->action, op->target, op->client_name, op->id); } // Try another device, if appropriate request_peer_fencing(op, NULL); return G_SOURCE_REMOVE; } /*! * \internal * \brief Finalize a remote fencer operation that timed out * * \param[in,out] op Fencer operation that timed out * \param[in] reason Readable description of what step timed out */ static void finalize_timed_out_op(remote_fencing_op_t *op, const char *reason) { pcmk__debug("Action '%s' targeting %s for client %s timed out " QB_XS " id=%.8s", op->action, op->target, op->client_name, op->id); if (op->phase == st_phase_on) { /* A remapped reboot operation timed out in the "on" phase, but the * "off" phase completed successfully, so quit trying any further * devices, and return success. */ op->state = st_done; pcmk__set_result(&op->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); } else { op->state = st_failed; pcmk__set_result(&op->result, CRM_EX_ERROR, PCMK_EXEC_TIMEOUT, reason); } finalize_op(op, NULL, false); } /*! * \internal * \brief Finalize a remote fencer operation that timed out * * \param[in,out] userdata Fencer operation that timed out * * \return G_SOURCE_REMOVE (which tells glib not to restart timer) */ static gboolean remote_op_timeout(gpointer userdata) { remote_fencing_op_t *op = userdata; op->op_timer_total = 0; if (op->state == st_done) { pcmk__debug("Action '%s' targeting %s for client %s already completed " QB_XS " id=%.8s", op->action, op->target, op->client_name, op->id); } else { finalize_timed_out_op(userdata, "Fencing did not complete within a " "total timeout based on the " "configured timeout and retries for " "any devices attempted"); } return G_SOURCE_REMOVE; } static gboolean remote_op_query_timeout(gpointer data) { remote_fencing_op_t *op = data; op->query_timer = 0; if (op->state == st_done) { pcmk__debug("Operation %.8s targeting %s already completed", op->id, op->target); } else if (op->state == st_exec) { pcmk__debug("Operation %.8s targeting %s already in progress", op->id, op->target); } else if (op->query_results) { // Query succeeded, so attempt the actual fencing pcmk__debug("Query %.8s targeting %s complete (state=%s)", op->id, op->target, stonith_op_state_str(op->state)); request_peer_fencing(op, NULL); } else { pcmk__debug("Query %.8s targeting %s timed out (state=%s)", op->id, op->target, stonith_op_state_str(op->state)); finalize_timed_out_op(op, "No capable peers replied to device query " "within timeout"); } return G_SOURCE_REMOVE; } static gboolean topology_is_empty(stonith_topology_t *tp) { int i; if (tp == NULL) { return TRUE; } for (i = 0; i < ST__LEVEL_COUNT; i++) { if (tp->levels[i] != NULL) { return FALSE; } } return TRUE; } /*! * \internal * \brief Add a device to an operation's automatic unfencing list * * \param[in,out] op Operation to modify * \param[in] device Device ID to add */ static void add_required_device(remote_fencing_op_t *op, const char *device) { GList *match = g_list_find_custom(op->automatic_list, device, sort_strings); if (!match) { op->automatic_list = g_list_prepend(op->automatic_list, pcmk__str_copy(device)); } } /*! * \internal * \brief Remove a device from the automatic unfencing list * * \param[in,out] op Operation to modify * \param[in] device Device ID to remove */ static void remove_required_device(remote_fencing_op_t *op, const char *device) { GList *match = g_list_find_custom(op->automatic_list, device, sort_strings); if (match) { op->automatic_list = g_list_remove(op->automatic_list, match->data); } } /* deep copy the device list */ static void set_op_device_list(remote_fencing_op_t * op, GList *devices) { GList *lpc = NULL; if (op->devices_list) { g_list_free_full(op->devices_list, free); op->devices_list = NULL; } for (lpc = devices; lpc != NULL; lpc = lpc->next) { const char *device = lpc->data; op->devices_list = g_list_append(op->devices_list, pcmk__str_copy(device)); } op->devices = op->devices_list; } /*! * \internal * \brief Check whether a node matches a topology target * * \param[in] tp Topology table entry to check * \param[in] node Name of node to check * * \return TRUE if node matches topology target */ static gboolean topology_matches(const stonith_topology_t *tp, const char *node) { regex_t r_patt; CRM_CHECK(node && tp && tp->target, return FALSE); switch (tp->kind) { case fenced_target_by_attribute: /* This level targets by attribute, so tp->target is a NAME=VALUE pair * of a permanent attribute applied to targeted nodes. The test below * relies on the locally cached copy of the CIB, so if fencing needs to * be done before the initial CIB is received or after a malformed CIB * is received, then the topology will be unable to be used. */ if (node_has_attr(node, tp->target_attribute, tp->target_value)) { pcmk__notice("Matched %s with %s by attribute", node, tp->target); return TRUE; } break; case fenced_target_by_pattern: /* This level targets node names matching a pattern, so tp->target * (and tp->target_pattern) is a regular expression. */ if (regcomp(&r_patt, tp->target_pattern, REG_EXTENDED|REG_NOSUB)) { pcmk__info("Bad regex '%s' for fencing level", tp->target); } else { int status = regexec(&r_patt, node, 0, NULL, 0); regfree(&r_patt); if (status == 0) { pcmk__notice("Matched %s with %s by name", node, tp->target); return TRUE; } } break; case fenced_target_by_name: pcmk__trace("Testing %s against %s", node, tp->target); return pcmk__str_eq(tp->target, node, pcmk__str_casei); default: break; } pcmk__trace("No match for %s with %s", node, tp->target); return FALSE; } stonith_topology_t * find_topology_for_host(const char *host) { GHashTableIter tIter; stonith_topology_t *tp = g_hash_table_lookup(topology, host); if(tp != NULL) { pcmk__trace("Found %s for %s in %u entries", tp->target, host, g_hash_table_size(topology)); return tp; } g_hash_table_iter_init(&tIter, topology); while (g_hash_table_iter_next(&tIter, NULL, (gpointer *) & tp)) { if (topology_matches(tp, host)) { pcmk__trace("Found %s for %s in %u entries", tp->target, host, g_hash_table_size(topology)); return tp; } } pcmk__trace("No matches for %s in %u topology entries", host, g_hash_table_size(topology)); return NULL; } /*! * \internal * \brief Set fencing operation's device list to target's next topology level * * \param[in,out] op Remote fencing operation to modify * \param[in] empty_ok If true, an operation without a target (i.e. * queries) or a target without a topology will get a * pcmk_rc_ok return value instead of ENODEV * * \return Standard Pacemaker return value */ static int advance_topology_level(remote_fencing_op_t *op, bool empty_ok) { stonith_topology_t *tp = NULL; if (op->target) { tp = find_topology_for_host(op->target); } if (topology_is_empty(tp)) { return empty_ok? pcmk_rc_ok : ENODEV; } pcmk__assert(tp->levels != NULL); stonith__set_call_options(op->call_options, op->id, st_opt_topology); /* This is a new level, so undo any remapping left over from previous */ undo_op_remap(op); do { op->level++; } while (op->level < ST__LEVEL_COUNT && tp->levels[op->level] == NULL); if (op->level < ST__LEVEL_COUNT) { pcmk__trace("Attempting fencing level %d targeting %s (%d devices) for " "client %s@%s (id=%.8s)", op->level, op->target, g_list_length(tp->levels[op->level]), op->client_name, op->originator, op->id); set_op_device_list(op, tp->levels[op->level]); // The requested delay has been applied for the first fencing level if ((op->level > 1) && (op->client_delay > 0)) { op->client_delay = 0; } if ((g_list_next(op->devices_list) != NULL) && pcmk__str_eq(op->action, PCMK_ACTION_REBOOT, pcmk__str_none)) { /* A reboot has been requested for a topology level with multiple * devices. Instead of rebooting the devices sequentially, we will * turn them all off, then turn them all on again. (Think about * switched power outlets for redundant power supplies.) */ op_phase_off(op); } return pcmk_rc_ok; } pcmk__info("All %sfencing options targeting %s for client %s@%s failed " QB_XS " id=%.8s", ((stonith_watchdog_timeout_ms > 0)? "non-watchdog " : ""), op->target, op->client_name, op->originator, op->id); return ENODEV; } /*! * \internal * \brief If fencing operation is a duplicate, merge it into the other one * * \param[in,out] op Fencing operation to check */ static void merge_duplicates(remote_fencing_op_t *op) { GHashTableIter iter; remote_fencing_op_t *other = NULL; time_t now = time(NULL); g_hash_table_iter_init(&iter, stonith_remote_op_list); while (g_hash_table_iter_next(&iter, NULL, (void **)&other)) { const char *other_action = op_requested_action(other); pcmk__node_status_t *node = NULL; if (!strcmp(op->id, other->id)) { continue; // Don't compare against self } if (other->state > st_exec) { pcmk__trace("%.8s not duplicate of %.8s: not in progress", op->id, other->id); continue; } if (!pcmk__str_eq(op->target, other->target, pcmk__str_casei)) { pcmk__trace("%.8s not duplicate of %.8s: node %s vs. %s", op->id, other->id, op->target, other->target); continue; } if (!pcmk__str_eq(op->action, other_action, pcmk__str_none)) { pcmk__trace("%.8s not duplicate of %.8s: action %s vs. %s", op->id, other->id, op->action, other_action); continue; } if (pcmk__str_eq(op->client_name, other->client_name, pcmk__str_casei)) { pcmk__trace("%.8s not duplicate of %.8s: same client %s", op->id, other->id, op->client_name); continue; } if (pcmk__str_eq(other->target, other->originator, pcmk__str_casei)) { pcmk__trace("%.8s not duplicate of %.8s: self-fencing for %s", op->id, other->id, other->target); continue; } node = pcmk__get_node(0, other->originator, NULL, pcmk__node_search_cluster_member); if (!fencing_peer_active(node)) { pcmk__notice("Failing action '%s' targeting %s originating from " "client %s@%s: Originator is dead " QB_XS " id=%.8s", other->action, other->target, other->client_name, other->originator, other->id); pcmk__trace("%.8s not duplicate of %.8s: originator dead", op->id, other->id); other->state = st_failed; continue; } if ((other->total_timeout > 0) && (now > (other->total_timeout + other->created))) { pcmk__trace("%.8s not duplicate of %.8s: old (%lld vs. %lld + %ds)", op->id, other->id, (long long) now, (long long) other->created, other->total_timeout); continue; } /* There is another in-flight request to fence the same host * Piggyback on that instead. If it fails, so do we. */ other->duplicates = g_list_append(other->duplicates, op); if (other->total_timeout == 0) { other->total_timeout = op->total_timeout = TIMEOUT_MULTIPLY_FACTOR * get_op_total_timeout(op, NULL); pcmk__trace("Best guess as to timeout used for %.8s: %ds", other->id, other->total_timeout); } pcmk__notice("Merging fencing action '%s' targeting %s originating " "from client %s with identical request from %s@%s " QB_XS " original=%.8s duplicate=%.8s total_timeout=%ds", op->action, op->target, op->client_name, other->client_name, other->originator, op->id, other->id, other->total_timeout); report_timeout_period(op, other->total_timeout); op->state = st_duplicate; } } static uint32_t fencing_active_peers(void) { uint32_t count = 0; pcmk__node_status_t *entry = NULL; GHashTableIter gIter; g_hash_table_iter_init(&gIter, pcmk__peer_cache); while (g_hash_table_iter_next(&gIter, NULL, (void **)&entry)) { if(fencing_peer_active(entry)) { count++; } } return count; } /*! * \internal * \brief Process a manual confirmation of a pending fence action * * \param[in] client IPC client that sent confirmation * \param[in,out] msg Request XML with manual confirmation * * \return Standard Pacemaker return code */ int fenced_handle_manual_confirmation(const pcmk__client_t *client, xmlNode *msg) { remote_fencing_op_t *op = NULL; xmlNode *dev = pcmk__xpath_find_one(msg->doc, "//*[@" PCMK__XA_ST_TARGET "]", LOG_ERR); CRM_CHECK(dev != NULL, return EPROTO); pcmk__notice("Received manual confirmation that %s has been fenced", pcmk__s(pcmk__xe_get(dev, PCMK__XA_ST_TARGET), "unknown target")); op = initiate_remote_stonith_op(client, msg, TRUE); if (op == NULL) { return EPROTO; } op->state = st_done; op->delegate = pcmk__str_copy("a human"); // For the fencer's purposes, the fencing operation is done pcmk__set_result(&op->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); finalize_op(op, msg, false); /* For the requester's purposes, the operation is still pending. The * actual result will be sent asynchronously via the operation's done_cb(). */ return EINPROGRESS; } /*! * \internal * \brief Create a new remote stonith operation * * \param[in] client ID of local stonith client that initiated the operation * \param[in] request The request from the client that started the operation * \param[in] peer TRUE if this operation is owned by another stonith peer * (an operation owned by one peer is stored on all peers, * but only the owner executes it; all nodes get the results * once the owner finishes execution) */ void * create_remote_stonith_op(const char *client, xmlNode *request, gboolean peer) { remote_fencing_op_t *op = NULL; xmlNode *dev = pcmk__xpath_find_one(request->doc, "//*[@" PCMK__XA_ST_TARGET "]", PCMK__LOG_NEVER); int rc = pcmk_rc_ok; const char *operation = NULL; init_stonith_remote_op_hash_table(&stonith_remote_op_list); /* If this operation is owned by another node, check to make * sure we haven't already created this operation. */ if (peer && dev) { const char *op_id = pcmk__xe_get(dev, PCMK__XA_ST_REMOTE_OP); CRM_CHECK(op_id != NULL, return NULL); op = g_hash_table_lookup(stonith_remote_op_list, op_id); if (op) { pcmk__debug("Reusing existing remote fencing op %.8s for %s", op_id, pcmk__s(client, "unknown client")); return op; } } op = pcmk__assert_alloc(1, sizeof(remote_fencing_op_t)); pcmk__xe_get_int(request, PCMK__XA_ST_TIMEOUT, &(op->base_timeout)); // Value -1 means disable any static/random fencing delays pcmk__xe_get_int(request, PCMK__XA_ST_DELAY, &(op->client_delay)); if (peer && dev) { op->id = pcmk__xe_get_copy(dev, PCMK__XA_ST_REMOTE_OP); } else { op->id = pcmk__generate_uuid(); } g_hash_table_replace(stonith_remote_op_list, op->id, op); op->state = st_query; op->replies_expected = fencing_active_peers(); op->action = pcmk__xe_get_copy(dev, PCMK__XA_ST_DEVICE_ACTION); /* The node initiating the stonith operation. If an operation is relayed, * this is the last node the operation lands on. When in standalone mode, * origin is the ID of the client that originated the operation. * * Or may be the name of the function that created the operation. */ op->originator = pcmk__xe_get_copy(dev, PCMK__XA_ST_ORIGIN); if (op->originator == NULL) { /* Local or relayed request */ op->originator = pcmk__str_copy(fenced_get_local_node()); } // Delegate may not be set op->delegate = pcmk__xe_get_copy(dev, PCMK__XA_ST_DELEGATE); op->created = time(NULL); CRM_LOG_ASSERT(client != NULL); op->client_id = pcmk__str_copy(client); /* For a RELAY operation, set fenced on the client. */ operation = pcmk__xe_get(request, PCMK__XA_ST_OP); if (pcmk__str_eq(operation, STONITH_OP_RELAY, pcmk__str_none)) { op->client_name = pcmk__assert_asprintf("%s.%lu", crm_system_name, (unsigned long) getpid()); } else { op->client_name = pcmk__xe_get_copy(request, PCMK__XA_ST_CLIENTNAME); } op->target = pcmk__xe_get_copy(dev, PCMK__XA_ST_TARGET); // @TODO Figure out how to avoid copying XML here op->request = pcmk__xml_copy(NULL, request); rc = pcmk__xe_get_flags(request, PCMK__XA_ST_CALLOPT, &(op->call_options), 0U); if (rc != pcmk_rc_ok) { pcmk__warn("Couldn't parse options from request %s: %s", op->id, pcmk_rc_str(rc)); } pcmk__xe_get_int(request, PCMK__XA_ST_CALLID, &(op->client_callid)); pcmk__trace("%s new fencing op %s ('%s' targeting %s for client %s, base " "timeout %ds, %u %s expected)", ((peer && (dev != NULL))? "Recorded" : "Generated"), op->id, op->action, op->target, op->client_name, op->base_timeout, op->replies_expected, pcmk__plural_alt(op->replies_expected, "reply", "replies")); if (op->call_options & st_opt_cs_nodeid) { int nodeid; pcmk__node_status_t *node = NULL; pcmk__scan_min_int(op->target, &nodeid, 0); node = pcmk__search_node_caches(nodeid, NULL, NULL, pcmk__node_search_any |pcmk__node_search_cluster_cib); /* Ensure the conversion only happens once */ stonith__clear_call_options(op->call_options, op->id, st_opt_cs_nodeid); if ((node != NULL) && (node->name != NULL)) { pcmk__str_update(&(op->target), node->name); } else { pcmk__warn("Could not expand nodeid '%s' into a host name", op->target); } } /* check to see if this is a duplicate operation of another in-flight operation */ merge_duplicates(op); if (op->state != st_duplicate) { /* kick history readers */ fenced_send_notification(PCMK__VALUE_ST_NOTIFY_HISTORY, NULL, NULL); } /* safe to trim as long as that doesn't touch pending ops */ stonith_fence_history_trim(); return op; } /*! * \internal * \brief Create a peer fencing operation from a request, and initiate it * * \param[in] client IPC client that made request (NULL to get from request) * \param[in] request Request XML * \param[in] manual_ack Whether this is a manual action confirmation * * \return Newly created operation on success, otherwise NULL */ remote_fencing_op_t * initiate_remote_stonith_op(const pcmk__client_t *client, xmlNode *request, gboolean manual_ack) { int query_timeout = 0; xmlNode *query = NULL; const char *client_id = NULL; remote_fencing_op_t *op = NULL; const char *relay_op_id = NULL; const char *operation = NULL; if (client) { client_id = client->id; } else { client_id = pcmk__xe_get(request, PCMK__XA_ST_CLIENTID); } CRM_LOG_ASSERT(client_id != NULL); op = create_remote_stonith_op(client_id, request, FALSE); op->owner = TRUE; if (manual_ack) { return op; } CRM_CHECK(op->action, return NULL); if (advance_topology_level(op, true) != pcmk_rc_ok) { op->state = st_failed; } switch (op->state) { case st_failed: // advance_topology_level() exhausted levels pcmk__set_result(&op->result, CRM_EX_ERROR, PCMK_EXEC_ERROR, "All topology levels failed"); pcmk__warn("Could not request peer fencing (%s) targeting %s " QB_XS " id=%.8s", op->action, op->target, op->id); finalize_op(op, NULL, false); return op; case st_duplicate: pcmk__info("Requesting peer fencing (%s) targeting %s (duplicate) " QB_XS " id=%.8s", op->action, op->target, op->id); return op; default: pcmk__notice("Requesting peer fencing (%s) targeting %s " QB_XS " id=%.8s state=%s base_timeout=%ds", op->action, op->target, op->id, stonith_op_state_str(op->state), op->base_timeout); } query = stonith_create_op(op->client_callid, op->id, STONITH_OP_QUERY, NULL, op->call_options); pcmk__xe_set(query, PCMK__XA_ST_REMOTE_OP, op->id); pcmk__xe_set(query, PCMK__XA_ST_TARGET, op->target); pcmk__xe_set(query, PCMK__XA_ST_DEVICE_ACTION, op_requested_action(op)); pcmk__xe_set(query, PCMK__XA_ST_ORIGIN, op->originator); pcmk__xe_set(query, PCMK__XA_ST_CLIENTID, op->client_id); pcmk__xe_set(query, PCMK__XA_ST_CLIENTNAME, op->client_name); pcmk__xe_set_int(query, PCMK__XA_ST_TIMEOUT, op->base_timeout); /* In case of RELAY operation, RELAY information is added to the query to delete the original operation of RELAY. */ operation = pcmk__xe_get(request, PCMK__XA_ST_OP); if (pcmk__str_eq(operation, STONITH_OP_RELAY, pcmk__str_none)) { relay_op_id = pcmk__xe_get(request, PCMK__XA_ST_REMOTE_OP); if (relay_op_id) { pcmk__xe_set(query, PCMK__XA_ST_REMOTE_OP_RELAY, relay_op_id); } } pcmk__cluster_send_message(NULL, pcmk_ipc_fenced, query); pcmk__xml_free(query); query_timeout = op->base_timeout * TIMEOUT_MULTIPLY_FACTOR; op->query_timer = pcmk__create_timer((1000 * query_timeout), remote_op_query_timeout, op); return op; } enum find_best_peer_options { /*! Skip checking the target peer for capable fencing devices */ FIND_PEER_SKIP_TARGET = 0x0001, /*! Only check the target peer for capable fencing devices */ FIND_PEER_TARGET_ONLY = 0x0002, /*! Skip peers and devices that are not verified */ FIND_PEER_VERIFIED_ONLY = 0x0004, }; static bool is_watchdog_fencing(const remote_fencing_op_t *op, const char *device) { return (stonith_watchdog_timeout_ms > 0 // Only an explicit mismatch is considered not a watchdog fencing. && pcmk__str_eq(device, STONITH_WATCHDOG_ID, pcmk__str_null_matches) && pcmk__is_fencing_action(op->action) && node_does_watchdog_fencing(op->target)); } static peer_device_info_t * find_best_peer(const char *device, remote_fencing_op_t * op, enum find_best_peer_options options) { GList *iter = NULL; gboolean verified_devices_only = (options & FIND_PEER_VERIFIED_ONLY) ? TRUE : FALSE; if ((device == NULL) && pcmk__is_set(op->call_options, st_opt_topology)) { return NULL; } for (iter = op->query_results; iter != NULL; iter = iter->next) { peer_device_info_t *peer = iter->data; pcmk__trace("Testing result from %s targeting %s with %d device%s: %d " "%x", peer->host, op->target, peer->ndevices, pcmk__plural_s(peer->ndevices), peer->tried, options); if ((options & FIND_PEER_SKIP_TARGET) && pcmk__str_eq(peer->host, op->target, pcmk__str_casei)) { continue; } if ((options & FIND_PEER_TARGET_ONLY) && !pcmk__str_eq(peer->host, op->target, pcmk__str_casei)) { continue; } if (pcmk__is_set(op->call_options, st_opt_topology)) { if (grab_peer_device(op, peer, device, verified_devices_only)) { return peer; } } else if (!peer->tried && count_peer_devices(op, peer, verified_devices_only, fenced_support_flag(op->action))) { /* No topology: Use the current best peer */ pcmk__trace("Simple fencing"); return peer; } } return NULL; } static peer_device_info_t * stonith_choose_peer(remote_fencing_op_t * op) { const char *device = NULL; peer_device_info_t *peer = NULL; uint32_t active = fencing_active_peers(); do { if (op->devices) { device = op->devices->data; pcmk__trace("Checking for someone to fence (%s) %s using %s", op->action, op->target, device); } else { pcmk__trace("Checking for someone to fence (%s) %s", op->action, op->target); } /* Best choice is a peer other than the target with verified access */ peer = find_best_peer(device, op, FIND_PEER_SKIP_TARGET|FIND_PEER_VERIFIED_ONLY); if (peer) { pcmk__trace("Found verified peer %s for %s", peer->host, pcmk__s(device, "")); return peer; } if(op->query_timer != 0 && op->replies < QB_MIN(op->replies_expected, active)) { pcmk__trace("Waiting before looking for unverified devices to " "fence %s", op->target); return NULL; } /* If no other peer has verified access, next best is unverified access */ peer = find_best_peer(device, op, FIND_PEER_SKIP_TARGET); if (peer) { pcmk__trace("Found best unverified peer %s", peer->host); return peer; } /* If no other peer can do it, last option is self-fencing * (which is never allowed for the "on" phase of a remapped reboot) */ if (op->phase != st_phase_on) { peer = find_best_peer(device, op, FIND_PEER_TARGET_ONLY); if (peer) { pcmk__trace("%s will fence itself", peer->host); return peer; } } /* Try the next fencing level if there is one (unless we're in the "on" * phase of a remapped "reboot", because we ignore errors in that case) */ } while ((op->phase != st_phase_on) && pcmk__is_set(op->call_options, st_opt_topology) && (advance_topology_level(op, false) == pcmk_rc_ok)); /* With a simple watchdog fencing configuration without a topology, * "device" is NULL here. Consider it should be done with watchdog fencing. */ if (is_watchdog_fencing(op, device)) { pcmk__info("Couldn't contact watchdog-fencing target-node (%s)", op->target); /* check_watchdog_fencing_and_wait will log additional info */ } else { pcmk__notice("Couldn't find anyone to fence (%s) %s using %s", op->action, op->target, pcmk__s(device, "any device")); } return NULL; } static int valid_fencing_timeout(int specified_timeout, bool action_specific, const remote_fencing_op_t *op, const char *device) { int timeout = specified_timeout; if (!is_watchdog_fencing(op, device)) { return timeout; } timeout = (int) QB_MIN(QB_MAX(specified_timeout, pcmk__timeout_ms2s(stonith_watchdog_timeout_ms)), INT_MAX); if (timeout > specified_timeout) { if (action_specific) { pcmk__warn("pcmk_%s_timeout %ds for %s is too short (must be >= " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " %ds), using %ds " "instead", op->action, specified_timeout, pcmk__s(device, "watchdog"), timeout, timeout); } else { pcmk__warn("Fencing timeout %ds is too short (must be >= " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " %ds), using %ds " "instead", specified_timeout, timeout, timeout); } } return timeout; } static int get_device_timeout(const remote_fencing_op_t *op, const peer_device_info_t *peer, const char *device, bool with_delay) { int timeout = op->base_timeout; device_properties_t *props; timeout = valid_fencing_timeout(op->base_timeout, false, op, device); if (!peer || !device) { return timeout; } props = g_hash_table_lookup(peer->devices, device); if (!props) { return timeout; } if (props->custom_action_timeout[op->phase]) { timeout = valid_fencing_timeout(props->custom_action_timeout[op->phase], true, op, device); } // op->client_delay < 0 means disable any static/random fencing delays if (with_delay && (op->client_delay >= 0)) { // delay_base is eventually limited by delay_max timeout += (props->delay_max[op->phase] > 0 ? props->delay_max[op->phase] : props->delay_base[op->phase]); } return timeout; } struct timeout_data { const remote_fencing_op_t *op; const peer_device_info_t *peer; int total_timeout; }; /*! * \internal * \brief Add timeout to a total if device has not been executed yet * * \param[in] key GHashTable key (device ID) * \param[in] value GHashTable value (device properties) * \param[in,out] user_data Timeout data */ static void add_device_timeout(gpointer key, gpointer value, gpointer user_data) { const char *device_id = key; device_properties_t *props = value; struct timeout_data *timeout = user_data; if (!props->executed[timeout->op->phase] && !props->disallowed[timeout->op->phase]) { timeout->total_timeout += get_device_timeout(timeout->op, timeout->peer, device_id, true); } } static int get_peer_timeout(const remote_fencing_op_t *op, const peer_device_info_t *peer) { struct timeout_data timeout; timeout.op = op; timeout.peer = peer; timeout.total_timeout = 0; g_hash_table_foreach(peer->devices, add_device_timeout, &timeout); return (timeout.total_timeout? timeout.total_timeout : op->base_timeout); } static int get_op_total_timeout(const remote_fencing_op_t *op, const peer_device_info_t *chosen_peer) { long long total_timeout = 0; stonith_topology_t *tp = find_topology_for_host(op->target); if (pcmk__is_set(op->call_options, st_opt_topology) && (tp != NULL)) { int i; GList *device_list = NULL; GList *iter = NULL; GList *auto_list = NULL; if (pcmk__str_eq(op->action, PCMK_ACTION_ON, pcmk__str_none) && (op->automatic_list != NULL)) { auto_list = g_list_copy(op->automatic_list); } /* Yep, this looks scary, nested loops all over the place. * Here is what is going on. * Loop1: Iterate through fencing levels. * Loop2: If a fencing level has devices, loop through each device * Loop3: For each device in a fencing level, see what peer owns it * and what that peer has reported the timeout is for the device. */ for (i = 0; i < ST__LEVEL_COUNT; i++) { if (!tp->levels[i]) { continue; } for (device_list = tp->levels[i]; device_list; device_list = device_list->next) { bool found = false; for (iter = op->query_results; iter != NULL; iter = iter->next) { const peer_device_info_t *peer = iter->data; if (auto_list) { GList *match = g_list_find_custom(auto_list, device_list->data, sort_strings); if (match) { auto_list = g_list_remove(auto_list, match->data); } } if (find_peer_device(op, peer, device_list->data, fenced_support_flag(op->action))) { total_timeout += get_device_timeout(op, peer, device_list->data, true); found = true; break; } } /* End Loop3: match device with peer that owns device, find device's timeout period */ /* in case of watchdog-device we add the timeout to the budget if didn't get a reply */ if (!found && is_watchdog_fencing(op, device_list->data)) { total_timeout += pcmk__timeout_ms2s(stonith_watchdog_timeout_ms); } } /* End Loop2: iterate through devices at a specific level */ } /*End Loop1: iterate through fencing levels */ //Add only exists automatic_list device timeout if (auto_list) { for (iter = auto_list; iter != NULL; iter = iter->next) { GList *iter2 = NULL; for (iter2 = op->query_results; iter2 != NULL; iter = iter2->next) { peer_device_info_t *peer = iter2->data; if (find_peer_device(op, peer, iter->data, st_device_supports_on)) { total_timeout += get_device_timeout(op, peer, iter->data, true); break; } } } } g_list_free(auto_list); } else if (chosen_peer) { total_timeout = get_peer_timeout(op, chosen_peer); } else { total_timeout = valid_fencing_timeout(op->base_timeout, false, op, NULL); } if (total_timeout <= 0) { total_timeout = op->base_timeout; } /* Take any requested fencing delay into account to prevent it from eating * up the total timeout. */ if (op->client_delay > 0) { total_timeout += op->client_delay; } return (int) QB_MIN(total_timeout, INT_MAX); } static void report_timeout_period(remote_fencing_op_t * op, int op_timeout) { GList *iter = NULL; xmlNode *update = NULL; const char *client_node = NULL; const char *client_id = NULL; const char *call_id = NULL; if (op->call_options & st_opt_sync_call) { /* There is no reason to report the timeout for a synchronous call. It * is impossible to use the reported timeout to do anything when the client * is blocking for the response. This update is only important for * async calls that require a callback to report the results in. */ return; } else if (!op->request) { return; } pcmk__trace("Reporting timeout for %s (id=%.8s)", op->client_name, op->id); client_node = pcmk__xe_get(op->request, PCMK__XA_ST_CLIENTNODE); call_id = pcmk__xe_get(op->request, PCMK__XA_ST_CALLID); client_id = pcmk__xe_get(op->request, PCMK__XA_ST_CLIENTID); if (!client_node || !call_id || !client_id) { return; } if (pcmk__str_eq(client_node, fenced_get_local_node(), pcmk__str_casei)) { // Client is connected to this node, so send update directly to them do_stonith_async_timeout_update(client_id, call_id, op_timeout); return; } /* The client is connected to another node, relay this update to them */ update = stonith_create_op(op->client_callid, op->id, STONITH_OP_TIMEOUT_UPDATE, NULL, 0); pcmk__xe_set(update, PCMK__XA_ST_REMOTE_OP, op->id); pcmk__xe_set(update, PCMK__XA_ST_CLIENTID, client_id); pcmk__xe_set(update, PCMK__XA_ST_CALLID, call_id); pcmk__xe_set_int(update, PCMK__XA_ST_TIMEOUT, op_timeout); pcmk__cluster_send_message(pcmk__get_node(0, client_node, NULL, pcmk__node_search_cluster_member), pcmk_ipc_fenced, update); pcmk__xml_free(update); for (iter = op->duplicates; iter != NULL; iter = iter->next) { remote_fencing_op_t *dup = iter->data; pcmk__trace("Reporting timeout for duplicate %.8s to client %s", dup->id, dup->client_name); report_timeout_period(iter->data, op_timeout); } } /*! * \internal * \brief Advance an operation to the next device in its topology * * \param[in,out] op Fencer operation to advance * \param[in] device ID of device that just completed * \param[in,out] msg If not NULL, XML reply of last delegated operation */ static void advance_topology_device_in_level(remote_fencing_op_t *op, const char *device, xmlNode *msg) { /* Advance to the next device at this topology level, if any */ if (op->devices) { op->devices = op->devices->next; } /* Handle automatic unfencing if an "on" action was requested */ if ((op->phase == st_phase_requested) && pcmk__str_eq(op->action, PCMK_ACTION_ON, pcmk__str_none)) { /* If the device we just executed was required, it's not anymore */ remove_required_device(op, device); /* If there are no more devices at this topology level, run through any * remaining devices with automatic unfencing */ if (op->devices == NULL) { op->devices = op->automatic_list; } } if ((op->devices == NULL) && (op->phase == st_phase_off)) { /* We're done with this level and with required devices, but we had * remapped "reboot" to "off", so start over with "on". If any devices * need to be turned back on, op->devices will be non-NULL after this. */ op_phase_on(op); } // This function is only called if the previous device succeeded pcmk__set_result(&op->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); if (op->devices) { /* Necessary devices remain, so execute the next one */ pcmk__trace("Next targeting %s on behalf of %s@%s", op->target, op->client_name, op->originator); // The requested delay has been applied for the first device if (op->client_delay > 0) { op->client_delay = 0; } request_peer_fencing(op, NULL); } else { /* We're done with all devices and phases, so finalize operation */ pcmk__trace("Marking complex fencing op targeting %s as complete", op->target); op->state = st_done; finalize_op(op, msg, false); } } static gboolean check_watchdog_fencing_and_wait(remote_fencing_op_t * op) { if (node_does_watchdog_fencing(op->target)) { guint timeout_ms = QB_MIN(stonith_watchdog_timeout_ms, UINT_MAX); pcmk__notice("Waiting %s for %s to self-fence (%s) for client %s " QB_XS " id=%.8s", pcmk__readable_interval(timeout_ms), op->target, op->action, op->client_name, op->id); if (op->op_timer_one) { g_source_remove(op->op_timer_one); } op->op_timer_one = pcmk__create_timer(timeout_ms, remote_op_watchdog_done, op); return TRUE; } else { pcmk__debug("Skipping fallback to watchdog-fencing as %s is not in " "host-list", op->target); } return FALSE; } /*! * \internal * \brief Ask a peer to execute a fencing operation * * \param[in,out] op Fencing operation to be executed * \param[in,out] peer If NULL or topology is in use, choose best peer to * execute the fencing, otherwise use this peer */ static void request_peer_fencing(remote_fencing_op_t *op, peer_device_info_t *peer) { const char *device = NULL; int timeout; CRM_CHECK(op != NULL, return); pcmk__trace("Action %.8s targeting %s for %s is %s", op->id, op->target, op->client_name, stonith_op_state_str(op->state)); if ((op->phase == st_phase_on) && (op->devices != NULL)) { /* We are in the "on" phase of a remapped topology reboot. If this * device has pcmk_reboot_action="off", or doesn't support the "on" * action, skip it. * * We can't check device properties at this point because we haven't * chosen a peer for this stage yet. Instead, we check the local node's * knowledge about the device. If different versions of the fence agent * are installed on different nodes, there's a chance this could be * mistaken, but the worst that could happen is we don't try turning the * node back on when we should. */ device = op->devices->data; if (pcmk__str_eq(fenced_device_reboot_action(device), PCMK_ACTION_OFF, pcmk__str_none)) { pcmk__info("Not turning %s back on using %s because the device is " "configured to stay off (pcmk_reboot_action='off')", op->target, device); advance_topology_device_in_level(op, device, NULL); return; } if (!fenced_device_supports_on(device)) { pcmk__info("Not turning %s back on using %s because the agent " "doesn't support 'on'", op->target, device); advance_topology_device_in_level(op, device, NULL); return; } } timeout = op->base_timeout; if ((peer == NULL) && !pcmk__is_set(op->call_options, st_opt_topology)) { peer = stonith_choose_peer(op); } if (!op->op_timer_total) { op->total_timeout = TIMEOUT_MULTIPLY_FACTOR * get_op_total_timeout(op, peer); op->op_timer_total = pcmk__create_timer(1000 * op->total_timeout, remote_op_timeout, op); report_timeout_period(op, op->total_timeout); pcmk__info("Total timeout set to %ds for peer's fencing targeting %s " "for %s " QB_XS " id=%.8s", op->total_timeout, op->target, op->client_name, op->id); } if (pcmk__is_set(op->call_options, st_opt_topology) && (op->devices != NULL)) { /* Ignore the caller's peer preference if topology is in use, because * that peer might not have access to the required device. With * topology, stonith_choose_peer() removes the device from further * consideration, so the timeout must be calculated beforehand. * * @TODO Basing the total timeout on the caller's preferred peer (above) * is less than ideal. */ peer = stonith_choose_peer(op); device = op->devices->data; /* Fencing timeout sent to peer takes no delay into account. * The peer will add a dedicated timer for any delay upon * schedule_stonith_command(). */ timeout = get_device_timeout(op, peer, device, false); } if (peer) { int timeout_one = 0; xmlNode *remote_op = stonith_create_op(op->client_callid, op->id, STONITH_OP_FENCE, NULL, 0); const pcmk__node_status_t *peer_node = pcmk__get_node(0, peer->host, NULL, pcmk__node_search_cluster_member); if (op->client_delay > 0) { /* Take requested fencing delay into account to prevent it from * eating up the timeout. */ timeout_one = TIMEOUT_MULTIPLY_FACTOR * op->client_delay; } pcmk__xe_set(remote_op, PCMK__XA_ST_REMOTE_OP, op->id); pcmk__xe_set(remote_op, PCMK__XA_ST_TARGET, op->target); pcmk__xe_set(remote_op, PCMK__XA_ST_DEVICE_ACTION, op->action); pcmk__xe_set(remote_op, PCMK__XA_ST_ORIGIN, op->originator); pcmk__xe_set(remote_op, PCMK__XA_ST_CLIENTID, op->client_id); pcmk__xe_set(remote_op, PCMK__XA_ST_CLIENTNAME, op->client_name); pcmk__xe_set_int(remote_op, PCMK__XA_ST_TIMEOUT, timeout); pcmk__xe_set_int(remote_op, PCMK__XA_ST_CALLOPT, op->call_options); pcmk__xe_set_int(remote_op, PCMK__XA_ST_DELAY, op->client_delay); if (device) { timeout_one += TIMEOUT_MULTIPLY_FACTOR * get_device_timeout(op, peer, device, true); pcmk__notice("Requesting that %s perform '%s' action targeting %s " "using %s " QB_XS " for client %s (%ds)", peer->host, op->action, op->target, device, op->client_name, timeout_one); pcmk__xe_set(remote_op, PCMK__XA_ST_DEVICE_ID, device); } else { timeout_one += TIMEOUT_MULTIPLY_FACTOR * get_peer_timeout(op, peer); pcmk__notice("Requesting that %s perform '%s' action targeting %s " QB_XS " for client %s (%ds, %s)", peer->host, op->action, op->target, op->client_name, timeout_one, pcmk__readable_interval(stonith_watchdog_timeout_ms)); } op->state = st_exec; if (op->op_timer_one) { g_source_remove(op->op_timer_one); op->op_timer_one = 0; } if (!is_watchdog_fencing(op, device) || !check_watchdog_fencing_and_wait(op)) { /* Some thoughts about self-fencing cases reaching this point: - Actually check in check_watchdog_fencing_and_wait shouldn't fail if STONITH_WATCHDOG_ID is chosen as fencing-device and it being present implies watchdog-fencing is enabled anyway - If watchdog-fencing is disabled either in general or for a specific target - detected in check_watchdog_fencing_and_wait - for some other kind of self-fencing we can't expect a success answer but timeout is fine if the node doesn't come back in between - Delicate might be the case where we have watchdog-fencing enabled for a node but the watchdog-fencing-device isn't explicitly chosen for self-fencing. Local scheduler execution in sbd might detect the node as unclean and lead to timely self-fencing. Otherwise the selection of PCMK_OPT_STONITH_WATCHDOG_TIMEOUT at least is questionable. */ /* coming here we're not waiting for watchdog timeout - thus engage timer with timout evaluated before */ op->op_timer_one = pcmk__create_timer((1000 * timeout_one), remote_op_timeout_one, op); } pcmk__cluster_send_message(peer_node, pcmk_ipc_fenced, remote_op); peer->tried = TRUE; pcmk__xml_free(remote_op); return; } else if (op->phase == st_phase_on) { /* A remapped "on" cannot be executed, but the node was already * turned off successfully, so ignore the error and continue. */ pcmk__warn("Ignoring %s 'on' failure (no capable peers) targeting %s " "after successful 'off'", device, op->target); advance_topology_device_in_level(op, device, NULL); return; } else if (op->owner == FALSE) { pcmk__err("Fencing (%s) targeting %s for client %s is not ours to " "control", op->action, op->target, op->client_name); } else if (op->query_timer == 0) { /* We've exhausted all available peers */ pcmk__info("No remaining peers capable of fencing (%s) %s for client " "%s " QB_XS " state=%s", op->action, op->target, op->client_name, stonith_op_state_str(op->state)); CRM_CHECK(op->state < st_done, return); finalize_timed_out_op(op, "All nodes failed, or are unable, to " "fence target"); } else if(op->replies >= op->replies_expected || op->replies >= fencing_active_peers()) { /* if the operation never left the query state, * but we have all the expected replies, then no devices * are available to execute the fencing operation. */ if (is_watchdog_fencing(op, device) && check_watchdog_fencing_and_wait(op)) { /* Consider a watchdog fencing targeting an offline node executing * once it starts waiting for the target to self-fence. So that when * the query timer pops, remote_op_query_timeout() considers the * fencing already in progress. */ op->state = st_exec; return; } if (op->state == st_query) { pcmk__info("No peers (out of %d) have devices capable of fencing " "(%s) %s for client %s " QB_XS " state=%s", op->replies, op->action, op->target, op->client_name, stonith_op_state_str(op->state)); pcmk__reset_result(&op->result); pcmk__set_result(&op->result, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE, NULL); } else { if (pcmk__is_set(op->call_options, st_opt_topology)) { pcmk__reset_result(&op->result); pcmk__set_result(&op->result, CRM_EX_ERROR, PCMK_EXEC_NO_FENCE_DEVICE, NULL); } /* ... else use existing result from previous failed attempt * (topology is not in use, and no devices remain to be attempted). * Overwriting the result with PCMK_EXEC_NO_FENCE_DEVICE would * prevent finalize_op() from setting the correct delegate if * needed. */ pcmk__info("No peers (out of %d) are capable of fencing (%s) %s " "for client %s " QB_XS " state=%s", op->replies, op->action, op->target, op->client_name, stonith_op_state_str(op->state)); } op->state = st_failed; finalize_op(op, NULL, false); } else { pcmk__info("Waiting for additional peers capable of fencing (%s) " "%s%s%s for client %s " QB_XS " id=%.8s", op->action, op->target, ((device != NULL)? " using " : ""), pcmk__s(device, ""), op->client_name, op->id); } } /*! * \internal * \brief Comparison function for sorting query results * * \param[in] a GList item to compare * \param[in] b GList item to compare * * \return Per the glib documentation, "a negative integer if the first value * comes before the second, 0 if they are equal, or a positive integer * if the first value comes after the second." */ static gint sort_peers(gconstpointer a, gconstpointer b) { const peer_device_info_t *peer_a = a; const peer_device_info_t *peer_b = b; return (peer_b->ndevices - peer_a->ndevices); } /*! * \internal * \brief Determine if all the devices in the topology are found or not * * \param[in] op Fencing operation with topology to check */ static gboolean all_topology_devices_found(const remote_fencing_op_t *op) { GList *device = NULL; GList *iter = NULL; device_properties_t *match = NULL; stonith_topology_t *tp = NULL; gboolean skip_target = FALSE; int i; tp = find_topology_for_host(op->target); if (!tp) { return FALSE; } if (pcmk__is_fencing_action(op->action)) { /* Don't count the devices on the target node if we are killing * the target node. */ skip_target = TRUE; } for (i = 0; i < ST__LEVEL_COUNT; i++) { for (device = tp->levels[i]; device; device = device->next) { match = NULL; for (iter = op->query_results; iter && !match; iter = iter->next) { peer_device_info_t *peer = iter->data; if (skip_target && pcmk__str_eq(peer->host, op->target, pcmk__str_casei)) { continue; } match = find_peer_device(op, peer, device->data, st_device_supports_none); } if (!match) { return FALSE; } } } return TRUE; } /*! * \internal * \brief Parse action-specific device properties from XML * * \param[in] xml XML element containing the properties * \param[in] peer Name of peer that sent XML (for logs) * \param[in] device Device ID (for logs) * \param[in] action Action the properties relate to (for logs) * \param[in,out] op Fencing operation that properties are being parsed for * \param[in] phase Phase the properties relate to * \param[in,out] props Device properties to update */ static void parse_action_specific(const xmlNode *xml, const char *peer, const char *device, const char *action, remote_fencing_op_t *op, enum st_remap_phase phase, device_properties_t *props) { props->custom_action_timeout[phase] = 0; pcmk__xe_get_int(xml, PCMK__XA_ST_ACTION_TIMEOUT, &props->custom_action_timeout[phase]); if (props->custom_action_timeout[phase]) { pcmk__trace("Peer %s with device %s returned %s action timeout %ds", peer, device, action, props->custom_action_timeout[phase]); } props->delay_max[phase] = 0; pcmk__xe_get_int(xml, PCMK__XA_ST_DELAY_MAX, &props->delay_max[phase]); if (props->delay_max[phase]) { pcmk__trace("Peer %s with device %s returned maximum of random delay " "%ds for %s", peer, device, props->delay_max[phase], action); } props->delay_base[phase] = 0; pcmk__xe_get_int(xml, PCMK__XA_ST_DELAY_BASE, &props->delay_base[phase]); if (props->delay_base[phase]) { pcmk__trace("Peer %s with device %s returned base delay %ds for %s", peer, device, props->delay_base[phase], action); } /* Handle devices with automatic unfencing */ if (pcmk__str_eq(action, PCMK_ACTION_ON, pcmk__str_none)) { int required = 0; pcmk__xe_get_int(xml, PCMK__XA_ST_REQUIRED, &required); if (required) { pcmk__trace("Peer %s requires device %s to execute for action %s", peer, device, action); add_required_device(op, device); } } /* If a reboot is remapped to off+on, it's possible that a node is allowed * to perform one action but not another. */ if (pcmk__xe_attr_is_true(xml, PCMK__XA_ST_ACTION_DISALLOWED)) { props->disallowed[phase] = TRUE; pcmk__trace("Peer %s is disallowed from executing %s for device %s", peer, action, device); } } /*! * \internal * \brief Parse one device's properties from peer's XML query reply * * \param[in] xml XML node containing device properties * \param[in,out] op Operation that query and reply relate to * \param[in,out] peer Peer's device information * \param[in] device ID of device being parsed */ static void add_device_properties(const xmlNode *xml, remote_fencing_op_t *op, peer_device_info_t *peer, const char *device) { xmlNode *child; int verified = 0; device_properties_t *props = pcmk__assert_alloc(1, sizeof(device_properties_t)); int rc = pcmk_rc_ok; /* Add a new entry to this peer's devices list */ g_hash_table_insert(peer->devices, pcmk__str_copy(device), props); /* Peers with verified (monitored) access will be preferred */ pcmk__xe_get_int(xml, PCMK__XA_ST_MONITOR_VERIFIED, &verified); if (verified) { pcmk__trace("Peer %s has confirmed a verified device %s", peer->host, device); props->verified = TRUE; } // Nodes <2.1.5 won't set this, so assume unfencing in that case rc = pcmk__xe_get_flags(xml, PCMK__XA_ST_DEVICE_SUPPORT_FLAGS, &(props->device_support_flags), st_device_supports_on); if (rc != pcmk_rc_ok) { pcmk__warn("Couldn't determine device support for %s " "(assuming unfencing): %s", device, pcmk_rc_str(rc)); } /* Parse action-specific device properties */ parse_action_specific(xml, peer->host, device, op_requested_action(op), op, st_phase_requested, props); for (child = pcmk__xe_first_child(xml, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child, NULL)) { /* Replies for "reboot" operations will include the action-specific * values for "off" and "on" in child elements, just in case the reboot * winds up getting remapped. */ if (pcmk__str_eq(pcmk__xe_id(child), PCMK_ACTION_OFF, pcmk__str_none)) { parse_action_specific(child, peer->host, device, PCMK_ACTION_OFF, op, st_phase_off, props); } else if (pcmk__str_eq(pcmk__xe_id(child), PCMK_ACTION_ON, pcmk__str_none)) { parse_action_specific(child, peer->host, device, PCMK_ACTION_ON, op, st_phase_on, props); } } } /*! * \internal * \brief Parse a peer's XML query reply and add it to operation's results * * \param[in,out] op Operation that query and reply relate to * \param[in] host Name of peer that sent this reply * \param[in] ndevices Number of devices expected in reply * \param[in] xml XML node containing device list * * \return Newly allocated result structure with parsed reply */ static peer_device_info_t * add_result(remote_fencing_op_t *op, const char *host, int ndevices, const xmlNode *xml) { peer_device_info_t *peer = pcmk__assert_alloc(1, sizeof(peer_device_info_t)); xmlNode *child; peer->host = pcmk__str_copy(host); peer->devices = pcmk__strkey_table(free, free); /* Each child element describes one capable device available to the peer */ for (child = pcmk__xe_first_child(xml, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child, NULL)) { const char *device = pcmk__xe_id(child); if (device) { add_device_properties(child, op, peer, device); } } peer->ndevices = g_hash_table_size(peer->devices); CRM_CHECK(ndevices == peer->ndevices, pcmk__err("Query claimed to have %d device%s but %d found", ndevices, pcmk__plural_s(ndevices), peer->ndevices)); op->query_results = g_list_insert_sorted(op->query_results, peer, sort_peers); return peer; } /*! * \internal * \brief Handle a peer's reply to our fencing query * * Parse a query result from XML and store it in the remote operation * table, and when enough replies have been received, issue a fencing request. * * \param[in] msg XML reply received * * \return pcmk_ok on success, -errno on error * * \note See initiate_remote_stonith_op() for how the XML query was initially * formed, and stonith_query() for how the peer formed its XML reply. */ int process_remote_stonith_query(xmlNode *msg) { int ndevices = 0; gboolean host_is_target = FALSE; gboolean have_all_replies = FALSE; const char *id = NULL; const char *host = NULL; remote_fencing_op_t *op = NULL; peer_device_info_t *peer = NULL; uint32_t replies_expected; xmlNode *dev = pcmk__xpath_find_one(msg->doc, "//*[@" PCMK__XA_ST_REMOTE_OP "]", LOG_ERR); CRM_CHECK(dev != NULL, return -EPROTO); id = pcmk__xe_get(dev, PCMK__XA_ST_REMOTE_OP); CRM_CHECK(id != NULL, return -EPROTO); dev = pcmk__xpath_find_one(msg->doc, "//*[@" PCMK__XA_ST_AVAILABLE_DEVICES "]", LOG_ERR); CRM_CHECK(dev != NULL, return -EPROTO); pcmk__xe_get_int(dev, PCMK__XA_ST_AVAILABLE_DEVICES, &ndevices); op = g_hash_table_lookup(stonith_remote_op_list, id); if (op == NULL) { pcmk__debug("Received query reply for unknown or expired operation %s", id); return -EOPNOTSUPP; } replies_expected = fencing_active_peers(); if (op->replies_expected < replies_expected) { replies_expected = op->replies_expected; } if ((++op->replies >= replies_expected) && (op->state == st_query)) { have_all_replies = TRUE; } host = pcmk__xe_get(msg, PCMK__XA_SRC); host_is_target = pcmk__str_eq(host, op->target, pcmk__str_casei); pcmk__info("Query result %d of %d from %s for %s/%s (%d device%s) %s", op->replies, replies_expected, host, op->target, op->action, ndevices, pcmk__plural_s(ndevices), id); if (ndevices > 0) { peer = add_result(op, host, ndevices, dev); } pcmk__set_result(&op->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); if (pcmk__is_set(op->call_options, st_opt_topology)) { /* If we start the fencing before all the topology results are in, * it is possible fencing levels will be skipped because of the missing * query results. */ if (op->state == st_query && all_topology_devices_found(op)) { /* All the query results are in for the topology, start the fencing ops. */ pcmk__trace("All topology devices found"); request_peer_fencing(op, peer); } else if (have_all_replies) { pcmk__info("All topology query replies have arrived, continuing " "(%d expected/%d received) ", replies_expected, op->replies); request_peer_fencing(op, NULL); } } else if (op->state == st_query) { int nverified = count_peer_devices(op, peer, TRUE, fenced_support_flag(op->action)); /* We have a result for a non-topology fencing op that looks promising, * go ahead and start fencing before query timeout */ if ((peer != NULL) && !host_is_target && nverified) { /* we have a verified device living on a peer that is not the target */ pcmk__trace("Found %d verified device%s", nverified, pcmk__plural_s(nverified)); request_peer_fencing(op, peer); } else if (have_all_replies) { pcmk__info("All query replies have arrived, continuing (%d " "expected/%d received) ", replies_expected, op->replies); request_peer_fencing(op, NULL); } else { pcmk__trace("Waiting for more peer results before launching " "fencing operation"); } } else if ((peer != NULL) && (op->state == st_done)) { pcmk__info("Discarding query result from %s (%d device%s): Operation " "is %s", peer->host, peer->ndevices, pcmk__plural_s(peer->ndevices), stonith_op_state_str(op->state)); } return pcmk_ok; } /*! * \internal * \brief Handle a peer's reply to a fencing request * * Parse a fencing reply from XML, and either finalize the operation * or attempt another device as appropriate. * * \param[in] msg XML reply received */ void fenced_process_fencing_reply(xmlNode *msg) { const char *id = NULL; const char *device = NULL; remote_fencing_op_t *op = NULL; xmlNode *dev = pcmk__xpath_find_one(msg->doc, "//*[@" PCMK__XA_ST_REMOTE_OP "]", LOG_ERR); pcmk__action_result_t result = PCMK__UNKNOWN_RESULT; CRM_CHECK(dev != NULL, return); id = pcmk__xe_get(dev, PCMK__XA_ST_REMOTE_OP); CRM_CHECK(id != NULL, return); dev = stonith__find_xe_with_result(msg); CRM_CHECK(dev != NULL, return); stonith__xe_get_result(dev, &result); device = pcmk__xe_get(dev, PCMK__XA_ST_DEVICE_ID); if (stonith_remote_op_list) { op = g_hash_table_lookup(stonith_remote_op_list, id); } if ((op == NULL) && pcmk__result_ok(&result)) { /* Record successful fencing operations */ const char *client_id = pcmk__xe_get(dev, PCMK__XA_ST_CLIENTID); op = create_remote_stonith_op(client_id, dev, TRUE); } if (op == NULL) { /* Could be for an event that began before we started */ /* TODO: Record the op for later querying */ pcmk__info("Received peer result of unknown or expired operation %s", id); pcmk__reset_result(&result); return; } pcmk__reset_result(&op->result); op->result = result; // The operation takes ownership of the result if (op->devices && device && !pcmk__str_eq(op->devices->data, device, pcmk__str_casei)) { pcmk__err("Received outdated reply for device %s (instead of %s) to " "fence (%s) %s. Operation already timed out at peer level.", device, (const char *) op->devices->data, op->action, op->target); return; } if (pcmk__str_eq(pcmk__xe_get(msg, PCMK__XA_SUBT), PCMK__VALUE_BROADCAST, pcmk__str_none)) { if (pcmk__result_ok(&op->result)) { op->state = st_done; } else { op->state = st_failed; } finalize_op(op, msg, false); return; } else if (!pcmk__str_eq(op->originator, fenced_get_local_node(), pcmk__str_casei)) { /* If this isn't a remote level broadcast, and we are not the * originator of the operation, we should not be receiving this msg. */ pcmk__err("Received non-broadcast fencing result for operation %.8s we " "do not own (device %s targeting %s)", op->id, device, op->target); return; } if (pcmk__is_set(op->call_options, st_opt_topology)) { const char *device = NULL; const char *reason = op->result.exit_reason; /* We own the op, and it is complete. broadcast the result to all nodes * and notify our local clients. */ if (op->state == st_done) { finalize_op(op, msg, false); return; } device = pcmk__xe_get(msg, PCMK__XA_ST_DEVICE_ID); if ((op->phase == 2) && !pcmk__result_ok(&op->result)) { /* A remapped "on" failed, but the node was already turned off * successfully, so ignore the error and continue. */ pcmk__warn("Ignoring %s 'on' failure (%s%s%s) targeting %s after " "successful 'off'", device, pcmk_exec_status_str(op->result.execution_status), ((reason != NULL)? ": " : ""), pcmk__s(reason, ""), op->target); pcmk__set_result(&op->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); } else { pcmk__notice("Action '%s' targeting %s%s%s on behalf of %s@%s: " "%s%s%s%s", op->action, op->target, ((device == NULL)? "" : " using "), pcmk__s(device, ""), op->client_name, op->originator, pcmk_exec_status_str(op->result.execution_status), ((reason != NULL)? " (" : ""), pcmk__s(reason, ""), ((reason != NULL)? ")" : "")); } if (pcmk__result_ok(&op->result)) { /* An operation completed successfully. Try another device if * necessary, otherwise mark the operation as done. */ advance_topology_device_in_level(op, device, msg); return; } else { /* This device failed, time to try another topology level. If no other * levels are available, mark this operation as failed and report results. */ if (advance_topology_level(op, false) != pcmk_rc_ok) { op->state = st_failed; finalize_op(op, msg, false); return; } } } else if (pcmk__result_ok(&op->result) && (op->devices == NULL)) { op->state = st_done; finalize_op(op, msg, false); return; } else if ((op->result.execution_status == PCMK_EXEC_TIMEOUT) && (op->devices == NULL)) { /* If the operation timed out don't bother retrying other peers. */ op->state = st_failed; finalize_op(op, msg, false); return; } else { /* fall-through and attempt other fencing action using another peer */ } /* Retry on failure */ pcmk__trace("Next for %s on behalf of %s@%s (result was: %s)", op->target, op->originator, op->client_name, pcmk_exec_status_str(op->result.execution_status)); request_peer_fencing(op, NULL); } gboolean stonith_check_fence_tolerance(int tolerance, const char *target, const char *action) { GHashTableIter iter; time_t now = time(NULL); remote_fencing_op_t *rop = NULL; if (tolerance <= 0 || !stonith_remote_op_list || target == NULL || action == NULL) { return FALSE; } g_hash_table_iter_init(&iter, stonith_remote_op_list); while (g_hash_table_iter_next(&iter, NULL, (void **)&rop)) { if (strcmp(rop->target, target) != 0) { continue; } else if (rop->state != st_done) { continue; /* We don't have to worry about remapped reboots here * because if state is done, any remapping has been undone */ } else if (strcmp(rop->action, action) != 0) { continue; } else if ((rop->completed + tolerance) < now) { continue; } pcmk__notice("Target %s was fenced (%s) less than %ds ago by %s on " "behalf of %s", target, action, tolerance, rop->delegate, rop->originator); return TRUE; } return FALSE; } diff --git a/include/crm/common/logging_internal.h b/include/crm/common/logging_internal.h index d8c6dec11f..cc44e0db0c 100644 --- a/include/crm/common/logging_internal.h +++ b/include/crm/common/logging_internal.h @@ -1,394 +1,403 @@ /* * 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 General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_LOGGING_INTERNAL__H #define PCMK__CRM_COMMON_LOGGING_INTERNAL__H #include #include // pcmk__is_set() #include #include #ifdef __cplusplus extern "C" { #endif /* Define custom log priorities. * * syslog(3) uses int for priorities, but libqb's struct qb_log_callsite uses * uint8_t, so make sure they fit in the latter. */ #ifndef PCMK__LOG_TRACE /*! * \internal * \brief Log level for tracing (less importance than \c LOG_DEBUG messages) * * \note This value must stay the same as \c LOG_TRACE until the latter is * dropped. Be mindful of public API functions that may pass arbitrary * integer log levels as well. */ #define PCMK__LOG_TRACE (LOG_DEBUG + 1) #endif // PCMK__LOG_TRACE #ifndef PCMK__LOG_STDOUT /*! * \internal * \brief Request to print message to \c stdout instead of logging it * * Some callees print nothing when this is the log level. * * \note This value must stay the same as \c LOG_STDOUT until the latter is * dropped. Be mindful of public API functions that may pass arbitrary * integer log levels as well. */ #define PCMK__LOG_STDOUT 254 #endif // PCMK__LOG_STDOUT #ifndef PCMK__LOG_NEVER /*! * \internal * \brief Request not to print or log message anywhere * * \note This value must stay the same as \c LOG_NEVER until the latter is * dropped. Be mindful of public API functions that may pass arbitrary * integer log levels as well. */ #define PCMK__LOG_NEVER 255 #endif // PCMK__LOG_NEVER /*! * \internal * \brief Log a message at \c LOG_EMERG level * * \param[in] fmt \c printf() format string for log message * \param[in] args Format string arguments */ #define pcmk__emerg(fmt, args...) qb_log(LOG_EMERG, fmt, ##args) /*! * \internal * \brief Log a message at \c LOG_CRIT level * * \param[in] fmt \c printf() format string for log message * \param[in] args Format string arguments */ #define pcmk__crit(fmt, args...) qb_log(LOG_CRIT, fmt, ##args) /*! * \internal * \brief Log a message at \c LOG_ERR level * * \param[in] fmt \c printf() format string for log message * \param[in] args Format string arguments */ #define pcmk__err(fmt, args...) qb_log(LOG_ERR, fmt, ##args) /*! * \internal * \brief Log a message at \c LOG_WARN level * * \param[in] fmt \c printf() format string for log message * \param[in] args Format string arguments */ #define pcmk__warn(fmt, args...) qb_log(LOG_WARNING, fmt, ##args) /*! * \internal * \brief Log a message at \c LOG_NOTICE level * * \param[in] fmt \c printf() format string for log message * \param[in] args Format string arguments */ #define pcmk__notice(fmt, args...) qb_log(LOG_NOTICE, fmt, ##args) /*! * \internal * \brief Log a message at \c LOG_INFO level * * \param[in] fmt \c printf() format string for log message * \param[in] args Format string arguments */ #define pcmk__info(fmt, args...) qb_log(LOG_INFO, fmt, ##args) /*! * \internal * \brief Log a message at \c LOG_DEBUG level * * \param[in] fmt \c printf() format string for log message * \param[in] args Format string arguments */ #define pcmk__debug(fmt, args...) do_crm_log_unlikely(LOG_DEBUG, fmt, ##args) /*! * \internal * \brief Log a message at \c PCMK__LOG_TRACE level * * \param[in] fmt \c printf() format string for log message * \param[in] args Format string arguments */ #define pcmk__trace(fmt, args...) do_crm_log_unlikely(LOG_TRACE, fmt, ##args) /*! * \internal * \brief Log XML line-by-line in a formatted fashion at \c LOG_ERR level * * \param[in] prefix Prefix for each line * \param[in] xml XML to log */ #define pcmk__log_xml_err(xml, prefix) do_crm_log_xml(LOG_ERR, prefix, xml) /*! * \internal * \brief Log XML line-by-line in a formatted fashion at \c LOG_WARNING level * * \param[in] prefix Prefix for each line * \param[in] xml XML to log */ #define pcmk__log_xml_warn(xml, prefix) do_crm_log_xml(LOG_WARNING, prefix, xml) /*! * \internal * \brief Log XML line-by-line in a formatted fashion at \c LOG_NOTICE level * * \param[in] prefix Prefix for each line * \param[in] xml XML to log */ #define pcmk__log_xml_notice(xml, prefix) \ do_crm_log_xml(LOG_NOTICE, prefix, xml) /*! * \internal * \brief Log XML line-by-line in a formatted fashion at \c LOG_INFO level * * \param[in] prefix Prefix for each line * \param[in] xml XML to log */ #define pcmk__log_xml_info(xml, prefix) do_crm_log_xml(LOG_INFO, prefix, xml) +/*! + * \internal + * \brief Log XML line-by-line in a formatted fashion at \c LOG_DEBUG level + * + * \param[in] prefix Prefix for each line + * \param[in] xml XML to log + */ +#define pcmk__log_xml_debug(xml, prefix) do_crm_log_xml(LOG_DEBUG, prefix, xml) + /* Some warnings are too noisy when logged every time a given function is called * (for example, using a deprecated feature). As an alternative, we allow * warnings to be logged once per invocation of the calling program. Each of * those warnings needs a flag defined here. */ enum pcmk__warnings { pcmk__wo_blind = (1 << 0), pcmk__wo_record_pending = (1 << 1), pcmk__wo_require_all = (1 << 4), pcmk__wo_order_score = (1 << 5), pcmk__wo_group_order = (1 << 11), pcmk__wo_group_coloc = (1 << 12), pcmk__wo_set_ordering = (1 << 15), pcmk__wo_rdisc_enabled = (1 << 16), pcmk__wo_op_attr_expr = (1 << 19), pcmk__wo_clone_master_max = (1 << 23), pcmk__wo_clone_master_node_max = (1 << 24), pcmk__wo_master_role = (1 << 26), pcmk__wo_slave_role = (1 << 27), }; /*! * \internal * \brief Log a warning once per invocation of calling program * * \param[in] wo_flag enum pcmk__warnings value for this warning * \param[in] fmt... printf(3)-style format and arguments */ #define pcmk__warn_once(wo_flag, fmt...) do { \ if (!pcmk__is_set(pcmk__warnings, wo_flag)) { \ if (wo_flag == pcmk__wo_blind) { \ pcmk__warn(fmt); \ } else { \ pcmk__config_warn(fmt); \ } \ pcmk__warnings = pcmk__set_flags_as(__func__, __LINE__, \ PCMK__LOG_TRACE, \ "Warn-once", "logging", \ pcmk__warnings, \ (wo_flag), #wo_flag); \ } \ } while (0) typedef void (*pcmk__config_error_func) (void *ctx, const char *msg, ...) G_GNUC_PRINTF(2, 3); typedef void (*pcmk__config_warning_func) (void *ctx, const char *msg, ...) G_GNUC_PRINTF(2, 3); extern pcmk__config_error_func pcmk__config_error_handler; extern pcmk__config_warning_func pcmk__config_warning_handler; extern void *pcmk__config_error_context; extern void *pcmk__config_warning_context; void pcmk__set_config_error_handler(pcmk__config_error_func error_handler, void *error_context); void pcmk__set_config_warning_handler(pcmk__config_warning_func warning_handler, void *warning_context); /* Pacemaker library functions set this when a configuration error is found, * which turns on extra messages at the end of processing. */ extern bool pcmk__config_has_error; /* Pacemaker library functions set this when a configuration warning is found, * which turns on extra messages at the end of processing. */ extern bool pcmk__config_has_warning; /*! * \internal * \brief Log an error and make crm_verify return failure status * * \param[in] fmt... printf(3)-style format string and arguments */ #define pcmk__config_err(fmt...) do { \ pcmk__config_has_error = true; \ if (pcmk__config_error_handler == NULL) { \ pcmk__err(fmt); \ } else { \ pcmk__config_error_handler(pcmk__config_error_context, fmt); \ } \ } while (0) /*! * \internal * \brief Log a warning and make crm_verify return failure status * * \param[in] fmt... printf(3)-style format string and arguments */ #define pcmk__config_warn(fmt...) do { \ pcmk__config_has_warning = true; \ if (pcmk__config_warning_handler == NULL) { \ pcmk__warn(fmt); \ } else { \ pcmk__config_warning_handler(pcmk__config_warning_context, fmt);\ } \ } while (0) /*! * \internal * \brief Execute code depending on whether trace logging is enabled * * This is similar to \p do_crm_log_unlikely() except instead of logging, it * selects one of two code blocks to execute. * * \param[in] if_action Code block to execute if trace logging is enabled * \param[in] else_action Code block to execute if trace logging is not enabled * * \note Neither \p if_action nor \p else_action can contain a \p break or * \p continue statement. */ #define pcmk__if_tracing(if_action, else_action) do { \ static struct qb_log_callsite *trace_cs = NULL; \ \ if (trace_cs == NULL) { \ trace_cs = qb_log_callsite_get(__func__, __FILE__, \ "if_tracing", PCMK__LOG_TRACE, \ __LINE__, crm_trace_nonlog); \ } \ if (crm_is_callsite_active(trace_cs, PCMK__LOG_TRACE, \ crm_trace_nonlog)) { \ if_action; \ } else { \ else_action; \ } \ } while (0) /*! * \internal * \brief Log XML changes line-by-line in a formatted fashion * * \param[in] level Priority at which to log the messages * \param[in] xml XML to log * * \note This does nothing when \p level is \c PCMK__LOG_STDOUT or * \c PCMK__LOG_NEVER. */ #define pcmk__log_xml_changes(level, xml) do { \ uint8_t _level = pcmk__clip_log_level(level); \ static struct qb_log_callsite *xml_cs = NULL; \ \ switch (_level) { \ case PCMK__LOG_STDOUT: \ case PCMK__LOG_NEVER: \ break; \ default: \ if (xml_cs == NULL) { \ xml_cs = qb_log_callsite_get(__func__, __FILE__, \ "xml-changes", _level, \ __LINE__, 0); \ } \ if (crm_is_callsite_active(xml_cs, _level, 0)) { \ pcmk__log_xml_changes_as(__FILE__, __func__, __LINE__, \ 0, _level, xml); \ } \ break; \ } \ } while(0) /*! * \internal * \brief Log an XML patchset line-by-line in a formatted fashion * * \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 PCMK__LOG_STDOUT or * \c PCMK__LOG_NEVER. */ #define pcmk__log_xml_patchset(level, patchset) do { \ uint8_t _level = pcmk__clip_log_level(level); \ static struct qb_log_callsite *xml_cs = NULL; \ \ switch (_level) { \ case PCMK__LOG_STDOUT: \ case PCMK__LOG_NEVER: \ break; \ default: \ if (xml_cs == NULL) { \ xml_cs = qb_log_callsite_get(__func__, __FILE__, \ "xml-patchset", _level, \ __LINE__, 0); \ } \ if (crm_is_callsite_active(xml_cs, _level, 0)) { \ pcmk__log_xml_patchset_as(__FILE__, __func__, __LINE__, \ 0, _level, patchset); \ } \ break; \ } \ } while(0) void pcmk__log_xml_changes_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const xmlNode *xml); void pcmk__log_xml_patchset_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const xmlNode *patchset); /*! * \internal * \brief Initialize logging for command line tools * * \param[in] name The name of the program * \param[in] verbosity How verbose to be in logging * * \note \p verbosity is not the same as the logging level (LOG_ERR, etc.). */ void pcmk__cli_init_logging(const char *name, unsigned int verbosity); int pcmk__add_logfile(const char *filename); void pcmk__add_logfiles(gchar **log_files, pcmk__output_t *out); void pcmk__free_common_logger(void); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_LOGGING_INTERNAL__H diff --git a/lib/cib/cib_attrs.c b/lib/cib/cib_attrs.c index d01a9dfceb..87b81a9e44 100644 --- a/lib/cib/cib_attrs.c +++ b/lib/cib/cib_attrs.c @@ -1,671 +1,671 @@ /* * 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 // crm_create_nvpair_xml() #include #include #include #include static pcmk__output_t * new_output_object(const char *ty) { int rc = pcmk_rc_ok; pcmk__output_t *out = NULL; const char* argv[] = { "", NULL }; pcmk__supported_format_t formats[] = { PCMK__SUPPORTED_FORMAT_LOG, PCMK__SUPPORTED_FORMAT_TEXT, { NULL, NULL, NULL } }; pcmk__register_formats(NULL, formats); rc = pcmk__output_new(&out, ty, NULL, (char**)argv); if ((rc != pcmk_rc_ok) || (out == NULL)) { pcmk__err("Can't out due to internal error: %s", pcmk_rc_str(rc)); return NULL; } return out; } static int find_attr(cib_t *cib, const char *section, const char *node_uuid, const char *attr_set_type, const char *set_name, const char *attr_id, const char *attr_name, const char *user_name, xmlNode **result) { int rc = pcmk_rc_ok; const char *xpath_base = NULL; GString *xpath = NULL; xmlNode *xml_search = NULL; const char *set_type = NULL; const char *node_type = NULL; if (attr_set_type) { set_type = attr_set_type; } else { set_type = PCMK_XE_INSTANCE_ATTRIBUTES; } if (pcmk__str_eq(section, PCMK_XE_CRM_CONFIG, pcmk__str_casei)) { node_uuid = NULL; set_type = PCMK_XE_CLUSTER_PROPERTY_SET; } else if (pcmk__strcase_any_of(section, PCMK_XE_OP_DEFAULTS, PCMK_XE_RSC_DEFAULTS, NULL)) { node_uuid = NULL; set_type = PCMK_XE_META_ATTRIBUTES; } else if (pcmk__str_eq(section, PCMK_XE_TICKETS, pcmk__str_casei)) { node_uuid = NULL; section = PCMK_XE_STATUS; node_type = PCMK_XE_TICKETS; } else if (node_uuid == NULL) { return EINVAL; } xpath_base = pcmk_cib_xpath_for(section); if (xpath_base == NULL) { pcmk__warn("%s CIB section not known", section); return ENOMSG; } xpath = g_string_sized_new(1024); g_string_append(xpath, xpath_base); if (pcmk__str_eq(node_type, PCMK_XE_TICKETS, pcmk__str_casei)) { pcmk__g_strcat(xpath, "//", node_type, NULL); } else if (node_uuid) { const char *node_type = PCMK_XE_NODE; if (pcmk__str_eq(section, PCMK_XE_STATUS, pcmk__str_casei)) { node_type = PCMK__XE_NODE_STATE; set_type = PCMK__XE_TRANSIENT_ATTRIBUTES; } pcmk__g_strcat(xpath, "//", node_type, "[@" PCMK_XA_ID "='", node_uuid, "']", NULL); } pcmk__g_strcat(xpath, "//", set_type, NULL); if (set_name) { pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", set_name, "']", NULL); } g_string_append(xpath, "//nvpair"); if (attr_id && attr_name) { pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", attr_id, "' " "and @" PCMK_XA_NAME "='", attr_name, "']", NULL); } else if (attr_id) { pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", attr_id, "']", NULL); } else if (attr_name) { pcmk__g_strcat(xpath, "[@" PCMK_XA_NAME "='", attr_name, "']", NULL); } rc = cib_internal_op(cib, PCMK__CIB_REQUEST_QUERY, NULL, (const char *) xpath->str, NULL, &xml_search, cib_sync_call|cib_xpath, user_name); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { pcmk__trace("Query failed for attribute %s (section=%s, node=%s, " "set=%s, xpath=%s): %s", attr_name, section, pcmk__s(node_uuid, ""), pcmk__s(set_name, ""), xpath->str, pcmk_rc_str(rc)); } else { - crm_log_xml_debug(xml_search, "Match"); + pcmk__log_xml_debug(xml_search, "Match"); } g_string_free(xpath, TRUE); *result = xml_search; return rc; } static int handle_multiples(pcmk__output_t *out, xmlNode *search, const char *attr_name) { if ((search != NULL) && (search->children != NULL)) { pcmk__warn_multiple_name_matches(out, search, attr_name); return ENOTUNIQ; } else { return pcmk_rc_ok; } } int cib__update_node_attr(pcmk__output_t *out, cib_t *cib, int call_options, const char *section, const char *node_uuid, const char *set_type, const char *set_name, const char *attr_id, const char *attr_name, const char *attr_value, const char *user_name, const char *node_type) { const char *tag = NULL; int rc = pcmk_rc_ok; xmlNode *xml_top = NULL; xmlNode *xml_obj = NULL; xmlNode *xml_search = NULL; char *local_attr_id = NULL; char *local_set_name = NULL; CRM_CHECK((out != NULL) && (cib != NULL) && (section != NULL) && ((attr_id != NULL) || (attr_name != NULL)) && (attr_value != NULL), return EINVAL); rc = find_attr(cib, section, node_uuid, set_type, set_name, attr_id, attr_name, user_name, &xml_search); if (rc == pcmk_rc_ok) { if (handle_multiples(out, xml_search, attr_name) == ENOTUNIQ) { pcmk__xml_free(xml_search); return ENOTUNIQ; } else { local_attr_id = pcmk__xe_get_copy(xml_search, PCMK_XA_ID); attr_id = local_attr_id; pcmk__xml_free(xml_search); goto do_modify; } } else if (rc != ENXIO) { pcmk__xml_free(xml_search); return rc; /* } else if(attr_id == NULL) { */ /* return EINVAL; */ } else { pcmk__xml_free(xml_search); pcmk__trace("%s does not exist, create it", attr_name); if (pcmk__str_eq(section, PCMK_XE_TICKETS, pcmk__str_casei)) { node_uuid = NULL; section = PCMK_XE_STATUS; node_type = PCMK_XE_TICKETS; xml_top = pcmk__xe_create(xml_obj, PCMK_XE_STATUS); xml_obj = pcmk__xe_create(xml_top, PCMK_XE_TICKETS); } else if (pcmk__str_eq(section, PCMK_XE_NODES, pcmk__str_casei)) { if (node_uuid == NULL) { return EINVAL; } if (pcmk__str_eq(node_type, PCMK_VALUE_REMOTE, pcmk__str_casei)) { xml_top = pcmk__xe_create(xml_obj, PCMK_XE_NODES); xml_obj = pcmk__xe_create(xml_top, PCMK_XE_NODE); pcmk__xe_set(xml_obj, PCMK_XA_TYPE, PCMK_VALUE_REMOTE); pcmk__xe_set(xml_obj, PCMK_XA_ID, node_uuid); pcmk__xe_set(xml_obj, PCMK_XA_UNAME, node_uuid); } else { tag = PCMK_XE_NODE; } } else if (pcmk__str_eq(section, PCMK_XE_STATUS, pcmk__str_casei)) { tag = PCMK__XE_TRANSIENT_ATTRIBUTES; if (node_uuid == NULL) { return EINVAL; } xml_top = pcmk__xe_create(xml_obj, PCMK__XE_NODE_STATE); pcmk__xe_set(xml_top, PCMK_XA_ID, node_uuid); xml_obj = xml_top; } else { tag = section; node_uuid = NULL; } if (set_name == NULL) { if (pcmk__str_eq(section, PCMK_XE_CRM_CONFIG, pcmk__str_casei)) { local_set_name = pcmk__str_copy(PCMK_VALUE_CIB_BOOTSTRAP_OPTIONS); } else if (pcmk__str_eq(node_type, PCMK_XE_TICKETS, pcmk__str_casei)) { local_set_name = pcmk__assert_asprintf("%s-%s", section, PCMK_XE_TICKETS); } else if (node_uuid) { local_set_name = pcmk__assert_asprintf("%s-%s", section, node_uuid); if (set_type) { char *tmp_set_name = local_set_name; local_set_name = pcmk__assert_asprintf("%s-%s", tmp_set_name, set_type); free(tmp_set_name); } } else { local_set_name = pcmk__assert_asprintf("%s-options", section); } set_name = local_set_name; } if (attr_id == NULL) { local_attr_id = pcmk__assert_asprintf("%s-%s", set_name, attr_name); pcmk__xml_sanitize_id(local_attr_id); attr_id = local_attr_id; } else if (attr_name == NULL) { attr_name = attr_id; } pcmk__trace("Creating %s/%s", section, tag); if (tag != NULL) { xml_obj = pcmk__xe_create(xml_obj, tag); pcmk__xe_set(xml_obj, PCMK_XA_ID, node_uuid); if (xml_top == NULL) { xml_top = xml_obj; } } if ((node_uuid == NULL) && !pcmk__str_eq(node_type, PCMK_XE_TICKETS, pcmk__str_casei)) { if (pcmk__str_eq(section, PCMK_XE_CRM_CONFIG, pcmk__str_casei)) { xml_obj = pcmk__xe_create(xml_obj, PCMK_XE_CLUSTER_PROPERTY_SET); } else { xml_obj = pcmk__xe_create(xml_obj, PCMK_XE_META_ATTRIBUTES); } } else if (set_type) { xml_obj = pcmk__xe_create(xml_obj, set_type); } else { xml_obj = pcmk__xe_create(xml_obj, PCMK_XE_INSTANCE_ATTRIBUTES); } pcmk__xe_set(xml_obj, PCMK_XA_ID, set_name); if (xml_top == NULL) { xml_top = xml_obj; } } do_modify: xml_obj = crm_create_nvpair_xml(xml_obj, attr_id, attr_name, attr_value); if (xml_top == NULL) { xml_top = xml_obj; } crm_log_xml_trace(xml_top, "update_attr"); rc = cib_internal_op(cib, PCMK__CIB_REQUEST_MODIFY, NULL, section, xml_top, NULL, call_options, user_name); if (!pcmk__is_set(call_options, cib_sync_call) && (cib->variant != cib_file) && (rc >= 0)) { // For async call, positive rc is the call ID (file always synchronous) rc = pcmk_rc_ok; } else { rc = pcmk_legacy2rc(rc); } if (rc != pcmk_rc_ok) { out->err(out, "Error setting %s=%s (section=%s, set=%s): %s", attr_name, attr_value, section, pcmk__s(set_name, ""), pcmk_rc_str(rc)); pcmk__log_xml_info(xml_top, "Update"); } free(local_set_name); free(local_attr_id); pcmk__xml_free(xml_top); return rc; } int cib__get_node_attrs(pcmk__output_t *out, cib_t *cib, const char *section, const char *node_uuid, const char *set_type, const char *set_name, const char *attr_id, const char *attr_name, const char *user_name, xmlNode **result) { int rc = pcmk_rc_ok; pcmk__assert(result != NULL); CRM_CHECK(section != NULL, return EINVAL); *result = NULL; rc = find_attr(cib, section, node_uuid, set_type, set_name, attr_id, attr_name, user_name, result); if (rc != pcmk_rc_ok) { pcmk__trace("Query failed for attribute %s (section=%s node=%s " "set=%s): %s", pcmk__s(attr_name, "with unspecified name"), section, pcmk__s(set_name, ""), pcmk__s(node_uuid, ""), pcmk_rc_str(rc)); } return rc; } int cib__delete_node_attr(pcmk__output_t *out, cib_t *cib, int options, const char *section, const char *node_uuid, const char *set_type, const char *set_name, const char *attr_id, const char *attr_name, const char *attr_value, const char *user_name) { int rc = pcmk_rc_ok; xmlNode *xml_obj = NULL; xmlNode *xml_search = NULL; char *local_attr_id = NULL; CRM_CHECK(section != NULL, return EINVAL); CRM_CHECK(attr_name != NULL || attr_id != NULL, return EINVAL); if (attr_id == NULL) { rc = find_attr(cib, section, node_uuid, set_type, set_name, attr_id, attr_name, user_name, &xml_search); if (rc != pcmk_rc_ok || handle_multiples(out, xml_search, attr_name) == ENOTUNIQ) { pcmk__xml_free(xml_search); return rc; } else { local_attr_id = pcmk__xe_get_copy(xml_search, PCMK_XA_ID); attr_id = local_attr_id; pcmk__xml_free(xml_search); } } xml_obj = crm_create_nvpair_xml(NULL, attr_id, attr_name, attr_value); rc = cib_internal_op(cib, PCMK__CIB_REQUEST_DELETE, NULL, section, xml_obj, NULL, options, user_name); if (!pcmk__is_set(options, cib_sync_call) && (cib->variant != cib_file) && (rc >= 0)) { // For async call, positive rc is the call ID (file always synchronous) rc = pcmk_rc_ok; } else { rc = pcmk_legacy2rc(rc); } if (rc == pcmk_rc_ok) { out->info(out, "Deleted %s %s: id=%s%s%s%s%s", section, node_uuid ? "attribute" : "option", local_attr_id, set_name ? " set=" : "", set_name ? set_name : "", attr_name ? " name=" : "", attr_name ? attr_name : ""); } free(local_attr_id); pcmk__xml_free(xml_obj); return rc; } int find_nvpair_attr_delegate(cib_t *cib, const char *attr, const char *section, const char *node_uuid, const char *attr_set_type, const char *set_name, const char *attr_id, const char *attr_name, gboolean to_console, char **value, const char *user_name) { pcmk__output_t *out = NULL; xmlNode *xml_search = NULL; int rc = pcmk_ok; out = new_output_object(to_console ? "text" : "log"); if (out == NULL) { return pcmk_err_generic; } rc = find_attr(cib, section, node_uuid, attr_set_type, set_name, attr_id, attr_name, user_name, &xml_search); if (rc == pcmk_rc_ok) { rc = handle_multiples(out, xml_search, attr_name); if (rc == pcmk_rc_ok) { pcmk__str_update(value, pcmk__xe_get(xml_search, attr)); } } out->finish(out, CRM_EX_OK, true, NULL); pcmk__xml_free(xml_search); pcmk__output_free(out); return pcmk_rc2legacy(rc); } int update_attr_delegate(cib_t *cib, int call_options, const char *section, const char *node_uuid, const char *set_type, const char *set_name, const char *attr_id, const char *attr_name, const char *attr_value, gboolean to_console, const char *user_name, const char *node_type) { pcmk__output_t *out = NULL; int rc = pcmk_ok; out = new_output_object(to_console ? "text" : "log"); if (out == NULL) { return pcmk_err_generic; } rc = cib__update_node_attr(out, cib, call_options, section, node_uuid, set_type, set_name, attr_id, attr_name, attr_value, user_name, node_type); out->finish(out, CRM_EX_OK, true, NULL); pcmk__output_free(out); return pcmk_rc2legacy(rc); } int read_attr_delegate(cib_t *cib, const char *section, const char *node_uuid, const char *set_type, const char *set_name, const char *attr_id, const char *attr_name, char **attr_value, gboolean to_console, const char *user_name) { pcmk__output_t *out = NULL; xmlNode *result = NULL; int rc = pcmk_ok; out = new_output_object(to_console ? "text" : "log"); if (out == NULL) { return pcmk_err_generic; } rc = cib__get_node_attrs(out, cib, section, node_uuid, set_type, set_name, attr_id, attr_name, user_name, &result); if (rc == pcmk_rc_ok) { if (result->children == NULL) { pcmk__str_update(attr_value, pcmk__xe_get(result, PCMK_XA_VALUE)); } else { rc = ENOTUNIQ; } } out->finish(out, CRM_EX_OK, true, NULL); pcmk__xml_free(result); pcmk__output_free(out); return pcmk_rc2legacy(rc); } int delete_attr_delegate(cib_t *cib, int options, const char *section, const char *node_uuid, const char *set_type, const char *set_name, const char *attr_id, const char *attr_name, const char *attr_value, gboolean to_console, const char *user_name) { pcmk__output_t *out = NULL; int rc = pcmk_ok; out = new_output_object(to_console ? "text" : "log"); if (out == NULL) { return pcmk_err_generic; } rc = cib__delete_node_attr(out, cib, options, section, node_uuid, set_type, set_name, attr_id, attr_name, attr_value, user_name); out->finish(out, CRM_EX_OK, true, NULL); pcmk__output_free(out); return pcmk_rc2legacy(rc); } /*! * \internal * \brief Parse node UUID from search result * * \param[in] result XML search result * \param[out] uuid If non-NULL, where to store parsed UUID * \param[out] is_remote If non-NULL, set TRUE if result is remote node * * \return pcmk_ok if UUID was successfully parsed, -ENXIO otherwise */ static int get_uuid_from_result(const xmlNode *result, char **uuid, int *is_remote) { int rc = -ENXIO; const char *parsed_uuid = NULL; int parsed_is_remote = FALSE; if (result == NULL) { return rc; } /* If there are multiple results, the first is sufficient */ if (pcmk__xe_is(result, PCMK__XE_XPATH_QUERY)) { result = pcmk__xe_first_child(result, NULL, NULL, NULL); CRM_CHECK(result != NULL, return rc); } if (pcmk__xe_is(result, PCMK_XE_NODE)) { // Result is PCMK_XE_NODE element from PCMK_XE_NODES section if (pcmk__str_eq(pcmk__xe_get(result, PCMK_XA_TYPE), PCMK_VALUE_REMOTE, pcmk__str_casei)) { parsed_uuid = pcmk__xe_get(result, PCMK_XA_UNAME); parsed_is_remote = TRUE; } else { parsed_uuid = pcmk__xe_id(result); parsed_is_remote = FALSE; } } else if (pcmk__xe_is(result, PCMK_XE_PRIMITIVE)) { /* Result is for ocf:pacemaker:remote resource */ parsed_uuid = pcmk__xe_id(result); parsed_is_remote = TRUE; } else if (pcmk__xe_is(result, PCMK_XE_NVPAIR)) { /* Result is PCMK_META_REMOTE_NODE parameter of for guest * node */ parsed_uuid = pcmk__xe_get(result, PCMK_XA_VALUE); parsed_is_remote = TRUE; } else if (pcmk__xe_is(result, PCMK__XE_NODE_STATE)) { // Result is PCMK__XE_NODE_STATE element from PCMK_XE_STATUS section parsed_uuid = pcmk__xe_get(result, PCMK_XA_UNAME); if (pcmk__xe_attr_is_true(result, PCMK_XA_REMOTE_NODE)) { parsed_is_remote = TRUE; } } if (parsed_uuid) { if (uuid) { *uuid = strdup(parsed_uuid); } if (is_remote) { *is_remote = parsed_is_remote; } rc = pcmk_ok; } return rc; } /* Search string to find a node by name, as: * - cluster or remote node in nodes section * - remote node in resources section * - guest node in resources section * - orphaned remote node or bundle guest node in status section */ #define XPATH_UPPER_TRANS "ABCDEFGHIJKLMNOPQRSTUVWXYZ" #define XPATH_LOWER_TRANS "abcdefghijklmnopqrstuvwxyz" #define XPATH_NODE \ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_NODES \ "/" PCMK_XE_NODE "[translate(@" PCMK_XA_UNAME ",'" XPATH_UPPER_TRANS "','" XPATH_LOWER_TRANS "') ='%s']" \ "|/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RESOURCES \ "/" PCMK_XE_PRIMITIVE \ "[@class='ocf'][@provider='pacemaker'][@type='remote'][translate(@id,'" XPATH_UPPER_TRANS "','" XPATH_LOWER_TRANS "') ='%s']" \ "|/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RESOURCES \ "/" PCMK_XE_PRIMITIVE "/" PCMK_XE_META_ATTRIBUTES "/" PCMK_XE_NVPAIR \ "[@name='" PCMK_META_REMOTE_NODE "'][translate(@value,'" XPATH_UPPER_TRANS "','" XPATH_LOWER_TRANS "') ='%s']" \ "|/" PCMK_XE_CIB "/" PCMK_XE_STATUS "/" PCMK__XE_NODE_STATE \ "[@" PCMK_XA_REMOTE_NODE "='true'][translate(@" PCMK_XA_ID ",'" XPATH_UPPER_TRANS "','" XPATH_LOWER_TRANS "') ='%s']" int query_node_uuid(cib_t * the_cib, const char *uname, char **uuid, int *is_remote_node) { int rc = pcmk_ok; char *xpath_string; xmlNode *xml_search = NULL; char *host_lowercase = NULL; pcmk__assert(uname != NULL); host_lowercase = g_ascii_strdown(uname, -1); if (uuid) { *uuid = NULL; } if (is_remote_node) { *is_remote_node = FALSE; } xpath_string = pcmk__assert_asprintf(XPATH_NODE, host_lowercase, host_lowercase, host_lowercase, host_lowercase); if (cib_internal_op(the_cib, PCMK__CIB_REQUEST_QUERY, NULL, xpath_string, NULL, &xml_search, cib_sync_call|cib_xpath, NULL) == pcmk_ok) { rc = get_uuid_from_result(xml_search, uuid, is_remote_node); } else { rc = -ENXIO; } free(xpath_string); pcmk__xml_free(xml_search); g_free(host_lowercase); if (rc != pcmk_ok) { pcmk__debug("Could not map node name '%s' to a UUID: %s", uname, pcmk_strerror(rc)); } else { pcmk__info("Mapped node name '%s' to UUID %s", uname, ((uuid != NULL)? *uuid : "")); } return rc; } diff --git a/lib/cib/cib_remote.c b/lib/cib/cib_remote.c index 2796b42985..3eaa223b58 100644 --- a/lib/cib/cib_remote.c +++ b/lib/cib/cib_remote.c @@ -1,662 +1,662 @@ /* * Copyright 2008-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 // GnuTLS handshake timeout in seconds #define TLS_HANDSHAKE_TIMEOUT 5 static pcmk__tls_t *tls = NULL; #include typedef struct cib_remote_opaque_s { int port; char *server; char *user; char *passwd; gboolean encrypted; pcmk__remote_t command; pcmk__remote_t callback; pcmk__output_t *out; time_t start_time; int timeout_sec; } cib_remote_opaque_t; static int cib_remote_perform_op(cib_t *cib, const char *op, const char *host, const char *section, xmlNode *data, xmlNode **output_data, int call_options, const char *user_name) { int rc; int remaining_time = 0; time_t start_time; xmlNode *op_msg = NULL; xmlNode *op_reply = NULL; cib_remote_opaque_t *private = cib->variant_opaque; if (cib->state == cib_disconnected) { return -ENOTCONN; } if (output_data != NULL) { *output_data = NULL; } if (op == NULL) { pcmk__err("No operation specified"); return -EINVAL; } rc = cib__create_op(cib, op, host, section, data, call_options, user_name, NULL, &op_msg); if (rc != pcmk_ok) { return rc; } if (pcmk__is_set(call_options, cib_transaction)) { rc = cib__extend_transaction(cib, op_msg); pcmk__xml_free(op_msg); return rc; } pcmk__trace("Sending %s message to the CIB manager", op); if (!(call_options & cib_sync_call)) { pcmk__remote_send_xml(&private->callback, op_msg); } else { pcmk__remote_send_xml(&private->command, op_msg); } pcmk__xml_free(op_msg); if ((call_options & cib_discard_reply)) { pcmk__trace("Discarding reply"); return pcmk_ok; } else if (!(call_options & cib_sync_call)) { return cib->call_id; } pcmk__trace("Waiting for a synchronous reply"); start_time = time(NULL); remaining_time = cib->call_timeout ? cib->call_timeout : 60; rc = pcmk_rc_ok; while (remaining_time > 0 && (rc != ENOTCONN)) { int reply_id = -1; int msg_id = cib->call_id; rc = pcmk__read_remote_message(&private->command, remaining_time * 1000); op_reply = pcmk__remote_message_xml(&private->command); if (!op_reply) { break; } pcmk__xe_get_int(op_reply, PCMK__XA_CIB_CALLID, &reply_id); if (reply_id == msg_id) { break; } else if (reply_id < msg_id) { pcmk__debug("Received old reply: %d (wanted %d)", reply_id, msg_id); crm_log_xml_trace(op_reply, "Old reply"); } else if ((reply_id - 10000) > msg_id) { /* wrap-around case */ pcmk__debug("Received old reply: %d (wanted %d)", reply_id, msg_id); crm_log_xml_trace(op_reply, "Old reply"); } else { pcmk__err("Received a __future__ reply:" " %d (wanted %d)", reply_id, msg_id); } pcmk__xml_free(op_reply); op_reply = NULL; /* wasn't the right reply, try and read some more */ remaining_time = time(NULL) - start_time; } if (rc == ENOTCONN) { pcmk__err("Disconnected while waiting for reply"); return -ENOTCONN; } else if (op_reply == NULL) { pcmk__err("No reply message - empty"); return -ENOMSG; } pcmk__trace("Synchronous reply received"); /* Start processing the reply... */ if (pcmk__xe_get_int(op_reply, PCMK__XA_CIB_RC, &rc) != pcmk_rc_ok) { rc = -EPROTO; } if (rc == -pcmk_err_diff_resync) { /* This is an internal value that clients do not and should not care about */ rc = pcmk_ok; } if (rc == pcmk_ok || rc == -EPERM) { - crm_log_xml_debug(op_reply, "passed"); + pcmk__log_xml_debug(op_reply, "passed"); } else { pcmk__err("Call failed: %s", pcmk_strerror(rc)); pcmk__log_xml_warn(op_reply, "failed"); } if (output_data == NULL) { /* do nothing more */ } else if (!(call_options & cib_discard_reply)) { xmlNode *wrapper = pcmk__xe_first_child(op_reply, PCMK__XE_CIB_CALLDATA, NULL, NULL); xmlNode *tmp = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); if (tmp == NULL) { pcmk__trace("No output in reply to \"%s\" command %d", op, (cib->call_id - 1)); } else { *output_data = pcmk__xml_copy(NULL, tmp); } } pcmk__xml_free(op_reply); return rc; } static int cib_remote_callback_dispatch(gpointer user_data) { int rc; cib_t *cib = user_data; cib_remote_opaque_t *private = cib->variant_opaque; xmlNode *msg = NULL; const char *type = NULL; /* If start time is 0, we've previously handled a complete message and this * connection is being reused for a new message. Reset the start_time, * giving this new message timeout_sec from now to complete. */ if (private->start_time == 0) { private->start_time = time(NULL); } rc = pcmk__read_available_remote_data(&private->callback); switch (rc) { case pcmk_rc_ok: /* We have the whole message so process it */ break; case EAGAIN: /* Have we timed out? */ if (time(NULL) >= private->start_time + private->timeout_sec) { pcmk__info("Error reading from CIB manager connection: %s", pcmk_rc_str(ETIME)); return -1; } /* We haven't read the whole message yet */ return 0; default: /* Error */ pcmk__info("Error reading from CIB manager connection: %s", pcmk_rc_str(rc)); return -1; } msg = pcmk__remote_message_xml(&private->callback); if (msg == NULL) { private->start_time = 0; return 0; } type = pcmk__xe_get(msg, PCMK__XA_T); pcmk__trace("Activating %s callbacks...", type); if (pcmk__str_eq(type, PCMK__VALUE_CIB, pcmk__str_none)) { cib_native_callback(cib, msg, 0, 0); } else if (pcmk__str_eq(type, PCMK__VALUE_CIB_NOTIFY, pcmk__str_none)) { g_list_foreach(cib->notify_list, cib_native_notify, msg); } else { pcmk__err("Unknown message type: %s", type); } pcmk__xml_free(msg); private->start_time = 0; return 0; } static int cib_remote_command_dispatch(gpointer user_data) { int rc; cib_t *cib = user_data; cib_remote_opaque_t *private = cib->variant_opaque; /* See cib_remote_callback_dispatch */ if (private->start_time == 0) { private->start_time = time(NULL); } rc = pcmk__read_available_remote_data(&private->command); if (rc == EAGAIN) { /* Have we timed out? */ if (time(NULL) >= private->start_time + private->timeout_sec) { pcmk__info("Error reading from CIB manager connection: %s", pcmk_rc_str(ETIME)); return -1; } /* We haven't read the whole message yet */ return 0; } free(private->command.buffer); private->command.buffer = NULL; pcmk__err("Received late reply for remote cib connection, discarding"); if (rc != pcmk_rc_ok) { pcmk__info("Error reading from CIB manager connection: %s", pcmk_rc_str(rc)); return -1; } private->start_time = 0; return 0; } static int cib_tls_close(cib_t *cib) { cib_remote_opaque_t *private = cib->variant_opaque; if (private->encrypted) { if (private->command.tls_session) { gnutls_bye(private->command.tls_session, GNUTLS_SHUT_RDWR); gnutls_deinit(private->command.tls_session); } if (private->callback.tls_session) { gnutls_bye(private->callback.tls_session, GNUTLS_SHUT_RDWR); gnutls_deinit(private->callback.tls_session); } private->command.tls_session = NULL; private->callback.tls_session = NULL; pcmk__free_tls(tls); tls = NULL; } if (private->command.tcp_socket >= 0) { shutdown(private->command.tcp_socket, SHUT_RDWR); /* no more receptions */ close(private->command.tcp_socket); } if (private->callback.tcp_socket >= 0) { shutdown(private->callback.tcp_socket, SHUT_RDWR); /* no more receptions */ close(private->callback.tcp_socket); } private->command.tcp_socket = -1; private->callback.tcp_socket = -1; free(private->command.buffer); free(private->callback.buffer); private->command.buffer = NULL; private->callback.buffer = NULL; return 0; } static void cib_remote_connection_destroy(gpointer user_data) { pcmk__err("Connection destroyed"); cib_tls_close(user_data); } static int cib_tls_signon(cib_t *cib, pcmk__remote_t *connection, gboolean event_channel) { cib_remote_opaque_t *private = cib->variant_opaque; int rc; xmlNode *answer = NULL; xmlNode *login = NULL; static struct mainloop_fd_callbacks cib_fd_callbacks = { 0, }; cib_fd_callbacks.dispatch = event_channel ? cib_remote_callback_dispatch : cib_remote_command_dispatch; cib_fd_callbacks.destroy = cib_remote_connection_destroy; connection->tcp_socket = -1; connection->tls_session = NULL; rc = pcmk__connect_remote(private->server, private->port, 0, NULL, &(connection->tcp_socket), NULL, NULL); if (rc != pcmk_rc_ok) { pcmk__info("Remote connection to %s:%d failed: %s " QB_XS " rc=%d", private->server, private->port, pcmk_rc_str(rc), rc); return -ENOTCONN; } if (private->encrypted) { bool use_cert = pcmk__x509_enabled(); int tls_rc = GNUTLS_E_SUCCESS; rc = pcmk__init_tls(&tls, false, use_cert ? GNUTLS_CRD_CERTIFICATE : GNUTLS_CRD_ANON); if (rc != pcmk_rc_ok) { return -1; } /* bind the socket to GnuTls lib */ connection->tls_session = pcmk__new_tls_session(tls, connection->tcp_socket); if (connection->tls_session == NULL) { cib_tls_close(cib); return -1; } rc = pcmk__tls_client_handshake(connection, TLS_HANDSHAKE_TIMEOUT, &tls_rc); if (rc != pcmk_rc_ok) { const bool proto_err = (rc == EPROTO); pcmk__err("Remote CIB session creation for %s:%d failed: %s", private->server, private->port, (proto_err? gnutls_strerror(tls_rc) : pcmk_rc_str(rc))); gnutls_deinit(connection->tls_session); connection->tls_session = NULL; cib_tls_close(cib); return -1; } } /* 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(connection->tls_session); /* login to server */ login = pcmk__xe_create(NULL, PCMK__XE_CIB_COMMAND); pcmk__xe_set(login, PCMK_XA_OP, "authenticate"); pcmk__xe_set(login, PCMK_XA_USER, private->user); pcmk__xe_set(login, PCMK__XA_PASSWORD, private->passwd); pcmk__xe_set(login, PCMK__XA_HIDDEN, PCMK__VALUE_PASSWORD); pcmk__remote_send_xml(connection, login); pcmk__xml_free(login); rc = pcmk_ok; if (pcmk__read_remote_message(connection, -1) == ENOTCONN) { rc = -ENOTCONN; } answer = pcmk__remote_message_xml(connection); crm_log_xml_trace(answer, "Reply"); if (answer == NULL) { rc = -EPROTO; } else { /* grab the token */ const char *msg_type = pcmk__xe_get(answer, PCMK__XA_CIB_OP); const char *tmp_ticket = pcmk__xe_get(answer, PCMK__XA_CIB_CLIENTID); if (!pcmk__str_eq(msg_type, CRM_OP_REGISTER, pcmk__str_casei)) { pcmk__err("Invalid registration message: %s", msg_type); rc = -EPROTO; } else if (tmp_ticket == NULL) { rc = -EPROTO; } else { connection->token = strdup(tmp_ticket); } } pcmk__xml_free(answer); answer = NULL; if (rc != 0) { cib_tls_close(cib); return rc; } pcmk__trace("remote client connection established"); private->timeout_sec = 60; connection->source = mainloop_add_fd("cib-remote", G_PRIORITY_HIGH, connection->tcp_socket, cib, &cib_fd_callbacks); return rc; } static int cib_remote_signon(cib_t *cib, const char *name, enum cib_conn_type type) { int rc = pcmk_ok; cib_remote_opaque_t *private = cib->variant_opaque; if (name == NULL) { name = pcmk__s(crm_system_name, "client"); } if (private->passwd == NULL) { if (private->out == NULL) { /* If no pcmk__output_t is set, just assume that a text prompt * is good enough. */ pcmk__text_prompt("Password", false, &(private->passwd)); } else { private->out->prompt("Password", false, &(private->passwd)); } } if (private->server == NULL || private->user == NULL) { rc = -EINVAL; goto done; } rc = cib_tls_signon(cib, &(private->command), FALSE); if (rc != pcmk_ok) { goto done; } rc = cib_tls_signon(cib, &(private->callback), TRUE); done: if (rc == pcmk_ok) { pcmk__info("Opened connection to %s:%d for %s", private->server, private->port, name); cib->state = cib_connected_command; cib->type = cib_command; } else { pcmk__info("Connection to %s:%d for %s failed: %s\n", private->server, private->port, name, pcmk_strerror(rc)); } return rc; } static int cib_remote_signoff(cib_t *cib) { int rc = pcmk_ok; pcmk__debug("Disconnecting from the CIB manager"); cib_tls_close(cib); cib->cmds->end_transaction(cib, false, cib_none); cib->state = cib_disconnected; cib->type = cib_no_connection; return rc; } static int cib_remote_free(cib_t *cib) { int rc = pcmk_ok; pcmk__warn("Freeing CIB"); if (cib->state != cib_disconnected) { rc = cib_remote_signoff(cib); if (rc == pcmk_ok) { cib_remote_opaque_t *private = cib->variant_opaque; free(private->server); free(private->user); free(private->passwd); free(cib->cmds); free(cib->user); free(private); free(cib); } } return rc; } static int cib_remote_register_notification(cib_t * cib, const char *callback, int enabled) { xmlNode *notify_msg = pcmk__xe_create(NULL, PCMK__XE_CIB_COMMAND); cib_remote_opaque_t *private = cib->variant_opaque; pcmk__xe_set(notify_msg, PCMK__XA_CIB_OP, PCMK__VALUE_CIB_NOTIFY); pcmk__xe_set(notify_msg, PCMK__XA_CIB_NOTIFY_TYPE, callback); pcmk__xe_set_int(notify_msg, PCMK__XA_CIB_NOTIFY_ACTIVATE, enabled); pcmk__remote_send_xml(&private->callback, notify_msg); pcmk__xml_free(notify_msg); return pcmk_ok; } static int cib_remote_set_connection_dnotify(cib_t * cib, void (*dnotify) (gpointer user_data)) { return -EPROTONOSUPPORT; } /*! * \internal * \brief Get the given CIB connection's unique client identifiers * * These can be used to check whether this client requested the action that * triggered a CIB notification. * * \param[in] cib CIB connection * \param[out] async_id If not \p NULL, where to store asynchronous client ID * \param[out] sync_id If not \p NULL, where to store synchronous client ID * * \return Legacy Pacemaker return code (specifically, \p pcmk_ok) * * \note This is the \p cib_remote variant implementation of * \p cib_api_operations_t:client_id(). * \note The client IDs are assigned during CIB sign-on. */ static int cib_remote_client_id(const cib_t *cib, const char **async_id, const char **sync_id) { cib_remote_opaque_t *private = cib->variant_opaque; if (async_id != NULL) { // private->callback is the channel for async requests *async_id = private->callback.token; } if (sync_id != NULL) { // private->command is the channel for sync requests *sync_id = private->command.token; } return pcmk_ok; } cib_t * cib_remote_new(const char *server, const char *user, const char *passwd, int port, gboolean encrypted) { cib_remote_opaque_t *private = NULL; cib_t *cib = cib_new_variant(); if (cib == NULL) { return NULL; } private = calloc(1, sizeof(cib_remote_opaque_t)); if (private == NULL) { free(cib); return NULL; } cib->variant = cib_remote; cib->variant_opaque = private; private->server = pcmk__str_copy(server); private->user = pcmk__str_copy(user); private->passwd = pcmk__str_copy(passwd); private->port = port; private->encrypted = encrypted; /* assign variant specific ops */ cib->delegate_fn = cib_remote_perform_op; cib->cmds->signon = cib_remote_signon; cib->cmds->signoff = cib_remote_signoff; cib->cmds->free = cib_remote_free; cib->cmds->register_notification = cib_remote_register_notification; cib->cmds->set_connection_dnotify = cib_remote_set_connection_dnotify; cib->cmds->client_id = cib_remote_client_id; return cib; } void cib__set_output(cib_t *cib, pcmk__output_t *out) { cib_remote_opaque_t *private; if (cib->variant != cib_remote) { return; } private = cib->variant_opaque; private->out = out; } diff --git a/lib/cib/cib_utils.c b/lib/cib/cib_utils.c index f6e20b9ac1..ace576eb65 100644 --- a/lib/cib/cib_utils.c +++ b/lib/cib/cib_utils.c @@ -1,968 +1,968 @@ /* * Original copyright 2004 International Business Machines * Later changes copyright 2008-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 // pcmk_acl_required(), etc. #include #include // pcmk_unpack_nvpair_blocks() #include #include gboolean cib_version_details(xmlNode * cib, int *admin_epoch, int *epoch, int *updates) { *epoch = -1; *updates = -1; *admin_epoch = -1; if (cib == NULL) { return FALSE; } pcmk__xe_get_int(cib, PCMK_XA_EPOCH, epoch); pcmk__xe_get_int(cib, PCMK_XA_NUM_UPDATES, updates); pcmk__xe_get_int(cib, PCMK_XA_ADMIN_EPOCH, admin_epoch); return TRUE; } gboolean cib_diff_version_details(xmlNode * diff, int *admin_epoch, int *epoch, int *updates, int *_admin_epoch, int *_epoch, int *_updates) { int add[] = { 0, 0, 0 }; int del[] = { 0, 0, 0 }; pcmk__xml_patchset_versions(diff, del, add); *admin_epoch = add[0]; *epoch = add[1]; *updates = add[2]; *_admin_epoch = del[0]; *_epoch = del[1]; *_updates = del[2]; return TRUE; } /*! * \internal * \brief Get the XML patchset from a CIB diff notification * * \param[in] msg CIB diff notification * \param[out] patchset Where to store XML patchset * * \return Standard Pacemaker return code */ int cib__get_notify_patchset(const xmlNode *msg, const xmlNode **patchset) { int rc = pcmk_err_generic; xmlNode *wrapper = NULL; pcmk__assert(patchset != NULL); *patchset = NULL; if (msg == NULL) { pcmk__err("CIB diff notification received with no XML"); return ENOMSG; } if ((pcmk__xe_get_int(msg, PCMK__XA_CIB_RC, &rc) != pcmk_rc_ok) || (rc != pcmk_ok)) { pcmk__warn("Ignore failed CIB update: %s " QB_XS " rc=%d", pcmk_strerror(rc), rc); - crm_log_xml_debug(msg, "failed"); + pcmk__log_xml_debug(msg, "failed"); return pcmk_legacy2rc(rc); } wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_UPDATE_RESULT, NULL, NULL); *patchset = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); if (*patchset == NULL) { pcmk__err("CIB diff notification received with no patchset"); return ENOMSG; } return pcmk_rc_ok; } /*! * \brief Create XML for a new (empty) CIB * * \param[in] cib_epoch What to use as \c PCMK_XA_EPOCH CIB attribute * * \return Newly created XML for empty CIB * * \note It is the caller's responsibility to free the result with * \c pcmk__xml_free(). */ xmlNode * createEmptyCib(int cib_epoch) { xmlNode *cib_root = NULL, *config = NULL; cib_root = pcmk__xe_create(NULL, PCMK_XE_CIB); pcmk__xe_set(cib_root, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET); pcmk__xe_set(cib_root, PCMK_XA_VALIDATE_WITH, pcmk__highest_schema_name()); pcmk__xe_set_int(cib_root, PCMK_XA_EPOCH, cib_epoch); pcmk__xe_set_int(cib_root, PCMK_XA_NUM_UPDATES, 0); pcmk__xe_set_int(cib_root, PCMK_XA_ADMIN_EPOCH, 0); config = pcmk__xe_create(cib_root, PCMK_XE_CONFIGURATION); pcmk__xe_create(cib_root, PCMK_XE_STATUS); pcmk__xe_create(config, PCMK_XE_CRM_CONFIG); pcmk__xe_create(config, PCMK_XE_NODES); pcmk__xe_create(config, PCMK_XE_RESOURCES); pcmk__xe_create(config, PCMK_XE_CONSTRAINTS); #if PCMK__RESOURCE_STICKINESS_DEFAULT != 0 { xmlNode *rsc_defaults = pcmk__xe_create(config, PCMK_XE_RSC_DEFAULTS); xmlNode *meta = pcmk__xe_create(rsc_defaults, PCMK_XE_META_ATTRIBUTES); xmlNode *nvpair = pcmk__xe_create(meta, PCMK_XE_NVPAIR); pcmk__xe_set(meta, PCMK_XA_ID, "build-resource-defaults"); pcmk__xe_set(nvpair, PCMK_XA_ID, "build-" PCMK_META_RESOURCE_STICKINESS); pcmk__xe_set(nvpair, PCMK_XA_NAME, PCMK_META_RESOURCE_STICKINESS); pcmk__xe_set_int(nvpair, PCMK_XA_VALUE, PCMK__RESOURCE_STICKINESS_DEFAULT); } #endif return cib_root; } static bool cib_acl_enabled(xmlNode *xml, const char *user) { bool rc = false; if(pcmk_acl_required(user)) { const char *value = NULL; GHashTable *options = pcmk__strkey_table(free, free); cib_read_config(options, xml); value = pcmk__cluster_option(options, PCMK_OPT_ENABLE_ACL); rc = pcmk__is_true(value); g_hash_table_destroy(options); } pcmk__trace("CIB ACL is %s", (rc? "enabled" : "disabled")); return rc; } /*! * \internal * \brief Determine whether to perform operations on a scratch copy of the CIB * * \param[in] op CIB operation * \param[in] section CIB section * \param[in] call_options CIB call options * * \return \p true if we should make a copy of the CIB, or \p false otherwise */ static bool should_copy_cib(const char *op, const char *section, int call_options) { if (pcmk__is_set(call_options, cib_dryrun)) { // cib_dryrun implies a scratch copy by definition; no side effects return true; } if (pcmk__str_eq(op, PCMK__CIB_REQUEST_COMMIT_TRANSACT, pcmk__str_none)) { /* Commit-transaction must make a copy for atomicity. We must revert to * the original CIB if the entire transaction cannot be applied * successfully. */ return true; } if (pcmk__is_set(call_options, cib_transaction)) { /* If cib_transaction is set, then we're in the process of committing a * transaction. The commit-transaction request already made a scratch * copy, and we're accumulating changes in that copy. */ return false; } if (pcmk__str_eq(section, PCMK_XE_STATUS, pcmk__str_none)) { /* Copying large CIBs accounts for a huge percentage of our CIB usage, * and this avoids some of it. * * @TODO: Is this safe? See discussion at * https://github.com/ClusterLabs/pacemaker/pull/3094#discussion_r1211400690. */ return false; } // Default behavior is to operate on a scratch copy return true; } int cib_perform_op(cib_t *cib, const char *op, uint32_t call_options, cib__op_fn_t fn, bool is_query, const char *section, xmlNode *req, xmlNode *input, bool manage_counters, bool *config_changed, xmlNode **current_cib, xmlNode **result_cib, xmlNode **diff, xmlNode **output) { int rc = pcmk_ok; bool check_schema = true; bool make_copy = true; xmlNode *top = NULL; xmlNode *scratch = NULL; xmlNode *patchset_cib = NULL; xmlNode *local_diff = NULL; const char *user = pcmk__xe_get(req, PCMK__XA_CIB_USER); const bool enable_acl = cib_acl_enabled(*current_cib, user); bool with_digest = false; pcmk__trace("Begin %s%s%s op", (pcmk__is_set(call_options, cib_dryrun)? "dry run of " : ""), (is_query? "read-only " : ""), op); CRM_CHECK(output != NULL, return -ENOMSG); CRM_CHECK(current_cib != NULL, return -ENOMSG); CRM_CHECK(result_cib != NULL, return -ENOMSG); CRM_CHECK(config_changed != NULL, return -ENOMSG); if(output) { *output = NULL; } *result_cib = NULL; *config_changed = false; if (fn == NULL) { return -EINVAL; } if (is_query) { xmlNode *cib_ro = *current_cib; xmlNode *cib_filtered = NULL; if (enable_acl && xml_acl_filtered_copy(user, *current_cib, *current_cib, &cib_filtered)) { if (cib_filtered == NULL) { pcmk__debug("Pre-filtered the entire cib"); return -EACCES; } cib_ro = cib_filtered; crm_log_xml_trace(cib_ro, "filtered"); } rc = (*fn) (op, call_options, section, req, input, cib_ro, result_cib, output); if(output == NULL || *output == NULL) { /* nothing */ } else if(cib_filtered == *output) { cib_filtered = NULL; /* Let them have this copy */ } else if (*output == *current_cib) { /* They already know not to free it */ } else if(cib_filtered && (*output)->doc == cib_filtered->doc) { /* We're about to free the document of which *output is a part */ *output = pcmk__xml_copy(NULL, *output); } else if ((*output)->doc == (*current_cib)->doc) { /* Give them a copy they can free */ *output = pcmk__xml_copy(NULL, *output); } pcmk__xml_free(cib_filtered); return rc; } make_copy = should_copy_cib(op, section, call_options); if (!make_copy) { /* Conditional on v2 patch style */ scratch = *current_cib; // Make a copy of the top-level element to store version details top = pcmk__xe_create(NULL, (const char *) scratch->name); pcmk__xe_copy_attrs(top, scratch, pcmk__xaf_none); patchset_cib = top; pcmk__xml_commit_changes(scratch->doc); pcmk__xml_doc_set_flags(scratch->doc, pcmk__xf_tracking); if (enable_acl) { pcmk__enable_acl(*current_cib, scratch, user); } rc = (*fn) (op, call_options, section, req, input, scratch, &scratch, output); /* If scratch points to a new object now (for example, after an erase * operation), then *current_cib should point to the same object. * * @TODO Enable tracking and ACLs and calculate changes? Change tracking * and unpacked ACLs didn't carry over to new object. */ *current_cib = scratch; } else { scratch = pcmk__xml_copy(NULL, *current_cib); patchset_cib = *current_cib; pcmk__xml_doc_set_flags(scratch->doc, pcmk__xf_tracking); if (enable_acl) { pcmk__enable_acl(*current_cib, scratch, user); } rc = (*fn) (op, call_options, section, req, input, *current_cib, &scratch, output); /* @TODO This appears to be a hack to determine whether scratch points * to a new object now, without saving the old pointer (which may be * invalid now) for comparison. Confirm this, and check more clearly. */ if (!pcmk__xml_doc_all_flags_set(scratch->doc, pcmk__xf_tracking)) { pcmk__trace("Inferring changes after %s op", op); pcmk__xml_commit_changes(scratch->doc); if (enable_acl) { pcmk__enable_acl(*current_cib, scratch, user); } pcmk__xml_mark_changes(*current_cib, scratch); } CRM_CHECK(*current_cib != scratch, return -EINVAL); } xml_acl_disable(scratch); /* Allow the system to make any additional changes */ if (rc == pcmk_ok && scratch == NULL) { rc = -EINVAL; goto done; } else if(rc == pcmk_ok && xml_acl_denied(scratch)) { pcmk__trace("ACL rejected part or all of the proposed changes"); rc = -EACCES; goto done; } else if (rc != pcmk_ok) { goto done; } /* If the CIB is from a file, we don't need to check that the feature set is * supported. All we care about in that case is the schema version, which * is checked elsewhere. */ if (scratch && (cib == NULL || cib->variant != cib_file)) { const char *new_version = pcmk__xe_get(scratch, PCMK_XA_CRM_FEATURE_SET); rc = pcmk__check_feature_set(new_version); if (rc != pcmk_rc_ok) { pcmk__err("Discarding update with feature set '%s' greater than " "our own '%s'", new_version, CRM_FEATURE_SET); rc = pcmk_rc2legacy(rc); goto done; } } if (patchset_cib != NULL) { int old = 0; int new = 0; pcmk__xe_get_int(scratch, PCMK_XA_ADMIN_EPOCH, &new); pcmk__xe_get_int(patchset_cib, PCMK_XA_ADMIN_EPOCH, &old); if (old > new) { pcmk__err("%s went backwards: %d -> %d (Opts: %#x)", PCMK_XA_ADMIN_EPOCH, old, new, call_options); pcmk__log_xml_warn(req, "Bad Op"); pcmk__log_xml_warn(input, "Bad Data"); rc = -pcmk_err_old_data; } else if (old == new) { pcmk__xe_get_int(scratch, PCMK_XA_EPOCH, &new); pcmk__xe_get_int(patchset_cib, PCMK_XA_EPOCH, &old); if (old > new) { pcmk__err("%s went backwards: %d -> %d (Opts: %#x)", PCMK_XA_EPOCH, old, new, call_options); pcmk__log_xml_warn(req, "Bad Op"); pcmk__log_xml_warn(input, "Bad Data"); rc = -pcmk_err_old_data; } } } pcmk__trace("Massaging CIB contents"); pcmk__strip_xml_text(scratch); if (make_copy) { static time_t expires = 0; time_t tm_now = time(NULL); if (expires < tm_now) { expires = tm_now + 60; /* Validate clients are correctly applying v2-style diffs at most once a minute */ with_digest = true; } } local_diff = xml_create_patchset(0, patchset_cib, scratch, config_changed, manage_counters); pcmk__log_xml_changes(PCMK__LOG_TRACE, scratch); pcmk__xml_commit_changes(scratch->doc); if(local_diff) { if (with_digest) { pcmk__xml_patchset_add_digest(local_diff, scratch); } pcmk__log_xml_patchset(LOG_INFO, local_diff); crm_log_xml_trace(local_diff, "raw patch"); } if (make_copy && (local_diff != NULL)) { // Original to compare against doesn't exist pcmk__if_tracing( { // Validate the calculated patch set int test_rc = pcmk_ok; int format = 1; xmlNode *cib_copy = pcmk__xml_copy(NULL, patchset_cib); pcmk__xe_get_int(local_diff, PCMK_XA_FORMAT, &format); test_rc = xml_apply_patchset(cib_copy, local_diff, manage_counters); if (test_rc != pcmk_ok) { pcmk__xml_write_temp_file(cib_copy, "PatchApply:calculated", NULL); pcmk__xml_write_temp_file(patchset_cib, "PatchApply:input", NULL); pcmk__xml_write_temp_file(scratch, "PatchApply:actual", NULL); pcmk__xml_write_temp_file(local_diff, "PatchApply:diff", NULL); pcmk__err("v%d patchset error, patch failed to apply: %s " "(%d)", format, pcmk_rc_str(pcmk_legacy2rc(test_rc)), test_rc); } pcmk__xml_free(cib_copy); }, {} ); } if (pcmk__str_eq(section, PCMK_XE_STATUS, pcmk__str_casei)) { /* Throttle the amount of costly validation we perform due to status updates * a) we don't really care whats in the status section * b) we don't validate any of its contents at the moment anyway */ check_schema = false; } /* === scratch must not be modified after this point === * Exceptions, anything in: static filter_t filter[] = { { 0, PCMK_XA_CRM_DEBUG_ORIGIN }, { 0, PCMK_XA_CIB_LAST_WRITTEN }, { 0, PCMK_XA_UPDATE_ORIGIN }, { 0, PCMK_XA_UPDATE_CLIENT }, { 0, PCMK_XA_UPDATE_USER }, }; */ if (*config_changed && !pcmk__is_set(call_options, cib_no_mtime)) { const char *schema = pcmk__xe_get(scratch, PCMK_XA_VALIDATE_WITH); if (schema == NULL) { rc = -pcmk_err_cib_corrupt; } pcmk__xe_add_last_written(scratch); pcmk__warn_if_schema_deprecated(schema); /* Make values of origin, client, and user in scratch match * the ones in req (if the schema allows the attributes) */ if (pcmk__cmp_schemas_by_name(schema, "pacemaker-1.2") >= 0) { const char *origin = pcmk__xe_get(req, PCMK__XA_SRC); const char *client = pcmk__xe_get(req, PCMK__XA_CIB_CLIENTNAME); if (origin != NULL) { pcmk__xe_set(scratch, PCMK_XA_UPDATE_ORIGIN, origin); } else { pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_ORIGIN); } if (client != NULL) { pcmk__xe_set(scratch, PCMK_XA_UPDATE_CLIENT, user); } else { pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_CLIENT); } if (user != NULL) { pcmk__xe_set(scratch, PCMK_XA_UPDATE_USER, user); } else { pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_USER); } } } pcmk__trace("Perform validation: %s", pcmk__btoa(check_schema)); if ((rc == pcmk_ok) && check_schema && !pcmk__configured_schema_validates(scratch)) { rc = -pcmk_err_schema_validation; } done: *result_cib = scratch; /* @TODO: This may not work correctly with !make_copy, since we don't * keep the original CIB. */ if ((rc != pcmk_ok) && cib_acl_enabled(patchset_cib, user) && xml_acl_filtered_copy(user, patchset_cib, scratch, result_cib)) { if (*result_cib == NULL) { pcmk__debug("Pre-filtered the entire cib result"); } pcmk__xml_free(scratch); } if(diff) { *diff = local_diff; } else { pcmk__xml_free(local_diff); } pcmk__xml_free(top); pcmk__trace("Done"); return rc; } int cib__create_op(cib_t *cib, const char *op, const char *host, const char *section, xmlNode *data, int call_options, const char *user_name, const char *client_name, xmlNode **op_msg) { CRM_CHECK((cib != NULL) && (op_msg != NULL), return -EPROTO); *op_msg = pcmk__xe_create(NULL, PCMK__XE_CIB_COMMAND); cib->call_id++; if (cib->call_id < 1) { cib->call_id = 1; } pcmk__xe_set(*op_msg, PCMK__XA_T, PCMK__VALUE_CIB); pcmk__xe_set(*op_msg, PCMK__XA_CIB_OP, op); pcmk__xe_set(*op_msg, PCMK__XA_CIB_HOST, host); pcmk__xe_set(*op_msg, PCMK__XA_CIB_SECTION, section); pcmk__xe_set(*op_msg, PCMK__XA_CIB_USER, user_name); pcmk__xe_set(*op_msg, PCMK__XA_CIB_CLIENTNAME, client_name); pcmk__xe_set_int(*op_msg, PCMK__XA_CIB_CALLID, cib->call_id); pcmk__trace("Sending call options: %.8lx, %d", (long) call_options, call_options); pcmk__xe_set_int(*op_msg, PCMK__XA_CIB_CALLOPT, call_options); if (data != NULL) { xmlNode *wrapper = pcmk__xe_create(*op_msg, PCMK__XE_CIB_CALLDATA); pcmk__xml_copy(wrapper, data); } return pcmk_ok; } /*! * \internal * \brief Check whether a CIB request is supported in a transaction * * \param[in] request CIB request * * \return Standard Pacemaker return code */ static int validate_transaction_request(const xmlNode *request) { const char *op = pcmk__xe_get(request, PCMK__XA_CIB_OP); const char *host = pcmk__xe_get(request, PCMK__XA_CIB_HOST); const cib__operation_t *operation = NULL; int rc = cib__get_operation(op, &operation); if (rc != pcmk_rc_ok) { // cib__get_operation() logs error return rc; } if (!pcmk__is_set(operation->flags, cib__op_attr_transaction)) { pcmk__err("Operation %s is not supported in CIB transactions", op); return EOPNOTSUPP; } if (host != NULL) { pcmk__err("Operation targeting a specific node (%s) is not supported " "in a CIB transaction", host); return EOPNOTSUPP; } return pcmk_rc_ok; } /*! * \internal * \brief Append a CIB request to a CIB transaction * * \param[in,out] cib CIB client whose transaction to extend * \param[in,out] request Request to add to transaction * * \return Legacy Pacemaker return code */ int cib__extend_transaction(cib_t *cib, xmlNode *request) { int rc = pcmk_rc_ok; pcmk__assert((cib != NULL) && (request != NULL)); rc = validate_transaction_request(request); if ((rc == pcmk_rc_ok) && (cib->transaction == NULL)) { rc = pcmk_rc_no_transaction; } if (rc == pcmk_rc_ok) { pcmk__xml_copy(cib->transaction, request); } else { const char *op = pcmk__xe_get(request, PCMK__XA_CIB_OP); const char *client_id = NULL; cib->cmds->client_id(cib, NULL, &client_id); pcmk__err("Failed to add '%s' operation to transaction for client %s: " "%s", op, pcmk__s(client_id, "(unidentified)"), pcmk_rc_str(rc)); pcmk__log_xml_info(request, "failed"); } return pcmk_rc2legacy(rc); } void cib_native_callback(cib_t * cib, xmlNode * msg, int call_id, int rc) { xmlNode *output = NULL; cib_callback_client_t *blob = NULL; if (msg != NULL) { xmlNode *wrapper = NULL; pcmk__xe_get_int(msg, PCMK__XA_CIB_RC, &rc); pcmk__xe_get_int(msg, PCMK__XA_CIB_CALLID, &call_id); wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_CALLDATA, NULL, NULL); output = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); } blob = cib__lookup_id(call_id); if (blob == NULL) { pcmk__trace("No callback found for call %d", call_id); } if (cib == NULL) { pcmk__debug("No cib object supplied"); } if (rc == -pcmk_err_diff_resync) { /* This is an internal value that clients do not and should not care about */ rc = pcmk_ok; } if (blob && blob->callback && (rc == pcmk_ok || blob->only_success == FALSE)) { pcmk__trace("Invoking callback %s for call %d", pcmk__s(blob->id, "without ID"), call_id); blob->callback(msg, call_id, rc, output, blob->user_data); } else if ((cib != NULL) && (rc != pcmk_ok)) { pcmk__warn("CIB command failed: %s", pcmk_strerror(rc)); - crm_log_xml_debug(msg, "Failed CIB Update"); + pcmk__log_xml_debug(msg, "Failed CIB Update"); } /* This may free user_data, so do it after the callback */ if (blob) { remove_cib_op_callback(call_id, FALSE); } pcmk__trace("OP callback activated for %d", call_id); } void cib_native_notify(gpointer data, gpointer user_data) { xmlNode *msg = user_data; cib_notify_client_t *entry = data; const char *event = NULL; if (msg == NULL) { pcmk__warn("Skipping callback - NULL message"); return; } event = pcmk__xe_get(msg, PCMK__XA_SUBT); if (entry == NULL) { pcmk__warn("Skipping callback - NULL callback client"); return; } else if (entry->callback == NULL) { pcmk__warn("Skipping callback - NULL callback"); return; } else if (!pcmk__str_eq(entry->event, event, pcmk__str_casei)) { pcmk__trace("Skipping callback - event mismatch %p/%s vs. %s", entry, entry->event, event); return; } pcmk__trace("Invoking callback for %p/%s event...", entry, event); entry->callback(event, msg); pcmk__trace("Callback invoked..."); } gboolean cib_read_config(GHashTable * options, xmlNode * current_cib) { xmlNode *config = NULL; crm_time_t *now = NULL; if (options == NULL || current_cib == NULL) { return FALSE; } now = crm_time_new(NULL); g_hash_table_remove_all(options); config = pcmk_find_cib_element(current_cib, PCMK_XE_CRM_CONFIG); if (config) { pcmk_rule_input_t rule_input = { .now = now, }; pcmk_unpack_nvpair_blocks(config, PCMK_XE_CLUSTER_PROPERTY_SET, PCMK_VALUE_CIB_BOOTSTRAP_OPTIONS, &rule_input, options, NULL); } pcmk__validate_cluster_options(options); crm_time_free(now); return TRUE; } int cib_internal_op(cib_t * cib, const char *op, const char *host, const char *section, xmlNode * data, xmlNode ** output_data, int call_options, const char *user_name) { int (*delegate)(cib_t *cib, const char *op, const char *host, const char *section, xmlNode *data, xmlNode **output_data, int call_options, const char *user_name) = NULL; if (cib == NULL) { return -EINVAL; } delegate = cib->delegate_fn; if (delegate == NULL) { return -EPROTONOSUPPORT; } if (user_name == NULL) { user_name = getenv("CIB_user"); } return delegate(cib, op, host, section, data, output_data, call_options, user_name); } /*! * \brief Apply a CIB update patch to a given CIB * * \param[in] event CIB update patch * \param[in] input CIB to patch * \param[out] output Resulting CIB after patch * \param[in] level Log the patch at this log level (unless LOG_CRIT) * * \return Legacy Pacemaker return code * \note sbd calls this function */ int cib_apply_patch_event(xmlNode *event, xmlNode *input, xmlNode **output, int level) { int rc = pcmk_err_generic; xmlNode *wrapper = NULL; xmlNode *diff = NULL; pcmk__assert((event != NULL) && (input != NULL) && (output != NULL)); pcmk__xe_get_int(event, PCMK__XA_CIB_RC, &rc); wrapper = pcmk__xe_first_child(event, PCMK__XE_CIB_UPDATE_RESULT, NULL, NULL); diff = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); if (rc < pcmk_ok || diff == NULL) { return rc; } if (level > LOG_CRIT) { pcmk__log_xml_patchset(level, diff); } if (input != NULL) { rc = cib_process_diff(NULL, cib_none, NULL, event, diff, input, output, NULL); if (rc != pcmk_ok) { pcmk__debug("Update didn't apply: %s (%d) %p", pcmk_strerror(rc), rc, *output); if (rc == -pcmk_err_old_data) { pcmk__trace("Masking error, we already have the supplied " "update"); return pcmk_ok; } pcmk__xml_free(*output); *output = NULL; return rc; } } return rc; } #define log_signon_query_err(out, fmt, args...) do { \ if (out != NULL) { \ out->err(out, fmt, ##args); \ } else { \ pcmk__err(fmt, ##args); \ } \ } while (0) int cib__signon_query(pcmk__output_t *out, cib_t **cib, xmlNode **cib_object) { int rc = pcmk_rc_ok; cib_t *cib_conn = NULL; pcmk__assert(cib_object != NULL); if (cib == NULL) { cib_conn = cib_new(); } else { if (*cib == NULL) { *cib = cib_new(); } cib_conn = *cib; } if (cib_conn == NULL) { return ENOMEM; } if (cib_conn->state == cib_disconnected) { rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command); rc = pcmk_legacy2rc(rc); } if (rc != pcmk_rc_ok) { log_signon_query_err(out, "Could not connect to the CIB: %s", pcmk_rc_str(rc)); goto done; } if (out != NULL) { out->transient(out, "Querying CIB..."); } rc = cib_conn->cmds->query(cib_conn, NULL, cib_object, cib_sync_call); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { log_signon_query_err(out, "CIB query failed: %s", pcmk_rc_str(rc)); } done: if (cib == NULL) { cib__clean_up_connection(&cib_conn); } if ((rc == pcmk_rc_ok) && (*cib_object == NULL)) { return pcmk_rc_no_input; } return rc; } int cib__signon_attempts(cib_t *cib, enum cib_conn_type type, int attempts) { int rc = pcmk_rc_ok; pcmk__trace("Attempting connection to CIB manager (up to %d time%s)", attempts, pcmk__plural_s(attempts)); for (int remaining = attempts - 1; remaining >= 0; --remaining) { rc = cib->cmds->signon(cib, crm_system_name, type); if ((rc == pcmk_rc_ok) || (remaining == 0) || ((errno != EAGAIN) && (errno != EALREADY))) { break; } // Retry after soft error (interrupted by signal, etc.) pcmk__sleep_ms((attempts - remaining) * 500); pcmk__debug("Re-attempting connection to CIB manager (%d attempt%s " "remaining)", remaining, pcmk__plural_s(remaining)); } return rc; } int cib__clean_up_connection(cib_t **cib) { int rc; if (*cib == NULL) { return pcmk_rc_ok; } rc = (*cib)->cmds->signoff(*cib); cib_delete(*cib); *cib = NULL; return pcmk_legacy2rc(rc); } diff --git a/lib/common/schemas.c b/lib/common/schemas.c index 983a84e389..0ea1643716 100644 --- a/lib/common/schemas.c +++ b/lib/common/schemas.c @@ -1,1571 +1,1571 @@ /* * 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 // UCHAR_MAX #include #include #include #include // xmlNode #include // xmlChar #include #include #include #include #include #include #include /* PCMK__XML_LOG_BASE */ #include "crmcommon_private.h" #define SCHEMA_ZERO { .v = { 0, 0 } } #define schema_strdup_printf(prefix, version, suffix) \ pcmk__assert_asprintf(prefix "%u.%u" suffix, (version).v[0], (version).v[1]) typedef struct { xmlRelaxNGPtr rng; xmlRelaxNGValidCtxtPtr valid; xmlRelaxNGParserCtxtPtr parser; } relaxng_ctx_cache_t; static GList *known_schemas = NULL; static bool initialized = false; static bool silent_logging = FALSE; static void G_GNUC_PRINTF(2, 3) xml_log(int priority, const char *fmt, ...) { va_list ap; va_start(ap, fmt); if (silent_logging == FALSE) { /* XXX should not this enable dechunking as well? */ PCMK__XML_LOG_BASE(priority, FALSE, 0, NULL, fmt, ap); } va_end(ap); } static int xml_latest_schema_index(void) { /* This function assumes that pcmk__schema_init() has been called * beforehand, so we have at least two schemas (one real schema and the * "none" schema). * * @COMPAT: The "none" schema is deprecated since 2.1.8. * Update this when we drop that schema. */ return g_list_length(known_schemas) - 2; } /*! * \internal * \brief Return the schema entry of the highest-versioned schema * * \return Schema entry of highest-versioned schema */ static GList * get_highest_schema(void) { /* The highest numerically versioned schema is the one before none * * @COMPAT none is deprecated since 2.1.8 */ GList *entry = pcmk__get_schema("none"); pcmk__assert((entry != NULL) && (entry->prev != NULL)); return entry->prev; } /*! * \internal * \brief Return the name of the highest-versioned schema * * \return Name of highest-versioned schema (or NULL on error) */ const char * pcmk__highest_schema_name(void) { GList *entry = get_highest_schema(); return ((pcmk__schema_t *)(entry->data))->name; } /*! * \internal * \brief Find first entry of highest major schema version series * * \return Schema entry of first schema with highest major version */ GList * pcmk__find_x_0_schema(void) { #if defined(PCMK__UNIT_TESTING) /* If we're unit testing, this can't be static because it'll stick * around from one test run to the next. It needs to be cleared out * every time. */ GList *x_0_entry = NULL; #else static GList *x_0_entry = NULL; #endif pcmk__schema_t *highest_schema = NULL; if (x_0_entry != NULL) { return x_0_entry; } x_0_entry = get_highest_schema(); highest_schema = x_0_entry->data; for (GList *iter = x_0_entry->prev; iter != NULL; iter = iter->prev) { pcmk__schema_t *schema = iter->data; /* We've found a schema in an older major version series. Return * the index of the first one in the same major version series as * the highest schema. */ if (schema->version.v[0] < highest_schema->version.v[0]) { x_0_entry = iter->next; break; } /* We're out of list to examine. This probably means there was only * one major version series, so return the first schema entry. */ if (iter->prev == NULL) { x_0_entry = known_schemas->data; break; } } return x_0_entry; } static inline bool version_from_filename(const char *filename, pcmk__schema_version_t *version) { if (pcmk__ends_with(filename, ".rng")) { return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2; } else { return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2; } } static int schema_filter(const struct dirent *a) { int rc = 0; pcmk__schema_version_t version = SCHEMA_ZERO; if (strstr(a->d_name, "pacemaker-") != a->d_name) { // pcmk__trace("%s - wrong prefix", a->d_name); } else if (!pcmk__ends_with_ext(a->d_name, ".rng")) { // pcmk__trace("%s - wrong suffix", a->d_name); } else if (!version_from_filename(a->d_name, &version)) { // pcmk__trace("%s - wrong format", a->d_name); } else { // pcmk__debug("%s - candidate", a->d_name); rc = 1; } return rc; } static int schema_cmp(pcmk__schema_version_t a_version, pcmk__schema_version_t b_version) { for (int i = 0; i < 2; ++i) { if (a_version.v[i] < b_version.v[i]) { return -1; } else if (a_version.v[i] > b_version.v[i]) { return 1; } } return 0; } static int schema_cmp_directory(const struct dirent **a, const struct dirent **b) { pcmk__schema_version_t a_version = SCHEMA_ZERO; pcmk__schema_version_t b_version = SCHEMA_ZERO; if (!version_from_filename(a[0]->d_name, &a_version) || !version_from_filename(b[0]->d_name, &b_version)) { // Shouldn't be possible, but makes static analysis happy return 0; } return schema_cmp(a_version, b_version); } /*! * \internal * \brief Add given schema + auxiliary data to internal bookkeeping. */ static void add_schema(enum pcmk__schema_validator validator, const pcmk__schema_version_t *version, const char *name, GList *transforms) { pcmk__schema_t *schema = NULL; schema = pcmk__assert_alloc(1, sizeof(pcmk__schema_t)); schema->validator = validator; schema->version.v[0] = version->v[0]; schema->version.v[1] = version->v[1]; schema->transforms = transforms; // schema->schema_index is set after all schemas are loaded and sorted if (version->v[0] || version->v[1]) { schema->name = schema_strdup_printf("pacemaker-", *version, ""); } else { schema->name = pcmk__str_copy(name); } known_schemas = g_list_prepend(known_schemas, schema); } static void wrap_libxslt(bool finalize) { static xsltSecurityPrefsPtr secprefs; int ret = 0; /* security framework preferences */ if (!finalize) { pcmk__assert(secprefs == NULL); secprefs = xsltNewSecurityPrefs(); ret = xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_FILE, xsltSecurityForbid) | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_CREATE_DIRECTORY, xsltSecurityForbid) | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_READ_NETWORK, xsltSecurityForbid) | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_NETWORK, xsltSecurityForbid); if (ret != 0) { return; } } else { xsltFreeSecurityPrefs(secprefs); secprefs = NULL; } /* cleanup only */ if (finalize) { xsltCleanupGlobals(); } } /*! * \internal * \brief Check whether a directory entry matches the upgrade XSLT pattern * * \param[in] entry Directory entry whose filename to check * * \return 1 if the entry's filename is of the form * upgrade-X.Y-ORDER.xsl with each number in the range 0 to * 255, or 0 otherwise */ static int transform_filter(const struct dirent *entry) { const char *re = NULL; unsigned int major = 0; unsigned int minor = 0; unsigned int order = 0; /* Each number is an unsigned char, which is 1 to 3 digits long. (Pacemaker * requires an 8-bit char via a configure test.) */ re = "upgrade-[[:digit:]]{1,3}\\.[[:digit:]]{1,3}-[[:digit:]]{1,3}\\.xsl"; if (!pcmk__str_eq(entry->d_name, re, pcmk__str_regex)) { return 0; } /* Performance isn't critical here and this is simpler than range-checking * within the regex */ if (sscanf(entry->d_name, "upgrade-%u.%u-%u.xsl", &major, &minor, &order) != 3) { return 0; } if ((major > UCHAR_MAX) || (minor > UCHAR_MAX) || (order > UCHAR_MAX)) { return 0; } return 1; } /*! * \internal * \brief Compare transform files based on the version strings in their names * * \retval -1 if \p entry1 sorts before \p entry2 * \retval 0 if \p entry1 sorts equal to \p entry2 * \retval 1 if \p entry1 sorts after \p entry2 * * \note The GNU \c versionsort() function would be perfect here, but it's not * portable. */ static int compare_transforms(const struct dirent **entry1, const struct dirent **entry2) { // We already validated the format of each filename in transform_filter() return pcmk__compare_versions((*entry1)->d_name + sizeof("upgrade-") - 1, (*entry2)->d_name + sizeof("upgrade-") - 1); } /*! * \internal * \brief Free a list of XSLT transform struct dirent objects * * \param[in,out] data List to free */ static void free_transform_list(void *data) { g_list_free_full((GList *) data, free); } /*! * \internal * \brief Load names of upgrade XSLT stylesheets from a directory into a table * * Stylesheets must have names of the form "upgrade-X.Y-order.xsl", where: * * X is the schema major version * * Y is the schema minor version * * ORDER is the order in which the stylesheet occurs in the transform pipeline * * \param[in] dir Directory containing XSLT stylesheets * * \return Table with schema version as key and \c GList of associated transform * files (as struct dirent) as value */ static GHashTable * load_transforms_from_dir(const char *dir) { struct dirent **namelist = NULL; GHashTable *transforms = NULL; int num_matches = scandir(dir, &namelist, transform_filter, compare_transforms); if (num_matches < 0) { int rc = errno; pcmk__warn("Could not load transforms from %s: %s", dir, pcmk_rc_str(rc)); goto done; } transforms = pcmk__strkey_table(free, free_transform_list); for (int i = 0; i < num_matches; i++) { pcmk__schema_version_t version = SCHEMA_ZERO; unsigned char order = 0; // Placeholder only if (sscanf(namelist[i]->d_name, "upgrade-%hhu.%hhu-%hhu.xsl", &(version.v[0]), &(version.v[1]), &order) == 3) { char *version_s = pcmk__assert_asprintf("%hhu.%hhu", version.v[0], version.v[1]); GList *list = g_hash_table_lookup(transforms, version_s); if (list == NULL) { /* Prepend is more efficient. However, there won't be many of * these, and we want them to remain sorted by version. It's not * worth reversing all the lists at the end. * * Avoid calling g_hash_table_insert() if the list already * exists. Otherwise free_transform_list() gets called on it. */ list = g_list_append(list, namelist[i]); g_hash_table_insert(transforms, version_s, list); } else { list = g_list_append(list, namelist[i]); free(version_s); } } else { // Sanity only, should never happen thanks to transform_filter() free(namelist[i]); } } done: free(namelist); return transforms; } void pcmk__load_schemas_from_dir(const char *dir) { int lpc, max; struct dirent **namelist = NULL; GHashTable *transforms = NULL; max = scandir(dir, &namelist, schema_filter, schema_cmp_directory); if (max < 0) { int rc = errno; pcmk__warn("Could not load schemas from %s: %s", dir, pcmk_rc_str(rc)); goto done; } // Look for any upgrade transforms in the same directory transforms = load_transforms_from_dir(dir); for (lpc = 0; lpc < max; lpc++) { pcmk__schema_version_t version = SCHEMA_ZERO; if (version_from_filename(namelist[lpc]->d_name, &version)) { char *version_s = pcmk__assert_asprintf("%hhu.%hhu", version.v[0], version.v[1]); char *orig_key = NULL; GList *transform_list = NULL; if (transforms != NULL) { // The schema becomes the owner of transform_list g_hash_table_lookup_extended(transforms, version_s, (gpointer *) &orig_key, (gpointer *) &transform_list); g_hash_table_steal(transforms, version_s); } add_schema(pcmk__schema_validator_rng, &version, NULL, transform_list); free(version_s); free(orig_key); } else { // Shouldn't be possible, but makes static analysis happy pcmk__warn("Skipping schema '%s': could not parse version", namelist[lpc]->d_name); } } for (lpc = 0; lpc < max; lpc++) { free(namelist[lpc]); } done: free(namelist); if (transforms != NULL) { g_hash_table_destroy(transforms); } } static gint schema_sort_GCompareFunc(gconstpointer a, gconstpointer b) { const pcmk__schema_t *schema_a = a; const pcmk__schema_t *schema_b = b; // @COMPAT The "none" schema is deprecated since 2.1.8 if (pcmk__str_eq(schema_a->name, PCMK_VALUE_NONE, pcmk__str_none)) { return 1; } else if (pcmk__str_eq(schema_b->name, PCMK_VALUE_NONE, pcmk__str_none)) { return -1; } else { return schema_cmp(schema_a->version, schema_b->version); } } /*! * \internal * \brief Sort the list of known schemas such that all pacemaker-X.Y are in * version order, then "none" * * This function should be called whenever additional schemas are loaded using * \c pcmk__load_schemas_from_dir(), after the initial sets in * \c pcmk__schema_init(). */ void pcmk__sort_schemas(void) { known_schemas = g_list_sort(known_schemas, schema_sort_GCompareFunc); } /*! * \internal * \brief Load pacemaker schemas into cache * * \note This currently also serves as an entry point for the * generic initialization of the libxslt library. */ void pcmk__schema_init(void) { if (!initialized) { const char *remote_schema_dir = pcmk__remote_schema_dir(); char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng); const pcmk__schema_version_t zero = SCHEMA_ZERO; int schema_index = 0; initialized = true; wrap_libxslt(false); pcmk__load_schemas_from_dir(base); pcmk__load_schemas_from_dir(remote_schema_dir); free(base); // @COMPAT Deprecated since 2.1.8 add_schema(pcmk__schema_validator_none, &zero, PCMK_VALUE_NONE, NULL); /* add_schema() prepends items to the list, so in the simple case, this * just reverses the list. However if there were any remote schemas, * sorting is necessary. */ pcmk__sort_schemas(); // Now set the schema indexes and log the final result for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { pcmk__schema_t *schema = iter->data; pcmk__debug("Loaded schema %d: %s", schema_index, schema->name); schema->schema_index = schema_index++; } } } static bool validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context, const char *relaxng_file, relaxng_ctx_cache_t **cached_ctx) { int rc = 0; bool valid = true; relaxng_ctx_cache_t *ctx = NULL; CRM_CHECK(doc != NULL, return false); CRM_CHECK(relaxng_file != NULL, return false); if (cached_ctx && *cached_ctx) { ctx = *cached_ctx; } else { pcmk__debug("Creating RNG parser context"); ctx = pcmk__assert_alloc(1, sizeof(relaxng_ctx_cache_t)); ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file); CRM_CHECK(ctx->parser != NULL, goto cleanup); if (error_handler) { xmlRelaxNGSetParserErrors(ctx->parser, (xmlRelaxNGValidityErrorFunc) error_handler, (xmlRelaxNGValidityWarningFunc) error_handler, error_handler_context); } else { xmlRelaxNGSetParserErrors(ctx->parser, (xmlRelaxNGValidityErrorFunc) fprintf, (xmlRelaxNGValidityWarningFunc) fprintf, stderr); } ctx->rng = xmlRelaxNGParse(ctx->parser); CRM_CHECK(ctx->rng != NULL, pcmk__err("Could not find/parse %s", relaxng_file); goto cleanup); ctx->valid = xmlRelaxNGNewValidCtxt(ctx->rng); CRM_CHECK(ctx->valid != NULL, goto cleanup); if (error_handler) { xmlRelaxNGSetValidErrors(ctx->valid, (xmlRelaxNGValidityErrorFunc) error_handler, (xmlRelaxNGValidityWarningFunc) error_handler, error_handler_context); } else { xmlRelaxNGSetValidErrors(ctx->valid, (xmlRelaxNGValidityErrorFunc) fprintf, (xmlRelaxNGValidityWarningFunc) fprintf, stderr); } } rc = xmlRelaxNGValidateDoc(ctx->valid, doc); if (rc > 0) { valid = false; } else if (rc < 0) { pcmk__err("Internal libxml error during validation"); } cleanup: if (cached_ctx) { *cached_ctx = ctx; } else { if (ctx->parser != NULL) { xmlRelaxNGFreeParserCtxt(ctx->parser); } if (ctx->valid != NULL) { xmlRelaxNGFreeValidCtxt(ctx->valid); } if (ctx->rng != NULL) { xmlRelaxNGFree(ctx->rng); } free(ctx); } return valid; } static void free_schema(gpointer data) { pcmk__schema_t *schema = data; relaxng_ctx_cache_t *ctx = NULL; switch (schema->validator) { case pcmk__schema_validator_none: // not cached break; case pcmk__schema_validator_rng: // cached ctx = (relaxng_ctx_cache_t *) schema->cache; if (ctx == NULL) { break; } if (ctx->parser != NULL) { xmlRelaxNGFreeParserCtxt(ctx->parser); } if (ctx->valid != NULL) { xmlRelaxNGFreeValidCtxt(ctx->valid); } if (ctx->rng != NULL) { xmlRelaxNGFree(ctx->rng); } free(ctx); schema->cache = NULL; break; } free(schema->name); g_list_free_full(schema->transforms, free); free(schema); } /*! * \internal * \brief Clean up global memory associated with XML schemas */ void pcmk__schema_cleanup(void) { if (known_schemas != NULL) { g_list_free_full(known_schemas, free_schema); known_schemas = NULL; } initialized = false; wrap_libxslt(true); } /*! * \internal * \brief Get schema list entry corresponding to a schema name * * \param[in] name Name of schema to get * * \return Schema list entry corresponding to \p name, or NULL if unknown */ GList * pcmk__get_schema(const char *name) { if (name == NULL) { return NULL; } for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { pcmk__schema_t *schema = iter->data; if (pcmk__str_eq(name, schema->name, pcmk__str_none)) { return iter; } } return NULL; } /*! * \internal * \brief Compare two schema version numbers given the schema names * * \param[in] schema1 Name of first schema to compare * \param[in] schema2 Name of second schema to compare * * \return Standard comparison result (negative integer if \p schema1 has the * lower version number, positive integer if \p schema1 has the higher * version number, of 0 if the version numbers are equal) */ int pcmk__cmp_schemas_by_name(const char *schema1_name, const char *schema2_name) { GList *entry1 = pcmk__get_schema(schema1_name); GList *entry2 = pcmk__get_schema(schema2_name); if (entry1 == NULL) { return (entry2 == NULL)? 0 : -1; } else if (entry2 == NULL) { return 1; } else { pcmk__schema_t *schema1 = entry1->data; pcmk__schema_t *schema2 = entry2->data; return schema1->schema_index - schema2->schema_index; } } static bool validate_with(xmlNode *xml, pcmk__schema_t *schema, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context) { bool valid = false; char *file = NULL; relaxng_ctx_cache_t **cache = NULL; if (schema == NULL) { return false; } if (schema->validator == pcmk__schema_validator_none) { return true; } file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, schema->name); pcmk__trace("Validating with %s (type=%d)",pcmk__s(file, "missing schema"), schema->validator); switch (schema->validator) { case pcmk__schema_validator_rng: cache = (relaxng_ctx_cache_t **) &(schema->cache); valid = validate_with_relaxng(xml->doc, error_handler, error_handler_context, file, cache); break; default: pcmk__err("Unknown validator type: %d", schema->validator); break; } free(file); return valid; } static bool validate_with_silent(xmlNode *xml, pcmk__schema_t *schema) { bool rc, sl_backup = silent_logging; silent_logging = TRUE; rc = validate_with(xml, schema, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR)); silent_logging = sl_backup; return rc; } bool pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context) { GList *entry = NULL; pcmk__schema_t *schema = NULL; CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return false); if (validation == NULL) { validation = pcmk__xe_get(xml_blob, PCMK_XA_VALIDATE_WITH); } pcmk__warn_if_schema_deprecated(validation); entry = pcmk__get_schema(validation); if (entry == NULL) { pcmk__config_err("Cannot validate CIB with %s " PCMK_XA_VALIDATE_WITH " (manually edit to use a known schema)", ((validation == NULL)? "missing" : "unknown")); return false; } schema = entry->data; return validate_with(xml_blob, schema, error_handler, error_handler_context); } /*! * \internal * \brief Validate XML using its configured schema (and send errors to logs) * * \param[in] xml XML to validate * * \return true if XML validates, otherwise false */ bool pcmk__configured_schema_validates(xmlNode *xml) { return pcmk__validate_xml(xml, NULL, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR)); } /* With this arrangement, an attempt to identify the message severity as explicitly signalled directly from XSLT is performed in rather a smart way (no reliance on formatting string + arguments being always specified as ["%s", purposeful_string], as it can also be ["%s: %s", some_prefix, purposeful_string] etc. so every argument pertaining %s specifier is investigated), and if such a mark found, the respective level is determined and, when the messages are to go to the native logs, the mark itself gets dropped (by the means of string shift). NOTE: whether the native logging is the right sink is decided per the ctx parameter -- NULL denotes this case, otherwise it carries a pointer to the numeric expression of the desired target logging level (messages with higher level will be suppressed) NOTE: on some architectures, this string shift may not have any effect, but that's an acceptable tradeoff The logging level for not explicitly designated messages (suspicious, likely internal errors or some runaways) is LOG_WARNING. */ static void G_GNUC_PRINTF(2, 3) cib_upgrade_err(void *ctx, const char *fmt, ...) { va_list ap, aq; char *arg_cur; bool found = false; const char *fmt_iter = fmt; uint8_t msg_log_level = LOG_WARNING; /* default for runaway messages */ const unsigned * log_level = (const unsigned *) ctx; enum { escan_seennothing, escan_seenpercent, } scan_state = escan_seennothing; va_start(ap, fmt); va_copy(aq, ap); while (!found && *fmt_iter != '\0') { /* while casing schema borrowed from libqb:qb_vsnprintf_serialize */ switch (*fmt_iter++) { case '%': if (scan_state == escan_seennothing) { scan_state = escan_seenpercent; } else if (scan_state == escan_seenpercent) { scan_state = escan_seennothing; } break; case 's': if (scan_state == escan_seenpercent) { size_t prefix_len = 0; scan_state = escan_seennothing; arg_cur = va_arg(aq, char *); if (pcmk__starts_with(arg_cur, "WARNING: ")) { prefix_len = sizeof("WARNING: ") - 1; msg_log_level = LOG_WARNING; } else if (pcmk__starts_with(arg_cur, "INFO: ")) { prefix_len = sizeof("INFO: ") - 1; msg_log_level = LOG_INFO; } else if (pcmk__starts_with(arg_cur, "DEBUG: ")) { prefix_len = sizeof("DEBUG: ") - 1; msg_log_level = LOG_DEBUG; } else { break; } found = true; if (ctx == NULL) { memmove(arg_cur, arg_cur + prefix_len, strlen(arg_cur + prefix_len) + 1); } } break; case '#': case '-': case ' ': case '+': case '\'': case 'I': case '.': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '*': break; case 'l': case 'z': case 't': case 'j': case 'd': case 'i': case 'o': case 'u': case 'x': case 'X': case 'e': case 'E': case 'f': case 'F': case 'g': case 'G': case 'a': case 'A': case 'c': case 'p': if (scan_state == escan_seenpercent) { (void) va_arg(aq, void *); /* skip forward */ scan_state = escan_seennothing; } break; default: scan_state = escan_seennothing; break; } } if (log_level != NULL) { /* intention of the following offset is: cibadmin -V -> start showing INFO labelled messages */ if (*log_level + 4 >= msg_log_level) { vfprintf(stderr, fmt, ap); } } else { PCMK__XML_LOG_BASE(msg_log_level, TRUE, 0, "CIB upgrade: ", fmt, ap); } va_end(aq); va_end(ap); } /*! * \internal * \brief Apply a single XSL transformation to given XML * * \param[in] xml XML to transform * \param[in] transform XSL name * \param[in] to_logs If false, certain validation errors will be sent to * stderr rather than logged * * \return Transformed XML on success, otherwise NULL */ static xmlNode * apply_transformation(const xmlNode *xml, const char *transform, gboolean to_logs) { char *xform = NULL; xmlNode *out = NULL; xmlDocPtr res = NULL; xsltStylesheet *xslt = NULL; xform = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, transform); /* for capturing, e.g., what's emitted via */ if (to_logs) { xsltSetGenericErrorFunc(NULL, cib_upgrade_err); } else { xsltSetGenericErrorFunc(&crm_log_level, cib_upgrade_err); } xslt = xsltParseStylesheetFile((const xmlChar *) xform); CRM_CHECK(xslt != NULL, goto cleanup); /* Caller allocates private data for final result document. Intermediate * result documents are temporary and don't need private data. */ res = xsltApplyStylesheet(xslt, xml->doc, NULL); CRM_CHECK(res != NULL, goto cleanup); xsltSetGenericErrorFunc(NULL, NULL); /* restore default one */ out = xmlDocGetRootElement(res); cleanup: if (xslt) { xsltFreeStylesheet(xslt); } free(xform); return out; } /*! * \internal * \brief Perform all transformations needed to upgrade XML to next schema * * \param[in] input_xml XML to transform * \param[in] schema_index Index of schema that successfully validates * \p original_xml * \param[in] to_logs If false, certain validation errors will be sent to * stderr rather than logged * * \return XML result of schema transforms if successful, otherwise NULL */ static xmlNode * apply_upgrade(const xmlNode *input_xml, int schema_index, gboolean to_logs) { pcmk__schema_t *schema = g_list_nth_data(known_schemas, schema_index); pcmk__schema_t *upgraded_schema = g_list_nth_data(known_schemas, schema_index + 1); xmlNode *old_xml = NULL; xmlNode *new_xml = NULL; xmlRelaxNGValidityErrorFunc error_handler = NULL; pcmk__assert((schema != NULL) && (upgraded_schema != NULL)); if (to_logs) { error_handler = (xmlRelaxNGValidityErrorFunc) xml_log; } for (GList *iter = schema->transforms; iter != NULL; iter = iter->next) { const struct dirent *entry = iter->data; const char *transform = entry->d_name; pcmk__debug("Upgrading schema from %s to %s: applying XSL transform %s", schema->name, upgraded_schema->name, transform); new_xml = apply_transformation(input_xml, transform, to_logs); pcmk__xml_free(old_xml); if (new_xml == NULL) { pcmk__err("XSL transform %s failed, aborting upgrade", transform); return NULL; } input_xml = new_xml; old_xml = new_xml; } // Final result document from upgrade pipeline needs private data pcmk__xml_new_private_data((xmlNode *) new_xml->doc); // Ensure result validates with its new schema if (!validate_with(new_xml, upgraded_schema, error_handler, GUINT_TO_POINTER(LOG_ERR))) { pcmk__err("Schema upgrade from %s to %s failed: XSL transform pipeline " "produced an invalid configuration", schema->name, upgraded_schema->name); - crm_log_xml_debug(new_xml, "bad-transform-result"); + pcmk__log_xml_debug(new_xml, "bad-transform-result"); pcmk__xml_free(new_xml); return NULL; } pcmk__info("Schema upgrade from %s to %s succeeded", schema->name, upgraded_schema->name); return new_xml; } /*! * \internal * \brief Get the schema list entry corresponding to XML configuration * * \param[in] xml CIB XML to check * * \return List entry of schema configured in \p xml */ static GList * get_configured_schema(const xmlNode *xml) { const char *schema_name = pcmk__xe_get(xml, PCMK_XA_VALIDATE_WITH); pcmk__warn_if_schema_deprecated(schema_name); return pcmk__get_schema(schema_name); } /*! * \brief Update CIB XML to latest schema that validates it * * \param[in,out] xml XML to update (may be freed and replaced * after being transformed) * \param[in] max_schema_name If not NULL, do not update \p xml to any * schema later than this one * \param[in] transform If false, do not update \p xml to any schema * that requires an XSL transform * \param[in] to_logs If false, certain validation errors will be * sent to stderr rather than logged * * \return Standard Pacemaker return code */ int pcmk__update_schema(xmlNode **xml, const char *max_schema_name, bool transform, bool to_logs) { int max_stable_schemas = xml_latest_schema_index(); int max_schema_index = 0; int rc = pcmk_rc_ok; GList *entry = NULL; pcmk__schema_t *best_schema = NULL; pcmk__schema_t *original_schema = NULL; xmlRelaxNGValidityErrorFunc error_handler = to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL; CRM_CHECK((xml != NULL) && (*xml != NULL) && ((*xml)->doc != NULL), return EINVAL); if (max_schema_name != NULL) { GList *max_entry = pcmk__get_schema(max_schema_name); if (max_entry != NULL) { pcmk__schema_t *max_schema = max_entry->data; max_schema_index = max_schema->schema_index; } } if ((max_schema_index < 1) || (max_schema_index > max_stable_schemas)) { max_schema_index = max_stable_schemas; } entry = get_configured_schema(*xml); if (entry == NULL) { return pcmk_rc_cib_corrupt; } original_schema = entry->data; if (original_schema->schema_index >= max_schema_index) { return pcmk_rc_ok; } for (; entry != NULL; entry = entry->next) { pcmk__schema_t *current_schema = entry->data; xmlNode *upgrade = NULL; if (current_schema->schema_index > max_schema_index) { break; } if (!validate_with(*xml, current_schema, error_handler, GUINT_TO_POINTER(LOG_ERR))) { pcmk__debug("Schema %s does not validate", current_schema->name); if (best_schema != NULL) { /* we've satisfied the validation, no need to check further */ break; } rc = pcmk_rc_schema_validation; continue; // Try again with the next higher schema } pcmk__debug("Schema %s validates", current_schema->name); rc = pcmk_rc_ok; best_schema = current_schema; if (current_schema->schema_index == max_schema_index) { break; // No further transformations possible } if (!transform || (current_schema->transforms == NULL) || validate_with_silent(*xml, entry->next->data)) { /* The next schema either doesn't require a transform or validates * successfully even without the transform. Skip the transform and * try the next schema with the same XML. */ continue; } upgrade = apply_upgrade(*xml, current_schema->schema_index, to_logs); if (upgrade == NULL) { /* The transform failed, so this schema can't be used. Later * schemas are unlikely to validate, but try anyway until we * run out of options. */ rc = pcmk_rc_transform_failed; } else { best_schema = current_schema; pcmk__xml_free(*xml); *xml = upgrade; } } if ((best_schema != NULL) && (best_schema->schema_index > original_schema->schema_index)) { pcmk__info("%s the configuration schema to %s", (transform? "Transformed" : "Upgraded"), best_schema->name); pcmk__xe_set(*xml, PCMK_XA_VALIDATE_WITH, best_schema->name); } return rc; } int pcmk_update_configured_schema(xmlNode **xml) { return pcmk__update_configured_schema(xml, true); } /*! * \brief Update XML from its configured schema to the latest major series * * \param[in,out] xml XML to update * \param[in] to_logs If false, certain validation errors will be * sent to stderr rather than logged * * \return Standard Pacemaker return code */ int pcmk__update_configured_schema(xmlNode **xml, bool to_logs) { pcmk__schema_t *x_0_schema = pcmk__find_x_0_schema()->data; pcmk__schema_t *original_schema = NULL; GList *entry = NULL; if (xml == NULL) { return EINVAL; } entry = get_configured_schema(*xml); if (entry == NULL) { return pcmk_rc_cib_corrupt; } original_schema = entry->data; if (original_schema->schema_index < x_0_schema->schema_index) { // Current configuration schema is not acceptable, try to update xmlNode *converted = NULL; const char *new_schema_name = NULL; pcmk__schema_t *schema = NULL; entry = NULL; converted = pcmk__xml_copy(NULL, *xml); if (pcmk__update_schema(&converted, NULL, true, to_logs) == pcmk_rc_ok) { new_schema_name = pcmk__xe_get(converted, PCMK_XA_VALIDATE_WITH); entry = pcmk__get_schema(new_schema_name); } schema = (entry == NULL)? NULL : entry->data; if ((schema == NULL) || (schema->schema_index < x_0_schema->schema_index)) { // Updated configuration schema is still not acceptable if ((schema == NULL) || (schema->schema_index < original_schema->schema_index)) { // We couldn't validate any schema at all if (to_logs) { pcmk__config_err("Cannot upgrade configuration (claiming " "%s schema) to at least %s because it " "does not validate with any schema from " "%s to the latest", original_schema->name, x_0_schema->name, original_schema->name); } else { fprintf(stderr, "Cannot upgrade configuration (claiming " "%s schema) to at least %s because it " "does not validate with any schema from " "%s to the latest\n", original_schema->name, x_0_schema->name, original_schema->name); } } else { // We updated configuration successfully, but still too low if (to_logs) { pcmk__config_err("Cannot upgrade configuration (claiming " "%s schema) to at least %s because it " "would not upgrade past %s", original_schema->name, x_0_schema->name, pcmk__s(new_schema_name, "unspecified version")); } else { fprintf(stderr, "Cannot upgrade configuration (claiming " "%s schema) to at least %s because it " "would not upgrade past %s\n", original_schema->name, x_0_schema->name, pcmk__s(new_schema_name, "unspecified version")); } } pcmk__xml_free(converted); converted = NULL; return pcmk_rc_transform_failed; } else { // Updated configuration schema is acceptable pcmk__xml_free(*xml); *xml = converted; if (schema->schema_index < xml_latest_schema_index()) { if (to_logs) { pcmk__config_warn("Configuration with %s schema was " "internally upgraded to acceptable (but " "not most recent) %s", original_schema->name, schema->name); } } else if (to_logs) { pcmk__info("Configuration with %s schema was internally " "upgraded to latest version %s", original_schema->name, schema->name); } } } else if (!to_logs) { pcmk__schema_t *none_schema = NULL; entry = pcmk__get_schema(PCMK_VALUE_NONE); pcmk__assert((entry != NULL) && (entry->data != NULL)); none_schema = entry->data; if (original_schema->schema_index >= none_schema->schema_index) { // @COMPAT the none schema is deprecated since 2.1.8 fprintf(stderr, "Schema validation of configuration is " "disabled (support for " PCMK_XA_VALIDATE_WITH " set to \"" PCMK_VALUE_NONE "\" is deprecated" " and will be removed in a future release)\n"); } } return pcmk_rc_ok; } /*! * \internal * \brief Return a list of all schema files and any associated XSLT files * later than the given one * \brief Return a list of all schema versions later than the given one * * \param[in] schema The schema to compare against (for example, * "pacemaker-3.1.rng" or "pacemaker-3.1") * * \note The caller is responsible for freeing both the returned list and * the elements of the list */ GList * pcmk__schema_files_later_than(const char *name) { GList *lst = NULL; pcmk__schema_version_t ver; if (!version_from_filename(name, &ver)) { return lst; } for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index()); iter != NULL; iter = iter->prev) { pcmk__schema_t *schema = iter->data; if (schema_cmp(ver, schema->version) != -1) { continue; } for (GList *iter2 = g_list_last(schema->transforms); iter2 != NULL; iter2 = iter2->prev) { const struct dirent *entry = iter2->data; lst = g_list_prepend(lst, pcmk__str_copy(entry->d_name)); } lst = g_list_prepend(lst, pcmk__assert_asprintf("%s.rng", schema->name)); } return lst; } static void append_href(xmlNode *xml, void *user_data) { GList **list = user_data; char *href = pcmk__xe_get_copy(xml, "href"); if (href == NULL) { return; } *list = g_list_prepend(*list, href); } static void external_refs_in_schema(GList **list, const char *contents) { /* local-name()= is needed to ignore the xmlns= setting at the top of * the XML file. Otherwise, the xpath query will always return nothing. */ const char *search = "//*[local-name()='externalRef'] | //*[local-name()='include']"; xmlNode *xml = pcmk__xml_parse(contents); pcmk__xpath_foreach_result(xml->doc, search, append_href, list); pcmk__xml_free(xml); } static int read_file_contents(const char *file, char **contents) { int rc = pcmk_rc_ok; char *path = NULL; if (pcmk__ends_with(file, ".rng")) { path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, file); } else { path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, file); } rc = pcmk__file_contents(path, contents); free(path); return rc; } static void add_schema_file_to_xml(xmlNode *parent, const char *file, GList **already_included) { char *contents = NULL; char *path = NULL; xmlNode *file_node = NULL; GList *includes = NULL; int rc = pcmk_rc_ok; /* If we already included this file, don't do so again. */ if (g_list_find_custom(*already_included, file, (GCompareFunc) strcmp) != NULL) { return; } /* Ensure whatever file we were given has a suffix we know about. If not, * just assume it's an RNG file. */ if (!pcmk__ends_with(file, ".rng") && !pcmk__ends_with(file, ".xsl")) { path = pcmk__assert_asprintf("%s.rng", file); } else { path = pcmk__str_copy(file); } rc = read_file_contents(path, &contents); if (rc != pcmk_rc_ok || contents == NULL) { pcmk__warn("Could not read schema file %s: %s", file, pcmk_rc_str(rc)); free(path); return; } /* Create a new node with the contents of the file * as a CDATA block underneath it. */ file_node = pcmk__xe_create(parent, PCMK__XE_FILE); pcmk__xe_set(file_node, PCMK_XA_PATH, path); *already_included = g_list_prepend(*already_included, path); xmlAddChild(file_node, xmlNewCDataBlock(parent->doc, (const xmlChar *) contents, strlen(contents))); /* Scan the file for any or nodes and build up * a list of the files they reference. */ external_refs_in_schema(&includes, contents); /* For each referenced file, recurse to add it (and potentially anything it * references, ...) to the XML. */ for (GList *iter = includes; iter != NULL; iter = iter->next) { add_schema_file_to_xml(parent, iter->data, already_included); } free(contents); g_list_free_full(includes, free); } /*! * \internal * \brief Add an XML schema file and all the files it references as children * of a given XML node * * \param[in,out] parent The parent XML node * \param[in] name The schema version to compare against * (for example, "pacemaker-3.1" or "pacemaker-3.1.rng") * \param[in,out] already_included A list of names that have already been added * to the parent node. * * \note The caller is responsible for freeing both the returned list and * the elements of the list */ void pcmk__build_schema_xml_node(xmlNode *parent, const char *name, GList **already_included) { xmlNode *schema_node = pcmk__xe_create(parent, PCMK__XA_SCHEMA); pcmk__xe_set(schema_node, PCMK_XA_VERSION, name); add_schema_file_to_xml(schema_node, name, already_included); if (schema_node->children == NULL) { // Not needed if empty. May happen if name was invalid, for example. pcmk__xml_free(schema_node); } } /*! * \internal * \brief Return the directory containing any extra schema files that a * Pacemaker Remote node fetched from the cluster */ const char * pcmk__remote_schema_dir(void) { const char *dir = pcmk__env_option(PCMK__ENV_REMOTE_SCHEMA_DIRECTORY); if (pcmk__str_empty(dir)) { return PCMK__REMOTE_SCHEMA_DIR; } return dir; } /*! * \internal * \brief Warn if a given validation schema is deprecated * * \param[in] Schema name to check */ void pcmk__warn_if_schema_deprecated(const char *schema) { /* @COMPAT Disabling validation is deprecated since 2.1.8, but * resource-agents' ocf-shellfuncs (at least as of 4.15.1) uses it */ if (pcmk__str_eq(schema, PCMK_VALUE_NONE, pcmk__str_none)) { pcmk__config_warn("Support for " PCMK_XA_VALIDATE_WITH "='%s' is " "deprecated and will be removed in a future release " "without the possibility of upgrades (manually edit " "to use a supported schema)", schema); } } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include gboolean cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) { int rc = pcmk__update_configured_schema(xml, to_logs); if (best_version != NULL) { const char *name = pcmk__xe_get(*xml, PCMK_XA_VALIDATE_WITH); if (name == NULL) { *best_version = -1; } else { GList *entry = pcmk__get_schema(name); pcmk__schema_t *schema = (entry == NULL)? NULL : entry->data; *best_version = (schema == NULL)? -1 : schema->schema_index; } } return (rc == pcmk_rc_ok)? TRUE: FALSE; } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/xml_io.c b/lib/common/xml_io.c index abc5483518..4eefbdf8ae 100644 --- a/lib/common/xml_io.c +++ b/lib/common/xml_io.c @@ -1,658 +1,658 @@ /* * 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 // xmlOutputBuffer* #include // xmlChar #include #include #include #include "crmcommon_private.h" /*! * \internal * \brief Decompress a bzip2-compressed file into a string buffer * * \param[in] filename Name of file to decompress * * \return Newly allocated string with the decompressed contents of \p filename, * or \c NULL on error. * * \note The caller is responsible for freeing the return value using \c free(). */ static char * decompress_file(const char *filename) { char *buffer = NULL; int rc = pcmk_rc_ok; size_t length = 0; BZFILE *bz_file = NULL; FILE *input = fopen(filename, "r"); if (input == NULL) { crm_perror(LOG_ERR, "Could not open %s for reading", filename); return NULL; } bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { pcmk__err("Could not prepare to read compressed %s: %s " QB_XS " rc=%d", filename, pcmk_rc_str(rc), rc); goto done; } do { int read_len = 0; buffer = pcmk__realloc(buffer, length + PCMK__BUFFER_SIZE + 1); read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE); if ((rc == BZ_OK) || (rc == BZ_STREAM_END)) { pcmk__trace("Read %ld bytes from file: %d", (long) read_len, rc); length += read_len; } } while (rc == BZ_OK); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { rc = pcmk__bzlib2rc(rc); pcmk__err("Could not read compressed %s: %s " QB_XS " rc=%d", filename, pcmk_rc_str(rc), rc); free(buffer); buffer = NULL; } else { buffer[length] = '\0'; } done: BZ2_bzReadClose(&rc, bz_file); fclose(input); return buffer; } /*! * \internal * \brief Parse XML from a file * * \param[in] filename Name of file containing XML (\c NULL or \c "-" for * \c stdin); if \p filename ends in \c ".bz2", the file * will be decompressed using \c bzip2 * * \return XML tree parsed from the given file on success, otherwise \c NULL */ xmlNode * pcmk__xml_read(const char *filename) { bool use_stdin = pcmk__str_eq(filename, "-", pcmk__str_null_matches); xmlNode *xml = NULL; xmlDoc *output = NULL; xmlParserCtxt *ctxt = NULL; const xmlError *last_error = NULL; // Create a parser context ctxt = xmlNewParserCtxt(); CRM_CHECK(ctxt != NULL, return NULL); xmlCtxtResetLastError(ctxt); xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); if (use_stdin) { output = xmlCtxtReadFd(ctxt, STDIN_FILENO, NULL, NULL, XML_PARSE_NOBLANKS); } else if (pcmk__ends_with_ext(filename, ".bz2")) { char *input = decompress_file(filename); if (input != NULL) { output = xmlCtxtReadDoc(ctxt, (const xmlChar *) input, NULL, NULL, XML_PARSE_NOBLANKS); free(input); } } else { output = xmlCtxtReadFile(ctxt, filename, NULL, XML_PARSE_NOBLANKS); } if (output != NULL) { pcmk__xml_new_private_data((xmlNode *) output); xml = xmlDocGetRootElement(output); if (xml != NULL) { /* @TODO Should we really be stripping out text? This seems like an * overly broad way to get rid of whitespace, if that's the goal. * Text nodes may be invalid in most or all Pacemaker inputs, but * stripping them in a generic "parse XML from file" function may * not be the best way to ignore them. */ pcmk__strip_xml_text(xml); } } last_error = xmlCtxtGetLastError(ctxt); if ((last_error != NULL) && (xml != NULL)) { - crm_log_xml_debug(xml, "partial"); + pcmk__log_xml_debug(xml, "partial"); pcmk__xml_free(xml); xml = NULL; } xmlFreeParserCtxt(ctxt); return xml; } /*! * \internal * \brief Parse XML from a string * * \param[in] input String to parse * * \return XML tree parsed from the given string on success, otherwise \c NULL */ xmlNode * pcmk__xml_parse(const char *input) { xmlNode *xml = NULL; xmlDoc *output = NULL; xmlParserCtxt *ctxt = NULL; const xmlError *last_error = NULL; if (input == NULL) { return NULL; } ctxt = xmlNewParserCtxt(); if (ctxt == NULL) { return NULL; } xmlCtxtResetLastError(ctxt); xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); output = xmlCtxtReadDoc(ctxt, (const xmlChar *) input, NULL, NULL, XML_PARSE_NOBLANKS); if (output != NULL) { pcmk__xml_new_private_data((xmlNode *) output); xml = xmlDocGetRootElement(output); } last_error = xmlCtxtGetLastError(ctxt); if ((last_error != NULL) && (xml != NULL)) { - crm_log_xml_debug(xml, "partial"); + pcmk__log_xml_debug(xml, "partial"); pcmk__xml_free(xml); xml = NULL; } xmlFreeParserCtxt(ctxt); return xml; } /*! * \internal * \brief Append a string representation of an XML element to a buffer * * \param[in] data XML whose representation to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer, int depth) { const bool pretty = pcmk__is_set(options, pcmk__xml_fmt_pretty); const bool filtered = pcmk__is_set(options, pcmk__xml_fmt_filtered); const int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "<", data->name, NULL); for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; attr = attr->next) { if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) { pcmk__dump_xml_attr(attr, buffer); } } if (data->children == NULL) { g_string_append(buffer, "/>"); } else { g_string_append_c(buffer, '>'); } if (pretty) { g_string_append_c(buffer, '\n'); } if (data->children) { for (const xmlNode *child = data->children; child != NULL; child = child->next) { pcmk__xml_string(child, options, buffer, depth + 1); } for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "name, ">", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } } /*! * \internal * \brief Append XML text content to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of enum pcmk__xml_fmt_options * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer, int depth) { const bool pretty = pcmk__is_set(options, pcmk__xml_fmt_pretty); const int spaces = pretty? (2 * depth) : 0; const char *content = (const char *) data->content; gchar *content_esc = NULL; if (pcmk__xml_needs_escape(content, pcmk__xml_escape_text)) { content_esc = pcmk__xml_escape(content, pcmk__xml_escape_text); content = content_esc; } for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } g_string_append(buffer, content); if (pretty) { g_string_append_c(buffer, '\n'); } g_free(content_esc); } /*! * \internal * \brief Append XML CDATA content to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer, int depth) { const bool pretty = pcmk__is_set(options, pcmk__xml_fmt_pretty); const int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "content, "]]>", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } /*! * \internal * \brief Append an XML comment to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer, int depth) { const bool pretty = pcmk__is_set(options, pcmk__xml_fmt_pretty); const int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } /*! * \internal * \brief Create a string representation of an XML object * * libxml2's \c xmlNodeDumpOutput() doesn't allow filtering, doesn't escape * special characters thoroughly, and doesn't allow a const argument. * * \param[in] data XML to convert * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to store the text (must not be \p NULL) * \param[in] depth Current indentation level * * \todo Create a wrapper that doesn't require \p depth. Only used with * recursive calls currently. */ void pcmk__xml_string(const xmlNode *data, uint32_t options, GString *buffer, int depth) { if (data == NULL) { pcmk__trace("Nothing to dump"); return; } pcmk__assert(buffer != NULL); CRM_CHECK(depth >= 0, depth = 0); switch(data->type) { case XML_ELEMENT_NODE: /* Handle below */ dump_xml_element(data, options, buffer, depth); break; case XML_TEXT_NODE: if (pcmk__is_set(options, pcmk__xml_fmt_text)) { dump_xml_text(data, options, buffer, depth); } break; case XML_COMMENT_NODE: dump_xml_comment(data, options, buffer, depth); break; case XML_CDATA_SECTION_NODE: dump_xml_cdata(data, options, buffer, depth); break; default: pcmk__warn("Cannot convert XML %s node to text " QB_XS " type=%d", pcmk__xml_element_type_text(data->type), data->type); break; } } /*! * \internal * \brief Write a string to a file stream, compressed using \c bzip2 * * \param[in] text String to write * \param[in] filename Name of file being written (for logging only) * \param[in,out] stream Open file stream to write to * \param[out] bytes_out Number of bytes written (valid only on success) * * \return Standard Pacemaker return code */ static int write_compressed_stream(char *text, const char *filename, FILE *stream, unsigned int *bytes_out) { unsigned int bytes_in = 0; int rc = pcmk_rc_ok; // (5, 0, 0): (intermediate block size, silent, default workFactor) BZFILE *bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 0); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { pcmk__warn("Not compressing %s: could not prepare file stream: %s " QB_XS " rc=%d", filename, pcmk_rc_str(rc), rc); goto done; } BZ2_bzWrite(&rc, bz_file, text, strlen(text)); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { pcmk__warn("Not compressing %s: could not compress data: %s " QB_XS " rc=%d errno=%d", filename, pcmk_rc_str(rc), rc, errno); goto done; } BZ2_bzWriteClose(&rc, bz_file, 0, &bytes_in, bytes_out); bz_file = NULL; rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { pcmk__warn("Not compressing %s: could not write compressed data: %s " QB_XS " rc=%d errno=%d", filename, pcmk_rc_str(rc), rc, errno); goto done; } pcmk__trace("Compressed XML for %s from %u bytes to %u", filename, bytes_in, *bytes_out); done: if (bz_file != NULL) { BZ2_bzWriteClose(&rc, bz_file, 0, NULL, NULL); } return rc; } /*! * \internal * \brief Write XML to a file stream * * \param[in] xml XML to write * \param[in] filename Name of file being written (for logging only) * \param[in,out] stream Open file stream corresponding to filename (closed * when this function returns) * \param[in] compress Whether to compress XML before writing * * \return Standard Pacemaker return code */ static int write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream, bool compress) { GString *buffer = g_string_sized_new(1024); unsigned int bytes_out = 0; int rc = pcmk_rc_ok; pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0); CRM_CHECK(!pcmk__str_empty(buffer->str), pcmk__log_xml_info(xml, "dump-failed"); rc = pcmk_rc_error; goto done); crm_log_xml_trace(xml, "writing"); if (compress && (write_compressed_stream(buffer->str, filename, stream, &bytes_out) == pcmk_rc_ok)) { goto done; } rc = fprintf(stream, "%s", buffer->str); if (rc < 0) { rc = EIO; crm_perror(LOG_ERR, "writing %s", filename); goto done; } bytes_out = (unsigned int) rc; rc = pcmk_rc_ok; done: if (fflush(stream) != 0) { rc = errno; crm_perror(LOG_ERR, "flushing %s", filename); } // Don't report error if the file does not support synchronization if ((fsync(fileno(stream)) < 0) && (errno != EROFS) && (errno != EINVAL)) { rc = errno; crm_perror(LOG_ERR, "synchronizing %s", filename); } fclose(stream); pcmk__trace("Saved %u bytes to %s as XML", bytes_out, filename); g_string_free(buffer, TRUE); return rc; } /*! * \internal * \brief Write XML to a file descriptor * * \param[in] xml XML to write * \param[in] filename Name of file being written (for logging only) * \param[in] fd Open file descriptor corresponding to \p filename * * \return Standard Pacemaker return code */ int pcmk__xml_write_fd(const xmlNode *xml, const char *filename, int fd) { FILE *stream = NULL; CRM_CHECK((xml != NULL) && (fd > 0), return EINVAL); stream = fdopen(fd, "w"); if (stream == NULL) { return errno; } return write_xml_stream(xml, pcmk__s(filename, "unnamed file"), stream, false); } /*! * \internal * \brief Write XML to a file * * \param[in] xml XML to write * \param[in] filename Name of file to write * \param[in] compress If \c true, compress XML before writing * * \return Standard Pacemaker return code */ int pcmk__xml_write_file(const xmlNode *xml, const char *filename, bool compress) { FILE *stream = NULL; CRM_CHECK((xml != NULL) && (filename != NULL), return EINVAL); stream = fopen(filename, "w"); if (stream == NULL) { return errno; } return write_xml_stream(xml, filename, stream, compress); } /*! * \internal * \brief Serialize XML (using libxml) into provided descriptor * * \param[in] fd File descriptor to (piece-wise) write to * \param[in] cur XML subtree to proceed * * \return a standard Pacemaker return code */ int pcmk__xml2fd(int fd, xmlNode *cur) { bool success; xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL); pcmk__mem_assert(fd_out); xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL); success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1; success = xmlOutputBufferClose(fd_out) != -1 && success; if (!success) { return EIO; } fsync(fd); return pcmk_rc_ok; } /*! * \internal * \brief Write XML to a file in a temporary directory * * \param[in] xml XML to write * \param[in] desc Description of \p xml * \param[in] filename Base name of file to write (\c NULL to create a name * based on a generated UUID) */ void pcmk__xml_write_temp_file(const xmlNode *xml, const char *desc, const char *filename) { char *path = NULL; char *uuid = NULL; CRM_CHECK((xml != NULL) && (desc != NULL), return); if (filename == NULL) { uuid = pcmk__generate_uuid(); filename = uuid; } path = pcmk__assert_asprintf("%s/%s", pcmk__get_tmpdir(), filename); pcmk__info("Saving %s to %s", desc, path); pcmk__xml_write_file(xml, filename, false); free(path); free(uuid); } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include void save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename) { char *f = NULL; if (filename == NULL) { char *uuid = pcmk__generate_uuid(); f = pcmk__assert_asprintf("%s/%s", pcmk__get_tmpdir(), uuid); filename = f; free(uuid); } pcmk__info("Saving %s to %s", desc, filename); pcmk__xml_write_file(xml, filename, false); free(f); } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/fencing/st_client.c b/lib/fencing/st_client.c index 40ee2a6e40..9c3c4c1a29 100644 --- a/lib/fencing/st_client.c +++ b/lib/fencing/st_client.c @@ -1,2756 +1,2756 @@ /* * 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 // xmlNode #include // xmlXPathObject, etc. #include #include #include #include // hash2field() #include #include #include "fencing_private.h" CRM_TRACE_INIT_DATA(stonith); // Used as stonith_t:st_private typedef struct stonith_private_s { char *token; crm_ipc_t *ipc; mainloop_io_t *source; GHashTable *stonith_op_callback_table; GList *notify_list; int notify_refcnt; bool notify_deletes; void (*op_callback) (stonith_t * st, stonith_callback_data_t * data); } stonith_private_t; // Used as stonith_event_t:opaque struct event_private { pcmk__action_result_t result; }; typedef struct stonith_notify_client_s { const char *event; const char *obj_id; /* implement one day */ const char *obj_type; /* implement one day */ void (*notify) (stonith_t * st, stonith_event_t * e); bool delete; } stonith_notify_client_t; typedef struct stonith_callback_client_s { void (*callback) (stonith_t * st, stonith_callback_data_t * data); const char *id; void *user_data; gboolean only_success; gboolean allow_timeout_updates; struct timer_rec_s *timer; } stonith_callback_client_t; struct notify_blob_s { stonith_t *stonith; xmlNode *xml; }; struct timer_rec_s { int call_id; int timeout; guint ref; stonith_t *stonith; }; typedef int (*stonith_op_t) (const char *, int, const char *, xmlNode *, xmlNode *, xmlNode *, xmlNode **, xmlNode **); bool stonith_dispatch(stonith_t * st); xmlNode *stonith_create_op(int call_id, const char *token, const char *op, xmlNode * data, int call_options); static int stonith_send_command(stonith_t *stonith, const char *op, xmlNode *data, xmlNode **output_data, int call_options, int timeout); static void stonith_connection_destroy(gpointer user_data); static void stonith_send_notification(gpointer data, gpointer user_data); static int stonith_api_del_notification(stonith_t *stonith, const char *event); /*! * \brief Get agent namespace by name * * \param[in] namespace_s Name of namespace as string * * \return Namespace as enum value */ enum stonith_namespace stonith_text2namespace(const char *namespace_s) { if (pcmk__str_eq(namespace_s, "any", pcmk__str_null_matches)) { return st_namespace_any; } else if (!strcmp(namespace_s, "redhat") || !strcmp(namespace_s, "stonith-ng")) { return st_namespace_rhcs; } else if (!strcmp(namespace_s, "internal")) { return st_namespace_internal; } else if (!strcmp(namespace_s, "heartbeat")) { return st_namespace_lha; } return st_namespace_invalid; } /*! * \brief Get agent namespace name * * \param[in] namespace Namespace as enum value * * \return Namespace name as string */ const char * stonith_namespace2text(enum stonith_namespace st_namespace) { switch (st_namespace) { case st_namespace_any: return "any"; case st_namespace_rhcs: return "stonith-ng"; case st_namespace_internal: return "internal"; case st_namespace_lha: return "heartbeat"; default: break; } return "unsupported"; } /*! * \brief Determine namespace of a fence agent * * \param[in] agent Fence agent type * \param[in] namespace_s Name of agent namespace as string, if known * * \return Namespace of specified agent, as enum value */ enum stonith_namespace stonith_get_namespace(const char *agent, const char *namespace_s) { if (pcmk__str_eq(namespace_s, "internal", pcmk__str_none)) { return st_namespace_internal; } if (stonith__agent_is_rhcs(agent)) { return st_namespace_rhcs; } #if HAVE_STONITH_STONITH_H if (stonith__agent_is_lha(agent)) { return st_namespace_lha; } #endif return st_namespace_invalid; } gboolean stonith__watchdog_fencing_enabled_for_node_api(stonith_t *st, const char *node) { gboolean rv = FALSE; stonith_t *stonith_api = st?st:stonith_api_new(); char *list = NULL; if(stonith_api) { if (stonith_api->state == stonith_disconnected) { int rc = stonith_api->cmds->connect(stonith_api, "stonith-api", NULL); if (rc != pcmk_ok) { pcmk__err("Failed connecting to Stonith-API for " "watchdog-fencing-query"); } } if (stonith_api->state != stonith_disconnected) { /* caveat!!! * this might fail when when stonithd is just updating the device-list * probably something we should fix as well for other api-calls */ int rc = stonith_api->cmds->list(stonith_api, st_opt_sync_call, STONITH_WATCHDOG_ID, &list, 0); if ((rc != pcmk_ok) || (list == NULL)) { /* due to the race described above it can happen that * we drop in here - so as not to make remote nodes * panic on that answer */ if (rc == -ENODEV) { pcmk__notice("Cluster does not have watchdog fencing " "device"); } else { pcmk__warn("Could not check for watchdog fencing device: %s", pcmk_strerror(rc)); } } else if (list[0] == '\0') { rv = TRUE; } else { GList *targets = stonith__parse_targets(list); rv = pcmk__str_in_list(node, targets, pcmk__str_casei); g_list_free_full(targets, free); } free(list); if (!st) { /* if we're provided the api we still might have done the * connection - but let's assume the caller won't bother */ stonith_api->cmds->disconnect(stonith_api); } } if (!st) { stonith_api_delete(stonith_api); } } else { pcmk__err("Stonith-API for watchdog-fencing-query couldn't be created"); } pcmk__trace("Pacemaker assumes node %s %sto do watchdog-fencing", node, (rv? "" : "not ")); return rv; } gboolean stonith__watchdog_fencing_enabled_for_node(const char *node) { return stonith__watchdog_fencing_enabled_for_node_api(NULL, node); } /* when cycling through the list we don't want to delete items so just mark them and when we know nobody is using the list loop over it to remove the marked items */ static void foreach_notify_entry (stonith_private_t *private, GFunc func, gpointer user_data) { private->notify_refcnt++; g_list_foreach(private->notify_list, func, user_data); private->notify_refcnt--; if ((private->notify_refcnt == 0) && private->notify_deletes) { GList *list_item = private->notify_list; private->notify_deletes = FALSE; while (list_item != NULL) { stonith_notify_client_t *list_client = list_item->data; GList *next = g_list_next(list_item); if (list_client->delete) { free(list_client); private->notify_list = g_list_delete_link(private->notify_list, list_item); } list_item = next; } } } static void stonith_connection_destroy(gpointer user_data) { stonith_t *stonith = user_data; stonith_private_t *native = NULL; struct notify_blob_s blob; pcmk__trace("Sending destroyed notification"); blob.stonith = stonith; blob.xml = pcmk__xe_create(NULL, PCMK__XE_NOTIFY); native = stonith->st_private; native->ipc = NULL; native->source = NULL; free(native->token); native->token = NULL; stonith->state = stonith_disconnected; pcmk__xe_set(blob.xml, PCMK__XA_T, PCMK__VALUE_ST_NOTIFY); pcmk__xe_set(blob.xml, PCMK__XA_SUBT, PCMK__VALUE_ST_NOTIFY_DISCONNECT); foreach_notify_entry(native, stonith_send_notification, &blob); pcmk__xml_free(blob.xml); } xmlNode * create_device_registration_xml(const char *id, enum stonith_namespace standard, const char *agent, const stonith_key_value_t *params, const char *rsc_provides) { xmlNode *data = pcmk__xe_create(NULL, PCMK__XE_ST_DEVICE_ID); xmlNode *args = pcmk__xe_create(data, PCMK__XE_ATTRIBUTES); #if HAVE_STONITH_STONITH_H if (standard == st_namespace_any) { standard = stonith_get_namespace(agent, NULL); } if (standard == st_namespace_lha) { hash2field((gpointer) "plugin", (gpointer) agent, args); agent = "fence_legacy"; } #endif pcmk__xe_set(data, PCMK_XA_ID, id); pcmk__xe_set(data, PCMK__XA_ST_ORIGIN, __func__); pcmk__xe_set(data, PCMK_XA_AGENT, agent); if ((standard != st_namespace_any) && (standard != st_namespace_invalid)) { pcmk__xe_set(data, PCMK__XA_NAMESPACE, stonith_namespace2text(standard)); } if (rsc_provides) { pcmk__xe_set(data, PCMK__XA_RSC_PROVIDES, rsc_provides); } for (; params; params = params->next) { hash2field((gpointer) params->key, (gpointer) params->value, args); } return data; } static int stonith_api_register_device(stonith_t *st, int call_options, const char *id, const char *namespace_s, const char *agent, const stonith_key_value_t *params) { int rc = 0; xmlNode *data = NULL; data = create_device_registration_xml(id, stonith_text2namespace(namespace_s), agent, params, NULL); rc = stonith_send_command(st, STONITH_OP_DEVICE_ADD, data, NULL, call_options, 0); pcmk__xml_free(data); return rc; } static int stonith_api_remove_device(stonith_t * st, int call_options, const char *name) { int rc = 0; xmlNode *data = NULL; data = pcmk__xe_create(NULL, PCMK__XE_ST_DEVICE_ID); pcmk__xe_set(data, PCMK__XA_ST_ORIGIN, __func__); pcmk__xe_set(data, PCMK_XA_ID, name); rc = stonith_send_command(st, STONITH_OP_DEVICE_DEL, data, NULL, call_options, 0); pcmk__xml_free(data); return rc; } static int stonith_api_remove_level_full(stonith_t *st, int options, const char *node, const char *pattern, const char *attr, const char *value, int level) { int rc = 0; xmlNode *data = NULL; CRM_CHECK(node || pattern || (attr && value), return -EINVAL); data = pcmk__xe_create(NULL, PCMK_XE_FENCING_LEVEL); pcmk__xe_set(data, PCMK__XA_ST_ORIGIN, __func__); if (node) { pcmk__xe_set(data, PCMK_XA_TARGET, node); } else if (pattern) { pcmk__xe_set(data, PCMK_XA_TARGET_PATTERN, pattern); } else { pcmk__xe_set(data, PCMK_XA_TARGET_ATTRIBUTE, attr); pcmk__xe_set(data, PCMK_XA_TARGET_VALUE, value); } pcmk__xe_set_int(data, PCMK_XA_INDEX, level); rc = stonith_send_command(st, STONITH_OP_LEVEL_DEL, data, NULL, options, 0); pcmk__xml_free(data); return rc; } static int stonith_api_remove_level(stonith_t * st, int options, const char *node, int level) { return stonith_api_remove_level_full(st, options, node, NULL, NULL, NULL, level); } /*! * \internal * \brief Create XML for fence topology level registration request * * \param[in] node If not NULL, target level by this node name * \param[in] pattern If not NULL, target by node name using this regex * \param[in] attr If not NULL, target by this node attribute * \param[in] value If not NULL, target by this node attribute value * \param[in] level Index number of level to register * \param[in] device_list List of devices in level * * \return Newly allocated XML tree on success, NULL otherwise * * \note The caller should set only one of node, pattern or attr/value. */ xmlNode * create_level_registration_xml(const char *node, const char *pattern, const char *attr, const char *value, int level, const stonith_key_value_t *device_list) { GString *list = NULL; xmlNode *data; CRM_CHECK(node || pattern || (attr && value), return NULL); data = pcmk__xe_create(NULL, PCMK_XE_FENCING_LEVEL); pcmk__xe_set(data, PCMK__XA_ST_ORIGIN, __func__); pcmk__xe_set_int(data, PCMK_XA_ID, level); pcmk__xe_set_int(data, PCMK_XA_INDEX, level); if (node) { pcmk__xe_set(data, PCMK_XA_TARGET, node); } else if (pattern) { pcmk__xe_set(data, PCMK_XA_TARGET_PATTERN, pattern); } else { pcmk__xe_set(data, PCMK_XA_TARGET_ATTRIBUTE, attr); pcmk__xe_set(data, PCMK_XA_TARGET_VALUE, value); } for (; device_list; device_list = device_list->next) { pcmk__add_separated_word(&list, 1024, device_list->value, ","); } if (list != NULL) { pcmk__xe_set(data, PCMK_XA_DEVICES, (const char *) list->str); g_string_free(list, TRUE); } return data; } static int stonith_api_register_level_full(stonith_t *st, int options, const char *node, const char *pattern, const char *attr, const char *value, int level, const stonith_key_value_t *device_list) { int rc = 0; xmlNode *data = create_level_registration_xml(node, pattern, attr, value, level, device_list); CRM_CHECK(data != NULL, return -EINVAL); rc = stonith_send_command(st, STONITH_OP_LEVEL_ADD, data, NULL, options, 0); pcmk__xml_free(data); return rc; } static int stonith_api_register_level(stonith_t * st, int options, const char *node, int level, const stonith_key_value_t * device_list) { return stonith_api_register_level_full(st, options, node, NULL, NULL, NULL, level, device_list); } static int stonith_api_device_list(stonith_t *stonith, int call_options, const char *namespace_s, stonith_key_value_t **devices, int timeout) { int count = 0; enum stonith_namespace ns = stonith_text2namespace(namespace_s); if (devices == NULL) { pcmk__err("Parameter error: stonith_api_device_list"); return -EFAULT; } #if HAVE_STONITH_STONITH_H // Include Linux-HA agents if requested if ((ns == st_namespace_any) || (ns == st_namespace_lha)) { count += stonith__list_lha_agents(devices); } #endif // Include Red Hat agents if requested if ((ns == st_namespace_any) || (ns == st_namespace_rhcs)) { count += stonith__list_rhcs_agents(devices); } return count; } // See stonith_api_operations_t:metadata() documentation static int stonith_api_device_metadata(stonith_t *stonith, int call_options, const char *agent, const char *namespace_s, char **output, int timeout_sec) { /* By executing meta-data directly, we can get it from stonith_admin when * the cluster is not running, which is important for higher-level tools. */ enum stonith_namespace ns = stonith_get_namespace(agent, namespace_s); if (timeout_sec <= 0) { timeout_sec = PCMK_DEFAULT_ACTION_TIMEOUT_MS; } pcmk__trace("Looking up metadata for %s agent %s", stonith_namespace2text(ns), agent); switch (ns) { case st_namespace_rhcs: return stonith__rhcs_metadata(agent, timeout_sec, output); #if HAVE_STONITH_STONITH_H case st_namespace_lha: return stonith__lha_metadata(agent, timeout_sec, output); #endif default: pcmk__err("Can't get fence agent '%s' meta-data: No such agent", agent); break; } return -ENODEV; } static int stonith_api_query(stonith_t * stonith, int call_options, const char *target, stonith_key_value_t ** devices, int timeout) { int rc = 0, lpc = 0, max = 0; xmlNode *data = NULL; xmlNode *output = NULL; xmlXPathObject *xpathObj = NULL; CRM_CHECK(devices != NULL, return -EINVAL); data = pcmk__xe_create(NULL, PCMK__XE_ST_DEVICE_ID); pcmk__xe_set(data, PCMK__XA_ST_ORIGIN, __func__); pcmk__xe_set(data, PCMK__XA_ST_TARGET, target); pcmk__xe_set(data, PCMK__XA_ST_DEVICE_ACTION, PCMK_ACTION_OFF); rc = stonith_send_command(stonith, STONITH_OP_QUERY, data, &output, call_options, timeout); if (rc < 0) { return rc; } xpathObj = pcmk__xpath_search(output->doc, "//*[@" PCMK_XA_AGENT "]"); if (xpathObj) { max = pcmk__xpath_num_results(xpathObj); for (lpc = 0; lpc < max; lpc++) { xmlNode *match = pcmk__xpath_result(xpathObj, lpc); CRM_LOG_ASSERT(match != NULL); if(match != NULL) { xmlChar *match_path = xmlGetNodePath(match); pcmk__info("//*[@" PCMK_XA_AGENT "][%d] = %s", lpc, match_path); free(match_path); *devices = stonith_key_value_add(*devices, NULL, pcmk__xe_get(match, PCMK_XA_ID)); } } xmlXPathFreeObject(xpathObj); } pcmk__xml_free(output); pcmk__xml_free(data); return max; } /*! * \internal * \brief Make a STONITH_OP_EXEC request * * \param[in,out] stonith Fencer connection * \param[in] call_options Bitmask of \c stonith_call_options * \param[in] id Fence device ID that request is for * \param[in] action Agent action to request (list, status, monitor) * \param[in] target Name of target node for requested action * \param[in] timeout_sec Error if not completed within this many seconds * \param[out] output Where to set agent output */ static int stonith_api_call(stonith_t *stonith, int call_options, const char *id, const char *action, const char *target, int timeout_sec, xmlNode **output) { int rc = 0; xmlNode *data = NULL; data = pcmk__xe_create(NULL, PCMK__XE_ST_DEVICE_ID); pcmk__xe_set(data, PCMK__XA_ST_ORIGIN, __func__); pcmk__xe_set(data, PCMK__XA_ST_DEVICE_ID, id); pcmk__xe_set(data, PCMK__XA_ST_DEVICE_ACTION, action); pcmk__xe_set(data, PCMK__XA_ST_TARGET, target); rc = stonith_send_command(stonith, STONITH_OP_EXEC, data, output, call_options, timeout_sec); pcmk__xml_free(data); return rc; } static int stonith_api_list(stonith_t * stonith, int call_options, const char *id, char **list_info, int timeout) { int rc; xmlNode *output = NULL; rc = stonith_api_call(stonith, call_options, id, PCMK_ACTION_LIST, NULL, timeout, &output); if (output && list_info) { const char *list_str; list_str = pcmk__xe_get(output, PCMK__XA_ST_OUTPUT); if (list_str) { *list_info = strdup(list_str); } } if (output) { pcmk__xml_free(output); } return rc; } static int stonith_api_monitor(stonith_t * stonith, int call_options, const char *id, int timeout) { return stonith_api_call(stonith, call_options, id, PCMK_ACTION_MONITOR, NULL, timeout, NULL); } static int stonith_api_status(stonith_t * stonith, int call_options, const char *id, const char *port, int timeout) { return stonith_api_call(stonith, call_options, id, PCMK_ACTION_STATUS, port, timeout, NULL); } static int stonith_api_fence_with_delay(stonith_t * stonith, int call_options, const char *node, const char *action, int timeout, int tolerance, int delay) { int rc = 0; xmlNode *data = NULL; data = pcmk__xe_create(NULL, __func__); pcmk__xe_set(data, PCMK__XA_ST_TARGET, node); pcmk__xe_set(data, PCMK__XA_ST_DEVICE_ACTION, action); pcmk__xe_set_int(data, PCMK__XA_ST_TIMEOUT, timeout); pcmk__xe_set_int(data, PCMK__XA_ST_TOLERANCE, tolerance); pcmk__xe_set_int(data, PCMK__XA_ST_DELAY, delay); rc = stonith_send_command(stonith, STONITH_OP_FENCE, data, NULL, call_options, timeout); pcmk__xml_free(data); return rc; } static int stonith_api_fence(stonith_t * stonith, int call_options, const char *node, const char *action, int timeout, int tolerance) { return stonith_api_fence_with_delay(stonith, call_options, node, action, timeout, tolerance, 0); } static int stonith_api_confirm(stonith_t * stonith, int call_options, const char *target) { stonith__set_call_options(call_options, target, st_opt_manual_ack); return stonith_api_fence(stonith, call_options, target, PCMK_ACTION_OFF, 0, 0); } static int stonith_api_history(stonith_t * stonith, int call_options, const char *node, stonith_history_t ** history, int timeout) { int rc = 0; xmlNode *data = NULL; xmlNode *output = NULL; stonith_history_t *last = NULL; *history = NULL; if (node) { data = pcmk__xe_create(NULL, __func__); pcmk__xe_set(data, PCMK__XA_ST_TARGET, node); } stonith__set_call_options(call_options, node, st_opt_sync_call); rc = stonith_send_command(stonith, STONITH_OP_FENCE_HISTORY, data, &output, call_options, timeout); pcmk__xml_free(data); if (rc == 0) { xmlNode *op = NULL; xmlNode *reply = pcmk__xpath_find_one(output->doc, "//" PCMK__XE_ST_HISTORY, PCMK__LOG_NEVER); for (op = pcmk__xe_first_child(reply, NULL, NULL, NULL); op != NULL; op = pcmk__xe_next(op, NULL)) { stonith_history_t *kvp = pcmk__assert_alloc(1, sizeof(stonith_history_t)); long long completed_nsec = 0LL; kvp->target = pcmk__xe_get_copy(op, PCMK__XA_ST_TARGET); kvp->action = pcmk__xe_get_copy(op, PCMK__XA_ST_DEVICE_ACTION); kvp->origin = pcmk__xe_get_copy(op, PCMK__XA_ST_ORIGIN); kvp->delegate = pcmk__xe_get_copy(op, PCMK__XA_ST_DELEGATE); kvp->client = pcmk__xe_get_copy(op, PCMK__XA_ST_CLIENTNAME); pcmk__xe_get_time(op, PCMK__XA_ST_DATE, &kvp->completed); pcmk__xe_get_ll(op, PCMK__XA_ST_DATE_NSEC, &completed_nsec); if ((completed_nsec >= LONG_MIN) && (completed_nsec <= LONG_MAX)) { kvp->completed_nsec = (long) completed_nsec; } pcmk__xe_get_int(op, PCMK__XA_ST_STATE, &kvp->state); kvp->exit_reason = pcmk__xe_get_copy(op, PCMK_XA_EXIT_REASON); if (last) { last->next = kvp; } else { *history = kvp; } last = kvp; } } pcmk__xml_free(output); return rc; } void stonith_history_free(stonith_history_t *history) { stonith_history_t *hp, *hp_old; for (hp = history; hp; hp_old = hp, hp = hp->next, free(hp_old)) { free(hp->target); free(hp->action); free(hp->origin); free(hp->delegate); free(hp->client); free(hp->exit_reason); } } static gint stonithlib_GCompareFunc(gconstpointer a, gconstpointer b) { int rc = 0; const stonith_notify_client_t *a_client = a; const stonith_notify_client_t *b_client = b; if (a_client->delete || b_client->delete) { /* make entries marked for deletion not findable */ return -1; } CRM_CHECK(a_client->event != NULL && b_client->event != NULL, return 0); rc = strcmp(a_client->event, b_client->event); if (rc == 0) { if (a_client->notify == NULL || b_client->notify == NULL) { return 0; } else if (a_client->notify == b_client->notify) { return 0; } else if (((long)a_client->notify) < ((long)b_client->notify)) { pcmk__err("callbacks for %s are not equal: %p vs. %p", a_client->event, a_client->notify, b_client->notify); return -1; } pcmk__err("callbacks for %s are not equal: %p vs. %p", a_client->event, a_client->notify, b_client->notify); return 1; } return rc; } xmlNode * stonith_create_op(int call_id, const char *token, const char *op, xmlNode * data, int call_options) { xmlNode *op_msg = NULL; CRM_CHECK(token != NULL, return NULL); op_msg = pcmk__xe_create(NULL, PCMK__XE_STONITH_COMMAND); pcmk__xe_set(op_msg, PCMK__XA_T, PCMK__VALUE_STONITH_NG); pcmk__xe_set(op_msg, PCMK__XA_ST_OP, op); pcmk__xe_set_int(op_msg, PCMK__XA_ST_CALLID, call_id); pcmk__trace("Sending call options: %.8lx, %d", (long) call_options, call_options); pcmk__xe_set_int(op_msg, PCMK__XA_ST_CALLOPT, call_options); if (data != NULL) { xmlNode *wrapper = pcmk__xe_create(op_msg, PCMK__XE_ST_CALLDATA); pcmk__xml_copy(wrapper, data); } return op_msg; } static void stonith_destroy_op_callback(gpointer data) { stonith_callback_client_t *blob = data; if (blob->timer && blob->timer->ref > 0) { g_source_remove(blob->timer->ref); } free(blob->timer); free(blob); } static int stonith_api_signoff(stonith_t * stonith) { stonith_private_t *native = stonith->st_private; pcmk__debug("Disconnecting from the fencer"); if (native->source != NULL) { /* Attached to mainloop */ mainloop_del_ipc_client(native->source); native->source = NULL; native->ipc = NULL; } else if (native->ipc) { /* Not attached to mainloop */ crm_ipc_t *ipc = native->ipc; native->ipc = NULL; crm_ipc_close(ipc); crm_ipc_destroy(ipc); } free(native->token); native->token = NULL; stonith->state = stonith_disconnected; return pcmk_ok; } static int stonith_api_del_callback(stonith_t * stonith, int call_id, bool all_callbacks) { stonith_private_t *private = stonith->st_private; if (all_callbacks) { private->op_callback = NULL; g_hash_table_destroy(private->stonith_op_callback_table); private->stonith_op_callback_table = pcmk__intkey_table(stonith_destroy_op_callback); } else if (call_id == 0) { private->op_callback = NULL; } else { pcmk__intkey_table_remove(private->stonith_op_callback_table, call_id); } return pcmk_ok; } /*! * \internal * \brief Invoke a (single) specified fence action callback * * \param[in,out] st Fencer API connection * \param[in] call_id If positive, call ID of completed fence action, * otherwise legacy return code for early failure * \param[in,out] result Full result for action * \param[in,out] userdata User data to pass to callback * \param[in] callback Fence action callback to invoke */ static void invoke_fence_action_callback(stonith_t *st, int call_id, pcmk__action_result_t *result, void *userdata, void (*callback) (stonith_t *st, stonith_callback_data_t *data)) { stonith_callback_data_t data = { 0, }; data.call_id = call_id; data.rc = pcmk_rc2legacy(stonith__result2rc(result)); data.userdata = userdata; data.opaque = (void *) result; callback(st, &data); } /*! * \internal * \brief Invoke any callbacks registered for a specified fence action result * * Given a fence action result from the fencer, invoke any callback registered * for that action, as well as any global callback registered. * * \param[in,out] stonith Fencer API connection * \param[in] msg If non-NULL, fencer reply * \param[in] call_id If \p msg is NULL, call ID of action that timed out */ static void invoke_registered_callbacks(stonith_t *stonith, const xmlNode *msg, int call_id) { stonith_private_t *private = NULL; stonith_callback_client_t *cb_info = NULL; pcmk__action_result_t result = PCMK__UNKNOWN_RESULT; CRM_CHECK(stonith != NULL, return); CRM_CHECK(stonith->st_private != NULL, return); private = stonith->st_private; if (msg == NULL) { // Fencer didn't reply in time pcmk__set_result(&result, CRM_EX_ERROR, PCMK_EXEC_TIMEOUT, "Fencer accepted request but did not reply in time"); CRM_LOG_ASSERT(call_id > 0); } else { // We have the fencer reply if ((pcmk__xe_get_int(msg, PCMK__XA_ST_CALLID, &call_id) != pcmk_rc_ok) || (call_id <= 0)) { pcmk__log_xml_warn(msg, "Bad fencer reply"); } stonith__xe_get_result(msg, &result); } if (call_id > 0) { cb_info = pcmk__intkey_table_lookup(private->stonith_op_callback_table, call_id); } if ((cb_info != NULL) && (cb_info->callback != NULL) && (pcmk__result_ok(&result) || !(cb_info->only_success))) { pcmk__trace("Invoking callback %s for call %d", pcmk__s(cb_info->id, "without ID"), call_id); invoke_fence_action_callback(stonith, call_id, &result, cb_info->user_data, cb_info->callback); } else if ((private->op_callback == NULL) && !pcmk__result_ok(&result)) { pcmk__warn("Fencing action without registered callback failed: %d " "(%s%s%s)", result.exit_status, pcmk_exec_status_str(result.execution_status), ((result.exit_reason != NULL)? ": " : ""), pcmk__s(result.exit_reason, "")); - crm_log_xml_debug(msg, "Failed fence update"); + pcmk__log_xml_debug(msg, "Failed fence update"); } if (private->op_callback != NULL) { pcmk__trace("Invoking global callback for call %d", call_id); invoke_fence_action_callback(stonith, call_id, &result, NULL, private->op_callback); } if (cb_info != NULL) { stonith_api_del_callback(stonith, call_id, FALSE); } pcmk__reset_result(&result); } static gboolean stonith_async_timeout_handler(gpointer data) { struct timer_rec_s *timer = data; pcmk__err("Async call %d timed out after %dms", timer->call_id, timer->timeout); invoke_registered_callbacks(timer->stonith, NULL, timer->call_id); /* Always return TRUE, never remove the handler * We do that in stonith_del_callback() */ return TRUE; } static void set_callback_timeout(stonith_callback_client_t * callback, stonith_t * stonith, int call_id, int timeout) { struct timer_rec_s *async_timer = callback->timer; if (timeout <= 0) { return; } if (!async_timer) { async_timer = pcmk__assert_alloc(1, sizeof(struct timer_rec_s)); callback->timer = async_timer; } async_timer->stonith = stonith; async_timer->call_id = call_id; /* Allow a fair bit of grace to allow the server to tell us of a timeout * This is only a fallback */ async_timer->timeout = (timeout + 60) * 1000; if (async_timer->ref) { g_source_remove(async_timer->ref); } async_timer->ref = pcmk__create_timer(async_timer->timeout, stonith_async_timeout_handler, async_timer); } static void update_callback_timeout(int call_id, int timeout, stonith_t * st) { stonith_callback_client_t *callback = NULL; stonith_private_t *private = st->st_private; callback = pcmk__intkey_table_lookup(private->stonith_op_callback_table, call_id); if (!callback || !callback->allow_timeout_updates) { return; } set_callback_timeout(callback, st, call_id, timeout); } static int stonith_dispatch_internal(const char *buffer, ssize_t length, gpointer userdata) { const char *type = NULL; struct notify_blob_s blob; stonith_t *st = userdata; stonith_private_t *private = NULL; pcmk__assert(st != NULL); private = st->st_private; blob.stonith = st; blob.xml = pcmk__xml_parse(buffer); if (blob.xml == NULL) { pcmk__warn("Received malformed message from fencer: %s", buffer); return 0; } /* do callbacks */ type = pcmk__xe_get(blob.xml, PCMK__XA_T); pcmk__trace("Activating %s callbacks...", type); if (pcmk__str_eq(type, PCMK__VALUE_STONITH_NG, pcmk__str_none)) { invoke_registered_callbacks(st, blob.xml, 0); } else if (pcmk__str_eq(type, PCMK__VALUE_ST_NOTIFY, pcmk__str_none)) { foreach_notify_entry(private, stonith_send_notification, &blob); } else if (pcmk__str_eq(type, PCMK__VALUE_ST_ASYNC_TIMEOUT_VALUE, pcmk__str_none)) { int call_id = 0; int timeout = 0; pcmk__xe_get_int(blob.xml, PCMK__XA_ST_TIMEOUT, &timeout); pcmk__xe_get_int(blob.xml, PCMK__XA_ST_CALLID, &call_id); update_callback_timeout(call_id, timeout, st); } else { pcmk__err("Unknown message type: %s", type); pcmk__log_xml_warn(blob.xml, "BadReply"); } pcmk__xml_free(blob.xml); return 1; } static int stonith_api_signon(stonith_t * stonith, const char *name, int *stonith_fd) { int rc = pcmk_ok; stonith_private_t *native = NULL; const char *display_name = name? name : "client"; struct ipc_client_callbacks st_callbacks = { .dispatch = stonith_dispatch_internal, .destroy = stonith_connection_destroy }; CRM_CHECK(stonith != NULL, return -EINVAL); native = stonith->st_private; pcmk__assert(native != NULL); pcmk__debug("Attempting fencer connection by %s with%s mainloop", display_name, ((stonith_fd != 0)? "out" : "")); stonith->state = stonith_connected_command; if (stonith_fd) { /* No mainloop */ native->ipc = crm_ipc_new("stonith-ng", 0); if (native->ipc != NULL) { rc = pcmk__connect_generic_ipc(native->ipc); if (rc == pcmk_rc_ok) { rc = pcmk__ipc_fd(native->ipc, stonith_fd); if (rc != pcmk_rc_ok) { pcmk__debug("Couldn't get file descriptor for IPC: %s", pcmk_rc_str(rc)); } } if (rc != pcmk_rc_ok) { crm_ipc_close(native->ipc); crm_ipc_destroy(native->ipc); native->ipc = NULL; } } } else { /* With mainloop */ native->source = mainloop_add_ipc_client("stonith-ng", G_PRIORITY_MEDIUM, 0, stonith, &st_callbacks); native->ipc = mainloop_get_ipc_client(native->source); } if (native->ipc == NULL) { rc = -ENOTCONN; } else { xmlNode *reply = NULL; xmlNode *hello = pcmk__xe_create(NULL, PCMK__XE_STONITH_COMMAND); pcmk__xe_set(hello, PCMK__XA_T, PCMK__VALUE_STONITH_NG); pcmk__xe_set(hello, PCMK__XA_ST_OP, CRM_OP_REGISTER); pcmk__xe_set(hello, PCMK__XA_ST_CLIENTNAME, name); rc = crm_ipc_send(native->ipc, hello, crm_ipc_client_response, -1, &reply); if (rc < 0) { pcmk__debug("Couldn't register with the fencer: %s " QB_XS " rc=%d", pcmk_strerror(rc), rc); rc = -ECOMM; } else if (reply == NULL) { pcmk__debug("Couldn't register with the fencer: no reply"); rc = -EPROTO; } else { const char *msg_type = pcmk__xe_get(reply, PCMK__XA_ST_OP); native->token = pcmk__xe_get_copy(reply, PCMK__XA_ST_CLIENTID); if (!pcmk__str_eq(msg_type, CRM_OP_REGISTER, pcmk__str_none)) { pcmk__debug("Couldn't register with the fencer: invalid reply " "type '%s'", pcmk__s(msg_type, "(missing)")); - crm_log_xml_debug(reply, "Invalid fencer reply"); + pcmk__log_xml_debug(reply, "Invalid fencer reply"); rc = -EPROTO; } else if (native->token == NULL) { pcmk__debug("Couldn't register with the fencer: no token in " "reply"); - crm_log_xml_debug(reply, "Invalid fencer reply"); + pcmk__log_xml_debug(reply, "Invalid fencer reply"); rc = -EPROTO; } else { pcmk__debug("Connection to fencer by %s succeeded " "(registration token: %s)", display_name, native->token); rc = pcmk_ok; } } pcmk__xml_free(reply); pcmk__xml_free(hello); } if (rc != pcmk_ok) { pcmk__debug("Connection attempt to fencer by %s failed: %s " QB_XS " rc=%d", display_name, pcmk_strerror(rc), rc); stonith->cmds->disconnect(stonith); } return rc; } static int stonith_set_notification(stonith_t * stonith, const char *callback, int enabled) { int rc = pcmk_ok; xmlNode *notify_msg = pcmk__xe_create(NULL, __func__); stonith_private_t *native = stonith->st_private; if (stonith->state != stonith_disconnected) { pcmk__xe_set(notify_msg, PCMK__XA_ST_OP, STONITH_OP_NOTIFY); if (enabled) { pcmk__xe_set(notify_msg, PCMK__XA_ST_NOTIFY_ACTIVATE, callback); } else { pcmk__xe_set(notify_msg, PCMK__XA_ST_NOTIFY_DEACTIVATE, callback); } rc = crm_ipc_send(native->ipc, notify_msg, crm_ipc_client_response, -1, NULL); if (rc < 0) { crm_perror(LOG_DEBUG, "Couldn't register for fencing notifications: %d", rc); rc = -ECOMM; } else { rc = pcmk_ok; } } pcmk__xml_free(notify_msg); return rc; } static int stonith_api_add_notification(stonith_t * stonith, const char *event, void (*callback) (stonith_t * stonith, stonith_event_t * e)) { GList *list_item = NULL; stonith_notify_client_t *new_client = NULL; stonith_private_t *private = NULL; private = stonith->st_private; pcmk__trace("Adding callback for %s events (%u)", event, g_list_length(private->notify_list)); new_client = pcmk__assert_alloc(1, sizeof(stonith_notify_client_t)); new_client->event = event; new_client->notify = callback; list_item = g_list_find_custom(private->notify_list, new_client, stonithlib_GCompareFunc); if (list_item != NULL) { pcmk__warn("Callback already present"); free(new_client); return -ENOTUNIQ; } else { private->notify_list = g_list_append(private->notify_list, new_client); stonith_set_notification(stonith, event, 1); pcmk__trace("Callback added (%u)", g_list_length(private->notify_list)); } return pcmk_ok; } static void del_notify_entry(gpointer data, gpointer user_data) { stonith_notify_client_t *entry = data; stonith_t * stonith = user_data; if (!entry->delete) { pcmk__debug("Removing callback for %s events", entry->event); stonith_api_del_notification(stonith, entry->event); } } static int stonith_api_del_notification(stonith_t * stonith, const char *event) { GList *list_item = NULL; stonith_notify_client_t *new_client = NULL; stonith_private_t *private = stonith->st_private; if (event == NULL) { foreach_notify_entry(private, del_notify_entry, stonith); pcmk__trace("Removed callback"); return pcmk_ok; } pcmk__debug("Removing callback for %s events", event); new_client = pcmk__assert_alloc(1, sizeof(stonith_notify_client_t)); new_client->event = event; new_client->notify = NULL; list_item = g_list_find_custom(private->notify_list, new_client, stonithlib_GCompareFunc); stonith_set_notification(stonith, event, 0); if (list_item != NULL) { stonith_notify_client_t *list_client = list_item->data; if (private->notify_refcnt) { list_client->delete = TRUE; private->notify_deletes = TRUE; } else { private->notify_list = g_list_remove(private->notify_list, list_client); free(list_client); } pcmk__trace("Removed callback"); } else { pcmk__trace("Callback not present"); } free(new_client); return pcmk_ok; } static int stonith_api_add_callback(stonith_t * stonith, int call_id, int timeout, int options, void *user_data, const char *callback_name, void (*callback) (stonith_t * st, stonith_callback_data_t * data)) { stonith_callback_client_t *blob = NULL; stonith_private_t *private = NULL; CRM_CHECK(stonith != NULL, return -EINVAL); CRM_CHECK(stonith->st_private != NULL, return -EINVAL); private = stonith->st_private; if (call_id == 0) { // Add global callback private->op_callback = callback; } else if (call_id < 0) { // Call failed immediately, so call callback now if (!(options & st_opt_report_only_success)) { pcmk__action_result_t result = PCMK__UNKNOWN_RESULT; pcmk__trace("Call failed, calling %s: %s", callback_name, pcmk_strerror(call_id)); pcmk__set_result(&result, CRM_EX_ERROR, stonith__legacy2status(call_id), NULL); invoke_fence_action_callback(stonith, call_id, &result, user_data, callback); } else { pcmk__warn("Fencer call failed: %s", pcmk_strerror(call_id)); } return FALSE; } blob = pcmk__assert_alloc(1, sizeof(stonith_callback_client_t)); blob->id = callback_name; blob->only_success = (options & st_opt_report_only_success) ? TRUE : FALSE; blob->user_data = user_data; blob->callback = callback; blob->allow_timeout_updates = (options & st_opt_timeout_updates) ? TRUE : FALSE; if (timeout > 0) { set_callback_timeout(blob, stonith, call_id, timeout); } pcmk__intkey_table_insert(private->stonith_op_callback_table, call_id, blob); pcmk__trace("Added callback to %s for call %d", callback_name, call_id); return TRUE; } static void stonith_dump_pending_op(gpointer key, gpointer value, gpointer user_data) { int call = GPOINTER_TO_INT(key); stonith_callback_client_t *blob = value; pcmk__debug("Call %d (%s): pending", call, pcmk__s(blob->id, "no ID")); } void stonith_dump_pending_callbacks(stonith_t * stonith) { stonith_private_t *private = stonith->st_private; if (private->stonith_op_callback_table == NULL) { return; } return g_hash_table_foreach(private->stonith_op_callback_table, stonith_dump_pending_op, NULL); } /*! * \internal * \brief Get the data section of a fencer notification * * \param[in] msg Notification XML * \param[in] ntype Notification type */ static xmlNode * get_event_data_xml(xmlNode *msg, const char *ntype) { char *data_addr = pcmk__assert_asprintf("//%s", ntype); xmlNode *data = pcmk__xpath_find_one(msg->doc, data_addr, LOG_DEBUG); free(data_addr); return data; } /* */ static stonith_event_t * xml_to_event(xmlNode *msg) { stonith_event_t *event = pcmk__assert_alloc(1, sizeof(stonith_event_t)); struct event_private *event_private = NULL; event->opaque = pcmk__assert_alloc(1, sizeof(struct event_private)); event_private = (struct event_private *) event->opaque; crm_log_xml_trace(msg, "stonith_notify"); // All notification types have the operation result and notification subtype stonith__xe_get_result(msg, &event_private->result); event->operation = pcmk__xe_get_copy(msg, PCMK__XA_ST_OP); // @COMPAT The API originally provided the result as a legacy return code event->result = pcmk_rc2legacy(stonith__result2rc(&event_private->result)); // Some notification subtypes have additional information if (pcmk__str_eq(event->operation, PCMK__VALUE_ST_NOTIFY_FENCE, pcmk__str_none)) { xmlNode *data = get_event_data_xml(msg, event->operation); if (data == NULL) { pcmk__err("No data for %s event", event->operation); pcmk__log_xml_notice(msg, "BadEvent"); } else { event->origin = pcmk__xe_get_copy(data, PCMK__XA_ST_ORIGIN); event->action = pcmk__xe_get_copy(data, PCMK__XA_ST_DEVICE_ACTION); event->target = pcmk__xe_get_copy(data, PCMK__XA_ST_TARGET); event->executioner = pcmk__xe_get_copy(data, PCMK__XA_ST_DELEGATE); event->id = pcmk__xe_get_copy(data, PCMK__XA_ST_REMOTE_OP); event->client_origin = pcmk__xe_get_copy(data, PCMK__XA_ST_CLIENTNAME); event->device = pcmk__xe_get_copy(data, PCMK__XA_ST_DEVICE_ID); } } else if (pcmk__str_any_of(event->operation, STONITH_OP_DEVICE_ADD, STONITH_OP_DEVICE_DEL, STONITH_OP_LEVEL_ADD, STONITH_OP_LEVEL_DEL, NULL)) { xmlNode *data = get_event_data_xml(msg, event->operation); if (data == NULL) { pcmk__err("No data for %s event", event->operation); pcmk__log_xml_notice(msg, "BadEvent"); } else { event->device = pcmk__xe_get_copy(data, PCMK__XA_ST_DEVICE_ID); } } return event; } static void event_free(stonith_event_t * event) { struct event_private *event_private = event->opaque; free(event->id); free(event->operation); free(event->origin); free(event->action); free(event->target); free(event->executioner); free(event->device); free(event->client_origin); pcmk__reset_result(&event_private->result); free(event->opaque); free(event); } static void stonith_send_notification(gpointer data, gpointer user_data) { struct notify_blob_s *blob = user_data; stonith_notify_client_t *entry = data; stonith_event_t *st_event = NULL; const char *event = NULL; if (blob->xml == NULL) { pcmk__warn("Skipping callback - NULL message"); return; } event = pcmk__xe_get(blob->xml, PCMK__XA_SUBT); if (entry == NULL) { pcmk__warn("Skipping callback - NULL callback client"); return; } else if (entry->delete) { pcmk__trace("Skipping callback - marked for deletion"); return; } else if (entry->notify == NULL) { pcmk__warn("Skipping callback - NULL callback"); return; } else if (!pcmk__str_eq(entry->event, event, pcmk__str_none)) { pcmk__trace("Skipping callback - event mismatch %p/%s vs. %s", entry, entry->event, event); return; } st_event = xml_to_event(blob->xml); pcmk__trace("Invoking callback for %p/%s event...", entry, event); entry->notify(blob->stonith, st_event); pcmk__trace("Callback invoked..."); event_free(st_event); } /*! * \internal * \brief Create and send an API request * * \param[in,out] stonith Stonith connection * \param[in] op API operation to request * \param[in] data Data to attach to request * \param[out] output_data If not NULL, will be set to reply if synchronous * \param[in] call_options Bitmask of stonith_call_options to use * \param[in] timeout Error if not completed within this many seconds * * \return pcmk_ok (for synchronous requests) or positive call ID * (for asynchronous requests) on success, -errno otherwise */ static int stonith_send_command(stonith_t * stonith, const char *op, xmlNode * data, xmlNode ** output_data, int call_options, int timeout) { int rc = 0; int reply_id = -1; xmlNode *op_msg = NULL; xmlNode *op_reply = NULL; stonith_private_t *native = NULL; pcmk__assert((stonith != NULL) && (stonith->st_private != NULL) && (op != NULL)); native = stonith->st_private; if (output_data != NULL) { *output_data = NULL; } if ((stonith->state == stonith_disconnected) || (native->token == NULL)) { return -ENOTCONN; } /* Increment the call ID, which must be positive to avoid conflicting with * error codes. This shouldn't be a problem unless the client mucked with * it or the counter wrapped around. */ stonith->call_id++; if (stonith->call_id < 1) { stonith->call_id = 1; } op_msg = stonith_create_op(stonith->call_id, native->token, op, data, call_options); if (op_msg == NULL) { return -EINVAL; } pcmk__xe_set_int(op_msg, PCMK__XA_ST_TIMEOUT, timeout); pcmk__trace("Sending %s message to fencer with timeout %ds", op, timeout); if (data) { const char *delay_s = pcmk__xe_get(data, PCMK__XA_ST_DELAY); if (delay_s) { pcmk__xe_set(op_msg, PCMK__XA_ST_DELAY, delay_s); } } { enum crm_ipc_flags ipc_flags = crm_ipc_flags_none; if (call_options & st_opt_sync_call) { pcmk__set_ipc_flags(ipc_flags, "stonith command", crm_ipc_client_response); } rc = crm_ipc_send(native->ipc, op_msg, ipc_flags, 1000 * (timeout + 60), &op_reply); } pcmk__xml_free(op_msg); if (rc < 0) { crm_perror(LOG_ERR, "Couldn't perform %s operation (timeout=%ds): %d", op, timeout, rc); rc = -ECOMM; goto done; } crm_log_xml_trace(op_reply, "Reply"); if (!(call_options & st_opt_sync_call)) { pcmk__trace("Async call %d, returning", stonith->call_id); pcmk__xml_free(op_reply); return stonith->call_id; } pcmk__xe_get_int(op_reply, PCMK__XA_ST_CALLID, &reply_id); if (reply_id == stonith->call_id) { pcmk__action_result_t result = PCMK__UNKNOWN_RESULT; pcmk__trace("Synchronous reply %d received", reply_id); stonith__xe_get_result(op_reply, &result); rc = pcmk_rc2legacy(stonith__result2rc(&result)); pcmk__reset_result(&result); if ((call_options & st_opt_discard_reply) || output_data == NULL) { pcmk__trace("Discarding reply"); } else { *output_data = op_reply; op_reply = NULL; /* Prevent subsequent free */ } } else if (reply_id <= 0) { pcmk__err("Received bad reply: No id set"); pcmk__log_xml_err(op_reply, "Bad reply"); pcmk__xml_free(op_reply); op_reply = NULL; rc = -ENOMSG; } else { pcmk__err("Received bad reply: %d (wanted %d)", reply_id, stonith->call_id); pcmk__log_xml_err(op_reply, "Old reply"); pcmk__xml_free(op_reply); op_reply = NULL; rc = -ENOMSG; } done: if (!crm_ipc_connected(native->ipc)) { pcmk__err("Fencer disconnected"); free(native->token); native->token = NULL; stonith->state = stonith_disconnected; } pcmk__xml_free(op_reply); return rc; } /* Not used with mainloop */ bool stonith_dispatch(stonith_t * st) { gboolean stay_connected = TRUE; stonith_private_t *private = NULL; pcmk__assert(st != NULL); private = st->st_private; while (crm_ipc_ready(private->ipc)) { if (crm_ipc_read(private->ipc) > 0) { const char *msg = crm_ipc_buffer(private->ipc); stonith_dispatch_internal(msg, strlen(msg), st); } if (!crm_ipc_connected(private->ipc)) { pcmk__err("Connection closed"); stay_connected = FALSE; } } return stay_connected; } static int stonith_api_free(stonith_t * stonith) { int rc = pcmk_ok; pcmk__trace("Destroying %p", stonith); if (stonith->state != stonith_disconnected) { pcmk__trace("Unregistering notifications and disconnecting %p first", stonith); stonith->cmds->remove_notification(stonith, NULL); rc = stonith->cmds->disconnect(stonith); } if (stonith->state == stonith_disconnected) { stonith_private_t *private = stonith->st_private; pcmk__trace("Removing %u callbacks", g_hash_table_size(private->stonith_op_callback_table)); g_hash_table_destroy(private->stonith_op_callback_table); pcmk__trace("Destroying %u notification clients", g_list_length(private->notify_list)); g_list_free_full(private->notify_list, free); free(stonith->st_private); free(stonith->cmds); free(stonith); } else { pcmk__err("Not free'ing active connection: %s (%d)", pcmk_strerror(rc), rc); } return rc; } void stonith_api_delete(stonith_t * stonith) { pcmk__trace("Destroying %p", stonith); if(stonith) { stonith->cmds->free(stonith); } } static gboolean is_stonith_param(gpointer key, gpointer value, gpointer user_data) { return pcmk_stonith_param(key); } int stonith__validate(stonith_t *st, int call_options, const char *rsc_id, const char *namespace_s, const char *agent, GHashTable *params, int timeout_sec, char **output, char **error_output) { int rc = pcmk_rc_ok; /* Use a dummy node name in case the agent requires a target. We assume the * actual target doesn't matter for validation purposes (if in practice, * that is incorrect, we will need to allow the caller to pass the target). */ const char *target = "node1"; char *host_arg = NULL; if (params != NULL) { host_arg = pcmk__str_copy(g_hash_table_lookup(params, PCMK_STONITH_HOST_ARGUMENT)); /* Remove special stonith params from the table before doing anything else */ g_hash_table_foreach_remove(params, is_stonith_param, NULL); } #if PCMK__ENABLE_CIBSECRETS rc = pcmk__substitute_secrets(rsc_id, params); if (rc != pcmk_rc_ok) { pcmk__warn("Could not replace secret parameters for validation of %s: " "%s", agent, pcmk_rc_str(rc)); // rc is standard return value, don't return it in this function } #endif if (output) { *output = NULL; } if (error_output) { *error_output = NULL; } if (timeout_sec <= 0) { timeout_sec = PCMK_DEFAULT_ACTION_TIMEOUT_MS; } switch (stonith_get_namespace(agent, namespace_s)) { case st_namespace_rhcs: rc = stonith__rhcs_validate(st, call_options, target, agent, params, host_arg, timeout_sec, output, error_output); rc = pcmk_legacy2rc(rc); break; #if HAVE_STONITH_STONITH_H case st_namespace_lha: rc = stonith__lha_validate(st, call_options, target, agent, params, timeout_sec, output, error_output); rc = pcmk_legacy2rc(rc); break; #endif case st_namespace_invalid: errno = ENOENT; rc = errno; if (error_output) { *error_output = pcmk__assert_asprintf("Agent %s not found", agent); } else { pcmk__err("Agent %s not found", agent); } break; default: errno = EOPNOTSUPP; rc = errno; if (error_output) { *error_output = pcmk__assert_asprintf("Agent %s does not " "support validation", agent); } else { pcmk__err("Agent %s does not support validation", agent); } break; } free(host_arg); return rc; } static int stonith_api_validate(stonith_t *st, int call_options, const char *rsc_id, const char *namespace_s, const char *agent, const stonith_key_value_t *params, int timeout_sec, char **output, char **error_output) { /* Validation should be done directly via the agent, so we can get it from * stonith_admin when the cluster is not running, which is important for * higher-level tools. */ int rc = pcmk_ok; GHashTable *params_table = pcmk__strkey_table(free, free); // Convert parameter list to a hash table for (; params; params = params->next) { if (!pcmk_stonith_param(params->key)) { pcmk__insert_dup(params_table, params->key, params->value); } } rc = stonith__validate(st, call_options, rsc_id, namespace_s, agent, params_table, timeout_sec, output, error_output); g_hash_table_destroy(params_table); return rc; } stonith_t * stonith_api_new(void) { stonith_t *new_stonith = NULL; stonith_private_t *private = NULL; new_stonith = calloc(1, sizeof(stonith_t)); if (new_stonith == NULL) { return NULL; } private = calloc(1, sizeof(stonith_private_t)); if (private == NULL) { free(new_stonith); return NULL; } new_stonith->st_private = private; private->stonith_op_callback_table = pcmk__intkey_table(stonith_destroy_op_callback); private->notify_list = NULL; private->notify_refcnt = 0; private->notify_deletes = FALSE; new_stonith->call_id = 1; new_stonith->state = stonith_disconnected; new_stonith->cmds = calloc(1, sizeof(stonith_api_operations_t)); if (new_stonith->cmds == NULL) { free(new_stonith->st_private); free(new_stonith); return NULL; } /* *INDENT-OFF* */ new_stonith->cmds->free = stonith_api_free; new_stonith->cmds->connect = stonith_api_signon; new_stonith->cmds->disconnect = stonith_api_signoff; new_stonith->cmds->list = stonith_api_list; new_stonith->cmds->monitor = stonith_api_monitor; new_stonith->cmds->status = stonith_api_status; new_stonith->cmds->fence = stonith_api_fence; new_stonith->cmds->fence_with_delay = stonith_api_fence_with_delay; new_stonith->cmds->confirm = stonith_api_confirm; new_stonith->cmds->history = stonith_api_history; new_stonith->cmds->list_agents = stonith_api_device_list; new_stonith->cmds->metadata = stonith_api_device_metadata; new_stonith->cmds->query = stonith_api_query; new_stonith->cmds->remove_device = stonith_api_remove_device; new_stonith->cmds->register_device = stonith_api_register_device; new_stonith->cmds->remove_level = stonith_api_remove_level; new_stonith->cmds->remove_level_full = stonith_api_remove_level_full; new_stonith->cmds->register_level = stonith_api_register_level; new_stonith->cmds->register_level_full = stonith_api_register_level_full; new_stonith->cmds->remove_callback = stonith_api_del_callback; new_stonith->cmds->register_callback = stonith_api_add_callback; new_stonith->cmds->remove_notification = stonith_api_del_notification; new_stonith->cmds->register_notification = stonith_api_add_notification; new_stonith->cmds->validate = stonith_api_validate; /* *INDENT-ON* */ return new_stonith; } /*! * \brief Make a blocking connection attempt to the fencer * * \param[in,out] st Fencer API object * \param[in] name Client name to use with fencer * \param[in] max_attempts Return error if this many attempts fail * * \return pcmk_ok on success, result of last attempt otherwise */ int stonith_api_connect_retry(stonith_t *st, const char *name, int max_attempts) { int rc = -EINVAL; // if max_attempts is not positive for (int attempt = 1; attempt <= max_attempts; attempt++) { rc = st->cmds->connect(st, name, NULL); if (rc == pcmk_ok) { return pcmk_ok; } else if (attempt < max_attempts) { pcmk__notice("Fencer connection attempt %d of %d failed (retrying " "in 2s): %s " QB_XS " rc=%d", attempt, max_attempts, pcmk_strerror(rc), rc); sleep(2); } } pcmk__notice("Could not connect to fencer: %s " QB_XS " rc=%d", pcmk_strerror(rc), rc); return rc; } stonith_key_value_t * stonith_key_value_add(stonith_key_value_t * head, const char *key, const char *value) { stonith_key_value_t *p, *end; p = pcmk__assert_alloc(1, sizeof(stonith_key_value_t)); p->key = pcmk__str_copy(key); p->value = pcmk__str_copy(value); end = head; while (end && end->next) { end = end->next; } if (end) { end->next = p; } else { head = p; } return head; } void stonith_key_value_freeall(stonith_key_value_t * head, int keys, int values) { stonith_key_value_t *p; while (head) { p = head->next; if (keys) { free(head->key); } if (values) { free(head->value); } free(head); head = p; } } #define api_log_open() openlog("stonith-api", LOG_CONS | LOG_NDELAY | LOG_PID, LOG_DAEMON) #define api_log(level, fmt, args...) syslog(level, "%s: "fmt, __func__, args) int stonith_api_kick(uint32_t nodeid, const char *uname, int timeout, bool off) { int rc = pcmk_ok; stonith_t *st = stonith_api_new(); const char *action = off? PCMK_ACTION_OFF : PCMK_ACTION_REBOOT; api_log_open(); if (st == NULL) { api_log(LOG_ERR, "API initialization failed, could not kick (%s) node %u/%s", action, nodeid, uname); return -EPROTO; } rc = st->cmds->connect(st, "stonith-api", NULL); if (rc != pcmk_ok) { api_log(LOG_ERR, "Connection failed, could not kick (%s) node %u/%s : %s (%d)", action, nodeid, uname, pcmk_strerror(rc), rc); } else { char *name = (uname == NULL)? pcmk__itoa(nodeid) : strdup(uname); int opts = 0; stonith__set_call_options(opts, name, st_opt_sync_call|st_opt_allow_self_fencing); if ((uname == NULL) && (nodeid > 0)) { stonith__set_call_options(opts, name, st_opt_cs_nodeid); } rc = st->cmds->fence(st, opts, name, action, timeout, 0); free(name); if (rc != pcmk_ok) { api_log(LOG_ERR, "Could not kick (%s) node %u/%s : %s (%d)", action, nodeid, uname, pcmk_strerror(rc), rc); } else { api_log(LOG_NOTICE, "Node %u/%s kicked: %s", nodeid, uname, action); } } stonith_api_delete(st); return rc; } time_t stonith_api_time(uint32_t nodeid, const char *uname, bool in_progress) { int rc = pcmk_ok; time_t when = 0; stonith_t *st = stonith_api_new(); stonith_history_t *history = NULL, *hp = NULL; if (st == NULL) { api_log(LOG_ERR, "Could not retrieve fence history for %u/%s: " "API initialization failed", nodeid, uname); return when; } rc = st->cmds->connect(st, "stonith-api", NULL); if (rc != pcmk_ok) { api_log(LOG_NOTICE, "Connection failed: %s (%d)", pcmk_strerror(rc), rc); } else { int entries = 0; int progress = 0; int completed = 0; int opts = 0; char *name = (uname == NULL)? pcmk__itoa(nodeid) : strdup(uname); stonith__set_call_options(opts, name, st_opt_sync_call); if ((uname == NULL) && (nodeid > 0)) { stonith__set_call_options(opts, name, st_opt_cs_nodeid); } rc = st->cmds->history(st, opts, name, &history, 120); free(name); for (hp = history; hp; hp = hp->next) { entries++; if (in_progress) { progress++; if (hp->state != st_done && hp->state != st_failed) { when = time(NULL); } } else if (hp->state == st_done) { completed++; if (hp->completed > when) { when = hp->completed; } } } stonith_history_free(history); if(rc == pcmk_ok) { api_log(LOG_INFO, "Found %d entries for %u/%s: %d in progress, %d completed", entries, nodeid, uname, progress, completed); } else { api_log(LOG_ERR, "Could not retrieve fence history for %u/%s: %s (%d)", nodeid, uname, pcmk_strerror(rc), rc); } } stonith_api_delete(st); if(when) { api_log(LOG_INFO, "Node %u/%s last kicked at: %ld", nodeid, uname, (long int)when); } return when; } bool stonith_agent_exists(const char *agent, int timeout) { stonith_t *st = NULL; stonith_key_value_t *devices = NULL; stonith_key_value_t *dIter = NULL; bool rc = FALSE; if (agent == NULL) { return rc; } st = stonith_api_new(); if (st == NULL) { pcmk__err("Could not list fence agents: API memory allocation failed"); return FALSE; } st->cmds->list_agents(st, st_opt_sync_call, NULL, &devices, timeout == 0 ? 120 : timeout); for (dIter = devices; dIter != NULL; dIter = dIter->next) { if (pcmk__str_eq(dIter->value, agent, pcmk__str_none)) { rc = TRUE; break; } } stonith_key_value_freeall(devices, 1, 1); stonith_api_delete(st); return rc; } const char * stonith_action_str(const char *action) { if (action == NULL) { return "fencing"; } else if (strcmp(action, PCMK_ACTION_ON) == 0) { return "unfencing"; } else if (strcmp(action, PCMK_ACTION_OFF) == 0) { return "turning off"; } else { return action; } } /*! * \internal * \brief Parse a target name from one line of a target list string * * \param[in] line One line of a target list string * \param[in] len String length of line * \param[in,out] output List to add newly allocated target name to */ static void parse_list_line(const char *line, int len, GList **output) { size_t i = 0; size_t entry_start = 0; /* Skip complaints about additional parameters device doesn't understand * * @TODO Document or eliminate the implied restriction of target names */ if (strstr(line, "invalid") || strstr(line, "variable")) { pcmk__debug("Skipping list output line: %s", line); return; } // Process line content, character by character for (i = 0; i <= len; i++) { if (isspace(line[i]) || (line[i] == ',') || (line[i] == ';') || (line[i] == '\0')) { // We've found a separator (i.e. the end of an entry) int rc = 0; char *entry = NULL; if (i == entry_start) { // Skip leading and sequential separators entry_start = i + 1; continue; } entry = pcmk__assert_alloc(i - entry_start + 1, sizeof(char)); /* Read entry, stopping at first separator * * @TODO Document or eliminate these character restrictions */ rc = sscanf(line + entry_start, "%[a-zA-Z0-9_-.]", entry); if (rc != 1) { pcmk__warn("Could not parse list output entry: %s " QB_XS " entry_start=%d position=%d", (line + entry_start), entry_start, i); free(entry); } else if (pcmk__strcase_any_of(entry, PCMK_ACTION_ON, PCMK_ACTION_OFF, NULL)) { /* Some agents print the target status in the list output, * though none are known now (the separate list-status command * is used for this, but it can also print "UNKNOWN"). To handle * this possibility, skip such entries. * * @TODO Document or eliminate the implied restriction of target * names. */ free(entry); } else { // We have a valid entry *output = g_list_append(*output, entry); } entry_start = i + 1; } } } /*! * \internal * \brief Parse a list of targets from a string * * \param[in] list_output Target list as a string * * \return List of target names * \note The target list string format is flexible, to allow for user-specified * lists such pcmk_host_list and the output of an agent's list action * (whether direct or via the API, which escapes newlines). There may be * multiple lines, separated by either a newline or an escaped newline * (backslash n). Each line may have one or more target names, separated * by any combination of whitespace, commas, and semi-colons. Lines * containing "invalid" or "variable" will be ignored entirely. Target * names "on" or "off" (case-insensitive) will be ignored. Target names * may contain only alphanumeric characters, underbars (_), dashes (-), * and dots (.) (if any other character occurs in the name, it and all * subsequent characters in the name will be ignored). * \note The caller is responsible for freeing the result with * g_list_free_full(result, free). */ GList * stonith__parse_targets(const char *target_spec) { GList *targets = NULL; if (target_spec != NULL) { size_t out_len = strlen(target_spec); size_t line_start = 0; // Starting index of line being processed for (size_t i = 0; i <= out_len; ++i) { if ((target_spec[i] == '\n') || (target_spec[i] == '\0') || ((target_spec[i] == '\\') && (target_spec[i + 1] == 'n'))) { // We've reached the end of one line of output int len = i - line_start; if (len > 0) { char *line = strndup(target_spec + line_start, len); line[len] = '\0'; // Because it might be a newline parse_list_line(line, len, &targets); free(line); } if (target_spec[i] == '\\') { ++i; // backslash-n takes up two positions } line_start = i + 1; } } } return targets; } /*! * \internal * \brief Check whether a fencing failure was followed by an equivalent success * * \param[in] event Fencing failure * \param[in] top_history Complete fencing history (must be sorted by * stonith__sort_history() beforehand) * * \return The name of the node that executed the fencing if a later successful * event exists, or NULL if no such event exists */ const char * stonith__later_succeeded(const stonith_history_t *event, const stonith_history_t *top_history) { const char *other = NULL; for (const stonith_history_t *prev_hp = top_history; prev_hp != NULL; prev_hp = prev_hp->next) { if (prev_hp == event) { break; } if ((prev_hp->state == st_done) && pcmk__str_eq(event->target, prev_hp->target, pcmk__str_casei) && pcmk__str_eq(event->action, prev_hp->action, pcmk__str_none) && ((event->completed < prev_hp->completed) || ((event->completed == prev_hp->completed) && (event->completed_nsec < prev_hp->completed_nsec)))) { if ((event->delegate == NULL) || pcmk__str_eq(event->delegate, prev_hp->delegate, pcmk__str_casei)) { // Prefer equivalent fencing by same executioner return prev_hp->delegate; } else if (other == NULL) { // Otherwise remember first successful executioner other = (prev_hp->delegate == NULL)? "some node" : prev_hp->delegate; } } } return other; } /*! * \internal * \brief Sort fencing history, pending first then by most recently completed * * \param[in,out] history List of stonith actions * * \return New head of sorted \p history */ stonith_history_t * stonith__sort_history(stonith_history_t *history) { stonith_history_t *new = NULL, *pending = NULL, *hp, *np, *tmp; for (hp = history; hp; ) { tmp = hp->next; if ((hp->state == st_done) || (hp->state == st_failed)) { /* sort into new */ if ((!new) || (hp->completed > new->completed) || ((hp->completed == new->completed) && (hp->completed_nsec > new->completed_nsec))) { hp->next = new; new = hp; } else { np = new; do { if ((!np->next) || (hp->completed > np->next->completed) || ((hp->completed == np->next->completed) && (hp->completed_nsec > np->next->completed_nsec))) { hp->next = np->next; np->next = hp; break; } np = np->next; } while (1); } } else { /* put into pending */ hp->next = pending; pending = hp; } hp = tmp; } /* pending actions don't have a completed-stamp so make them go front */ if (pending) { stonith_history_t *last_pending = pending; while (last_pending->next) { last_pending = last_pending->next; } last_pending->next = new; new = pending; } return new; } /*! * \brief Return string equivalent of an operation state value * * \param[in] state Fencing operation state value * * \return Human-friendly string equivalent of state */ const char * stonith_op_state_str(enum op_state state) { switch (state) { case st_query: return "querying"; case st_exec: return "executing"; case st_done: return "completed"; case st_duplicate: return "duplicate"; case st_failed: return "failed"; } return "unknown"; } stonith_history_t * stonith__first_matching_event(stonith_history_t *history, bool (*matching_fn)(stonith_history_t *, void *), void *user_data) { for (stonith_history_t *hp = history; hp; hp = hp->next) { if (matching_fn(hp, user_data)) { return hp; } } return NULL; } bool stonith__event_state_pending(stonith_history_t *history, void *user_data) { return history->state != st_failed && history->state != st_done; } bool stonith__event_state_eq(stonith_history_t *history, void *user_data) { return history->state == GPOINTER_TO_INT(user_data); } bool stonith__event_state_neq(stonith_history_t *history, void *user_data) { return history->state != GPOINTER_TO_INT(user_data); } void stonith__device_parameter_flags(uint32_t *device_flags, const char *device_name, xmlNode *metadata) { xmlXPathObject *xpath = NULL; int max = 0; int lpc = 0; CRM_CHECK((device_flags != NULL) && (metadata != NULL), return); xpath = pcmk__xpath_search(metadata->doc, "//" PCMK_XE_PARAMETER); max = pcmk__xpath_num_results(xpath); if (max == 0) { xmlXPathFreeObject(xpath); return; } for (lpc = 0; lpc < max; lpc++) { const char *parameter = NULL; xmlNode *match = pcmk__xpath_result(xpath, lpc); CRM_LOG_ASSERT(match != NULL); if (match == NULL) { continue; } parameter = pcmk__xe_get(match, PCMK_XA_NAME); if (pcmk__str_eq(parameter, "plug", pcmk__str_casei)) { stonith__set_device_flags(*device_flags, device_name, st_device_supports_parameter_plug); } else if (pcmk__str_eq(parameter, "port", pcmk__str_casei)) { stonith__set_device_flags(*device_flags, device_name, st_device_supports_parameter_port); } } xmlXPathFreeObject(xpath); } /*! * \internal * \brief Retrieve fence agent meta-data asynchronously * * \param[in] agent Agent to execute * \param[in] timeout_sec Error if not complete within this time * \param[in] callback Function to call with result (this will always be * called, whether by this function directly or * later via the main loop, and on success the * metadata will be in its result argument's * action_stdout) * \param[in,out] user_data User data to pass to callback * * \return Standard Pacemaker return code * \note The caller must use a main loop. This function is not a * stonith_api_operations_t method because it does not need a stonith_t * object and does not go through the fencer, but executes the agent * directly. */ int stonith__metadata_async(const char *agent, int timeout_sec, void (*callback)(int pid, const pcmk__action_result_t *result, void *user_data), void *user_data) { switch (stonith_get_namespace(agent, NULL)) { case st_namespace_rhcs: { stonith_action_t *action = NULL; int rc = pcmk_ok; action = stonith__action_create(agent, PCMK_ACTION_METADATA, NULL, 0, timeout_sec, NULL, NULL, NULL); rc = stonith__execute_async(action, user_data, callback, NULL); if (rc != pcmk_ok) { callback(0, stonith__action_result(action), user_data); stonith__destroy_action(action); } return pcmk_legacy2rc(rc); } #if HAVE_STONITH_STONITH_H case st_namespace_lha: // LHA metadata is simply synthesized, so simulate async { pcmk__action_result_t result = { .exit_status = CRM_EX_OK, .execution_status = PCMK_EXEC_DONE, .exit_reason = NULL, .action_stdout = NULL, .action_stderr = NULL, }; stonith__lha_metadata(agent, timeout_sec, &result.action_stdout); callback(0, &result, user_data); pcmk__reset_result(&result); return pcmk_rc_ok; } #endif default: { pcmk__action_result_t result = { .exit_status = CRM_EX_NOSUCH, .execution_status = PCMK_EXEC_ERROR_HARD, .exit_reason = pcmk__assert_asprintf("No such agent '%s'", agent), .action_stdout = NULL, .action_stderr = NULL, }; callback(0, &result, user_data); pcmk__reset_result(&result); return ENOENT; } } } /*! * \internal * \brief Return the exit status from an async action callback * * \param[in] data Callback data * * \return Exit status from callback data */ int stonith__exit_status(const stonith_callback_data_t *data) { if ((data == NULL) || (data->opaque == NULL)) { return CRM_EX_ERROR; } return ((pcmk__action_result_t *) data->opaque)->exit_status; } /*! * \internal * \brief Return the execution status from an async action callback * * \param[in] data Callback data * * \return Execution status from callback data */ int stonith__execution_status(const stonith_callback_data_t *data) { if ((data == NULL) || (data->opaque == NULL)) { return PCMK_EXEC_UNKNOWN; } return ((pcmk__action_result_t *) data->opaque)->execution_status; } /*! * \internal * \brief Return the exit reason from an async action callback * * \param[in] data Callback data * * \return Exit reason from callback data */ const char * stonith__exit_reason(const stonith_callback_data_t *data) { if ((data == NULL) || (data->opaque == NULL)) { return NULL; } return ((pcmk__action_result_t *) data->opaque)->exit_reason; } /*! * \internal * \brief Return the exit status from an event notification * * \param[in] event Event * * \return Exit status from event */ int stonith__event_exit_status(const stonith_event_t *event) { if ((event == NULL) || (event->opaque == NULL)) { return CRM_EX_ERROR; } else { struct event_private *event_private = event->opaque; return event_private->result.exit_status; } } /*! * \internal * \brief Return the execution status from an event notification * * \param[in] event Event * * \return Execution status from event */ int stonith__event_execution_status(const stonith_event_t *event) { if ((event == NULL) || (event->opaque == NULL)) { return PCMK_EXEC_UNKNOWN; } else { struct event_private *event_private = event->opaque; return event_private->result.execution_status; } } /*! * \internal * \brief Return the exit reason from an event notification * * \param[in] event Event * * \return Exit reason from event */ const char * stonith__event_exit_reason(const stonith_event_t *event) { if ((event == NULL) || (event->opaque == NULL)) { return NULL; } else { struct event_private *event_private = event->opaque; return event_private->result.exit_reason; } } /*! * \internal * \brief Return a human-friendly description of a fencing event * * \param[in] event Event to describe * * \return Newly allocated string with description of \p event * \note The caller is responsible for freeing the return value. * This function asserts on memory errors and never returns NULL. */ char * stonith__event_description(const stonith_event_t *event) { // Use somewhat readable defaults const char *origin = pcmk__s(event->client_origin, "a client"); const char *origin_node = pcmk__s(event->origin, "a node"); const char *executioner = pcmk__s(event->executioner, "the cluster"); const char *device = pcmk__s(event->device, "unknown"); const char *action = pcmk__s(event->action, event->operation); const char *target = pcmk__s(event->target, "no node"); const char *reason = stonith__event_exit_reason(event); const char *status; if (action == NULL) { action = "(unknown)"; } if (stonith__event_execution_status(event) != PCMK_EXEC_DONE) { status = pcmk_exec_status_str(stonith__event_execution_status(event)); } else if (stonith__event_exit_status(event) != CRM_EX_OK) { status = pcmk_exec_status_str(PCMK_EXEC_ERROR); } else { status = crm_exit_str(CRM_EX_OK); } if (pcmk__str_eq(event->operation, PCMK__VALUE_ST_NOTIFY_HISTORY, pcmk__str_none)) { return pcmk__assert_asprintf("Fencing history may have changed"); } else if (pcmk__str_eq(event->operation, STONITH_OP_DEVICE_ADD, pcmk__str_none)) { return pcmk__assert_asprintf("A fencing device (%s) was added", device); } else if (pcmk__str_eq(event->operation, STONITH_OP_DEVICE_DEL, pcmk__str_none)) { return pcmk__assert_asprintf("A fencing device (%s) was removed", device); } else if (pcmk__str_eq(event->operation, STONITH_OP_LEVEL_ADD, pcmk__str_none)) { return pcmk__assert_asprintf("A fencing topology level (%s) was added", device); } else if (pcmk__str_eq(event->operation, STONITH_OP_LEVEL_DEL, pcmk__str_none)) { return pcmk__assert_asprintf("A fencing topology level (%s) was " "removed", device); } // event->operation should be PCMK__VALUE_ST_NOTIFY_FENCE at this point return pcmk__assert_asprintf("Operation %s of %s by %s for %s@%s: %s%s%s%s " "(ref=%s)", action, target, executioner, origin, origin_node, status, ((reason == NULL)? "" : " ("), pcmk__s(reason, ""), ((reason == NULL)? "" : ")"), pcmk__s(event->id, "(none)")); } diff --git a/lib/pacemaker/pcmk_injections.c b/lib/pacemaker/pcmk_injections.c index 7a307e9c50..d205bf2ebe 100644 --- a/lib/pacemaker/pcmk_injections.c +++ b/lib/pacemaker/pcmk_injections.c @@ -1,802 +1,802 @@ /* * 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 #include #include #include #include #include #include // xmlNode #include #include #include #include // crm_create_nvpair_xml() #include #include #include #include // lrmd_event_data_t, etc. #include #include #include #include "libpacemaker_private.h" // @TODO Replace this with a new scheduler flag bool pcmk__simulate_node_config = false; #define XPATH_NODE_CONFIG "//" PCMK_XE_NODE "[@" PCMK_XA_UNAME "='%s']" #define XPATH_NODE_STATE "//" PCMK__XE_NODE_STATE "[@" PCMK_XA_UNAME "='%s']" #define XPATH_NODE_STATE_BY_ID "//" PCMK__XE_NODE_STATE "[@" PCMK_XA_ID "='%s']" #define XPATH_RSC_HISTORY XPATH_NODE_STATE \ "//" PCMK__XE_LRM_RESOURCE "[@" PCMK_XA_ID "='%s']" /*! * \internal * \brief Inject a fictitious transient node attribute into scheduler input * * \param[in,out] out Output object for displaying error messages * \param[in,out] cib_node \c PCMK__XE_NODE_STATE XML to inject attribute into * \param[in] name Transient node attribute name to inject * \param[in] value Transient node attribute value to inject */ static void inject_transient_attr(pcmk__output_t *out, xmlNode *cib_node, const char *name, const char *value) { xmlNode *attrs = NULL; xmlNode *instance_attrs = NULL; const char *node_uuid = pcmk__xe_id(cib_node); out->message(out, "inject-attr", name, value, cib_node); attrs = pcmk__xe_first_child(cib_node, PCMK__XE_TRANSIENT_ATTRIBUTES, NULL, NULL); if (attrs == NULL) { attrs = pcmk__xe_create(cib_node, PCMK__XE_TRANSIENT_ATTRIBUTES); pcmk__xe_set(attrs, PCMK_XA_ID, node_uuid); } instance_attrs = pcmk__xe_first_child(attrs, PCMK_XE_INSTANCE_ATTRIBUTES, NULL, NULL); if (instance_attrs == NULL) { instance_attrs = pcmk__xe_create(attrs, PCMK_XE_INSTANCE_ATTRIBUTES); pcmk__xe_set(instance_attrs, PCMK_XA_ID, node_uuid); } crm_create_nvpair_xml(instance_attrs, NULL, name, value); } /*! * \internal * \brief Inject a fictitious fail count into a scheduler input * * \param[in,out] out Output object for displaying error messages * \param[in,out] cib_conn CIB connection * \param[in,out] cib_node Node state XML to inject into * \param[in] resource ID of resource for fail count to inject * \param[in] task Action name for fail count to inject * \param[in] interval_ms Action interval (in milliseconds) for fail count * \param[in] exit_status Action result for fail count to inject (if * \c PCMK_OCF_OK, or \c PCMK_OCF_NOT_RUNNING when * \p interval_ms is 0, inject nothing) * \param[in] infinity If true, set fail count to "INFINITY", otherwise * increase it by 1 */ void pcmk__inject_failcount(pcmk__output_t *out, cib_t *cib_conn, xmlNode *cib_node, const char *resource, const char *task, guint interval_ms, int exit_status, bool infinity) { char *name = NULL; char *value = NULL; int failcount = 0; xmlNode *output = NULL; CRM_CHECK((out != NULL) && (cib_conn != NULL) && (cib_node != NULL) && (resource != NULL) && (task != NULL), return); if ((exit_status == PCMK_OCF_OK) || ((exit_status == PCMK_OCF_NOT_RUNNING) && (interval_ms == 0))) { return; } // Get current failcount and increment it name = pcmk__failcount_name(resource, task, interval_ms); if (cib__get_node_attrs(out, cib_conn, PCMK_XE_STATUS, pcmk__xe_id(cib_node), NULL, NULL, NULL, name, NULL, &output) == pcmk_rc_ok) { if (pcmk__xe_get_int(output, PCMK_XA_VALUE, &failcount) != pcmk_rc_ok) { failcount = 0; } } if (infinity) { value = pcmk__str_copy(PCMK_VALUE_INFINITY); } else { value = pcmk__itoa(failcount + 1); } inject_transient_attr(out, cib_node, name, value); free(name); free(value); pcmk__xml_free(output); name = pcmk__lastfailure_name(resource, task, interval_ms); value = pcmk__ttoa(time(NULL)); inject_transient_attr(out, cib_node, name, value); free(name); free(value); } /*! * \internal * \brief Create a CIB configuration entry for a fictitious node * * \param[in,out] cib_conn CIB object to use * \param[in] node Node name to use */ static void create_node_entry(cib_t *cib_conn, const char *node) { int rc = pcmk_ok; char *xpath = pcmk__assert_asprintf(XPATH_NODE_CONFIG, node); rc = cib_conn->cmds->query(cib_conn, xpath, NULL, cib_xpath|cib_sync_call); if (rc == -ENXIO) { // Only add if not already existing xmlNode *cib_object = pcmk__xe_create(NULL, PCMK_XE_NODE); pcmk__xe_set(cib_object, PCMK_XA_ID, node); // Use node name as ID pcmk__xe_set(cib_object, PCMK_XA_UNAME, node); cib_conn->cmds->create(cib_conn, PCMK_XE_NODES, cib_object, cib_sync_call); /* Not bothering with subsequent query to see if it exists, we'll bomb out later in the call to query_node_uuid()... */ pcmk__xml_free(cib_object); } free(xpath); } /*! * \internal * \brief Synthesize a fake executor event for an action * * \param[in] cib_resource XML for any existing resource action history * \param[in] task Name of action to synthesize * \param[in] interval_ms Interval of action to synthesize * \param[in] outcome Result of action to synthesize * * \return Newly allocated executor event * \note It is the caller's responsibility to free the result with * lrmd_free_event(). */ static lrmd_event_data_t * create_op(const xmlNode *cib_resource, const char *task, guint interval_ms, int outcome) { lrmd_event_data_t *op = NULL; xmlNode *xop = NULL; op = lrmd_new_event(pcmk__xe_id(cib_resource), task, interval_ms); lrmd__set_result(op, outcome, PCMK_EXEC_DONE, "Simulated action result"); op->params = NULL; // Not needed for simulation purposes op->t_run = time(NULL); op->t_rcchange = op->t_run; // Use a call ID higher than any existing history entries op->call_id = 0; for (xop = pcmk__xe_first_child(cib_resource, NULL, NULL, NULL); xop != NULL; xop = pcmk__xe_next(xop, NULL)) { int tmp = 0; pcmk__xe_get_int(xop, PCMK__XA_CALL_ID, &tmp); if (tmp > op->call_id) { op->call_id = tmp; } } op->call_id++; return op; } /*! * \internal * \brief Inject a fictitious resource history entry into a scheduler input * * \param[in,out] cib_resource Resource history XML to inject entry into * \param[in,out] op Action result to inject * \param[in] node Name of node where the action occurred * \param[in] target_rc Expected result for action to inject * * \return XML of injected resource history entry */ xmlNode * pcmk__inject_action_result(xmlNode *cib_resource, lrmd_event_data_t *op, const char *node, int target_rc) { return pcmk__create_history_xml(cib_resource, op, CRM_FEATURE_SET, target_rc, node, crm_system_name); } /*! * \internal * \brief Inject a fictitious node into a scheduler input * * \param[in,out] cib_conn Scheduler input CIB to inject node into * \param[in] node Name of node to inject * \param[in] uuid UUID of node to inject * * \return XML of \c PCMK__XE_NODE_STATE entry for new node * \note If the global pcmk__simulate_node_config has been set to true, a * node entry in the configuration section will be added, as well as a * node state entry in the status section. */ xmlNode * pcmk__inject_node(cib_t *cib_conn, const char *node, const char *uuid) { int rc = pcmk_ok; xmlNode *cib_object = NULL; char *xpath = pcmk__assert_asprintf(XPATH_NODE_STATE, node); bool duplicate = false; char *found_uuid = NULL; if (pcmk__simulate_node_config) { create_node_entry(cib_conn, node); } rc = cib_conn->cmds->query(cib_conn, xpath, &cib_object, cib_xpath|cib_sync_call); if ((cib_object != NULL) && (pcmk__xe_id(cib_object) == NULL)) { pcmk__err("Detected multiple " PCMK__XE_NODE_STATE " entries for " "xpath=%s, bailing", xpath); duplicate = true; goto done; } if (rc == -ENXIO) { if (uuid == NULL) { query_node_uuid(cib_conn, node, &found_uuid, NULL); } else { found_uuid = strdup(uuid); } if (found_uuid) { char *xpath_by_uuid = pcmk__assert_asprintf(XPATH_NODE_STATE_BY_ID, found_uuid); /* It's possible that a PCMK__XE_NODE_STATE entry doesn't have a * PCMK_XA_UNAME yet */ rc = cib_conn->cmds->query(cib_conn, xpath_by_uuid, &cib_object, cib_xpath|cib_sync_call); if ((cib_object != NULL) && (pcmk__xe_id(cib_object) == NULL)) { pcmk__err("Can't inject node state for %s because multiple " "state entries found for ID %s", node, found_uuid); duplicate = true; free(xpath_by_uuid); goto done; } else if (cib_object != NULL) { pcmk__xe_set(cib_object, PCMK_XA_UNAME, node); rc = cib_conn->cmds->modify(cib_conn, PCMK_XE_STATUS, cib_object, cib_sync_call); } free(xpath_by_uuid); } } if (rc == -ENXIO) { cib_object = pcmk__xe_create(NULL, PCMK__XE_NODE_STATE); pcmk__xe_set(cib_object, PCMK_XA_ID, found_uuid); pcmk__xe_set(cib_object, PCMK_XA_UNAME, node); cib_conn->cmds->create(cib_conn, PCMK_XE_STATUS, cib_object, cib_sync_call); pcmk__xml_free(cib_object); rc = cib_conn->cmds->query(cib_conn, xpath, &cib_object, cib_xpath|cib_sync_call); pcmk__trace("Injecting node state for %s (rc=%d)", node, rc); } done: free(found_uuid); free(xpath); if (duplicate) { pcmk__log_xml_warn(cib_object, "Duplicates"); crm_exit(CRM_EX_SOFTWARE); return NULL; // not reached, but makes static analysis happy } pcmk__assert(rc == pcmk_ok); return cib_object; } /*! * \internal * \brief Inject a fictitious node state change into a scheduler input * * \param[in,out] cib_conn Scheduler input CIB to inject into * \param[in] node Name of node to inject change for * \param[in] up If true, change state to online, otherwise offline * * \return XML of changed (or added) node state entry */ xmlNode * pcmk__inject_node_state_change(cib_t *cib_conn, const char *node, bool up) { xmlNode *cib_node = pcmk__inject_node(cib_conn, node, NULL); if (up) { pcmk__xe_set_props(cib_node, PCMK__XA_IN_CCM, PCMK_VALUE_TRUE, PCMK_XA_CRMD, PCMK_VALUE_ONLINE, PCMK__XA_JOIN, CRMD_JOINSTATE_MEMBER, PCMK_XA_EXPECTED, CRMD_JOINSTATE_MEMBER, NULL); } else { pcmk__xe_set_props(cib_node, PCMK__XA_IN_CCM, PCMK_VALUE_FALSE, PCMK_XA_CRMD, PCMK_VALUE_OFFLINE, PCMK__XA_JOIN, CRMD_JOINSTATE_DOWN, PCMK_XA_EXPECTED, CRMD_JOINSTATE_DOWN, NULL); } pcmk__xe_set(cib_node, PCMK_XA_CRM_DEBUG_ORIGIN, crm_system_name); return cib_node; } /*! * \internal * \brief Check whether a node has history for a given resource * * \param[in,out] cib_node Node state XML to check * \param[in] resource Resource name to check for * * \return Resource's \c PCMK__XE_LRM_RESOURCE XML entry beneath \p cib_node if * found, otherwise \c NULL */ static xmlNode * find_resource_xml(xmlNode *cib_node, const char *resource) { const char *node = pcmk__xe_get(cib_node, PCMK_XA_UNAME); char *xpath = pcmk__assert_asprintf(XPATH_RSC_HISTORY, node, resource); xmlNode *match = pcmk__xpath_find_one(cib_node->doc, xpath, PCMK__LOG_TRACE); free(xpath); return match; } /*! * \internal * \brief Inject a resource history element into a scheduler input * * \param[in,out] out Output object for displaying error messages * \param[in,out] cib_node Node state XML to inject resource history entry into * \param[in] resource ID (in configuration) of resource to inject * \param[in] lrm_name ID as used in history (could be clone instance) * \param[in] rclass Resource agent class of resource to inject * \param[in] rtype Resource agent type of resource to inject * \param[in] rprovider Resource agent provider of resource to inject * * \return XML of injected resource history element * \note If a history element already exists under either \p resource or * \p lrm_name, this will return it rather than injecting a new one. */ xmlNode * pcmk__inject_resource_history(pcmk__output_t *out, xmlNode *cib_node, const char *resource, const char *lrm_name, const char *rclass, const char *rtype, const char *rprovider) { xmlNode *lrm = NULL; xmlNode *container = NULL; xmlNode *cib_resource = NULL; cib_resource = find_resource_xml(cib_node, resource); if (cib_resource != NULL) { /* If an existing LRM history entry uses the resource name, * continue using it, even if lrm_name is different. */ return cib_resource; } // Check for history entry under preferred name if (strcmp(resource, lrm_name) != 0) { cib_resource = find_resource_xml(cib_node, lrm_name); if (cib_resource != NULL) { return cib_resource; } } if ((rclass == NULL) || (rtype == NULL)) { // @TODO query configuration for class, provider, type out->err(out, "Resource %s not found in the status section of %s " "(supply class and type to continue)", resource, pcmk__xe_id(cib_node)); return NULL; } else if (!pcmk__strcase_any_of(rclass, PCMK_RESOURCE_CLASS_OCF, PCMK_RESOURCE_CLASS_STONITH, PCMK_RESOURCE_CLASS_SERVICE, PCMK_RESOURCE_CLASS_SYSTEMD, PCMK_RESOURCE_CLASS_LSB, NULL)) { out->err(out, "Invalid class for %s: %s", resource, rclass); return NULL; } else if (pcmk__is_set(pcmk_get_ra_caps(rclass), pcmk_ra_cap_provider) && (rprovider == NULL)) { // @TODO query configuration for provider out->err(out, "Please specify the provider for resource %s", resource); return NULL; } pcmk__info("Injecting new resource %s into node state '%s'", lrm_name, pcmk__xe_id(cib_node)); lrm = pcmk__xe_first_child(cib_node, PCMK__XE_LRM, NULL, NULL); if (lrm == NULL) { const char *node_uuid = pcmk__xe_id(cib_node); lrm = pcmk__xe_create(cib_node, PCMK__XE_LRM); pcmk__xe_set(lrm, PCMK_XA_ID, node_uuid); } container = pcmk__xe_first_child(lrm, PCMK__XE_LRM_RESOURCES, NULL, NULL); if (container == NULL) { container = pcmk__xe_create(lrm, PCMK__XE_LRM_RESOURCES); } cib_resource = pcmk__xe_create(container, PCMK__XE_LRM_RESOURCE); // If we're creating a new entry, use the preferred name pcmk__xe_set(cib_resource, PCMK_XA_ID, lrm_name); pcmk__xe_set(cib_resource, PCMK_XA_CLASS, rclass); pcmk__xe_set(cib_resource, PCMK_XA_PROVIDER, rprovider); pcmk__xe_set(cib_resource, PCMK_XA_TYPE, rtype); return cib_resource; } /*! * \internal * \brief Inject a ticket attribute into ticket state * * \param[in,out] out Output object for displaying error messages * \param[in] ticket_id Ticket whose state should be changed * \param[in] attr_name Ticket attribute name to inject * \param[in] attr_value Boolean value of ticket attribute to inject * \param[in,out] cib CIB object to use * * \return Standard Pacemaker return code */ static int set_ticket_state_attr(pcmk__output_t *out, const char *ticket_id, const char *attr_name, bool attr_value, cib_t *cib) { int rc = pcmk_rc_ok; xmlNode *xml_top = NULL; xmlNode *ticket_state_xml = NULL; // Check for an existing ticket state entry rc = pcmk__get_ticket_state(cib, ticket_id, &ticket_state_xml); if (rc == pcmk_rc_duplicate_id) { out->err(out, "Multiple " PCMK__XE_TICKET_STATE "s match ticket_id=%s", ticket_id); rc = pcmk_rc_ok; } if (rc == pcmk_rc_ok) { // Ticket state found, use it pcmk__debug("Injecting attribute into existing ticket state %s", ticket_id); xml_top = ticket_state_xml; } else if (rc == ENXIO) { // No ticket state, create it xmlNode *xml_obj = NULL; xml_top = pcmk__xe_create(NULL, PCMK_XE_STATUS); xml_obj = pcmk__xe_create(xml_top, PCMK_XE_TICKETS); ticket_state_xml = pcmk__xe_create(xml_obj, PCMK__XE_TICKET_STATE); pcmk__xe_set(ticket_state_xml, PCMK_XA_ID, ticket_id); } else { // Error return rc; } // Add the attribute to the ticket state pcmk__xe_set_bool_attr(ticket_state_xml, attr_name, attr_value); - crm_log_xml_debug(xml_top, "Update"); + pcmk__log_xml_debug(xml_top, "Update"); // Commit the change to the CIB rc = cib->cmds->modify(cib, PCMK_XE_STATUS, xml_top, cib_sync_call); rc = pcmk_legacy2rc(rc); pcmk__xml_free(xml_top); return rc; } /*! * \internal * \brief Inject a fictitious action into the cluster * * \param[in,out] out Output object for displaying error messages * \param[in] spec Action specification to inject * \param[in,out] cib CIB object for scheduler input * \param[in] scheduler Scheduler data */ static void inject_action(pcmk__output_t *out, const char *spec, cib_t *cib, const pcmk_scheduler_t *scheduler) { int rc; int outcome = PCMK_OCF_OK; guint interval_ms = 0; char *key = NULL; char *node = NULL; char *task = NULL; char *resource = NULL; const char *rtype = NULL; const char *rclass = NULL; const char *rprovider = NULL; xmlNode *cib_op = NULL; xmlNode *cib_node = NULL; xmlNode *cib_resource = NULL; const pcmk_resource_t *rsc = NULL; lrmd_event_data_t *op = NULL; bool infinity = false; out->message(out, "inject-spec", spec); key = pcmk__assert_alloc(1, strlen(spec) + 1); node = pcmk__assert_alloc(1, strlen(spec) + 1); rc = sscanf(spec, "%[^@]@%[^=]=%d", key, node, &outcome); if (rc != 3) { out->err(out, "Invalid operation spec: %s. Only found %d fields", spec, rc); goto done; } parse_op_key(key, &resource, &task, &interval_ms); rsc = pe_find_resource(scheduler->priv->resources, resource); if (rsc == NULL) { out->err(out, "Invalid resource name: %s", resource); goto done; } rclass = pcmk__xe_get(rsc->priv->xml, PCMK_XA_CLASS); rtype = pcmk__xe_get(rsc->priv->xml, PCMK_XA_TYPE); rprovider = pcmk__xe_get(rsc->priv->xml, PCMK_XA_PROVIDER); cib_node = pcmk__inject_node(cib, node, NULL); pcmk__assert(cib_node != NULL); if (pcmk__str_eq(task, PCMK_ACTION_STOP, pcmk__str_none)) { infinity = true; } else if (pcmk__str_eq(task, PCMK_ACTION_START, pcmk__str_none) && pcmk__is_set(scheduler->flags, pcmk__sched_start_failure_fatal)) { infinity = true; } pcmk__inject_failcount(out, cib, cib_node, resource, task, interval_ms, outcome, infinity); cib_resource = pcmk__inject_resource_history(out, cib_node, resource, resource, rclass, rtype, rprovider); pcmk__assert(cib_resource != NULL); op = create_op(cib_resource, task, interval_ms, outcome); pcmk__assert(op != NULL); cib_op = pcmk__inject_action_result(cib_resource, op, node, 0); pcmk__assert(cib_op != NULL); lrmd_free_event(op); rc = cib->cmds->modify(cib, PCMK_XE_STATUS, cib_node, cib_sync_call); pcmk__assert(rc == pcmk_ok); done: free(task); free(node); free(key); } /*! * \internal * \brief Inject fictitious scheduler inputs * * \param[in,out] scheduler Scheduler data * \param[in,out] cib CIB object for scheduler input to modify * \param[in] injections Injections to apply */ void pcmk__inject_scheduler_input(pcmk_scheduler_t *scheduler, cib_t *cib, const pcmk_injections_t *injections) { int rc = pcmk_ok; const GList *iter = NULL; xmlNode *cib_node = NULL; pcmk__output_t *out = scheduler->priv->out; out->message(out, "inject-modify-config", injections->quorum, injections->watchdog); if (injections->quorum != NULL) { xmlNode *top = pcmk__xe_create(NULL, PCMK_XE_CIB); /* pcmk__xe_set(top, PCMK_XA_DC_UUID, dc_uuid); */ pcmk__xe_set(top, PCMK_XA_HAVE_QUORUM, injections->quorum); rc = cib->cmds->modify(cib, NULL, top, cib_sync_call); pcmk__assert(rc == pcmk_ok); } if (injections->watchdog != NULL) { rc = cib__update_node_attr(out, cib, cib_sync_call, PCMK_XE_CRM_CONFIG, NULL, NULL, NULL, NULL, PCMK_OPT_HAVE_WATCHDOG, injections->watchdog, NULL, NULL); pcmk__assert(rc == pcmk_rc_ok); } for (iter = injections->node_up; iter != NULL; iter = iter->next) { const char *node = (const char *) iter->data; out->message(out, "inject-modify-node", "Online", node); cib_node = pcmk__inject_node_state_change(cib, node, true); pcmk__assert(cib_node != NULL); rc = cib->cmds->modify(cib, PCMK_XE_STATUS, cib_node, cib_sync_call); pcmk__assert(rc == pcmk_ok); pcmk__xml_free(cib_node); } for (iter = injections->node_down; iter != NULL; iter = iter->next) { const char *node = (const char *) iter->data; char *xpath = NULL; out->message(out, "inject-modify-node", "Offline", node); cib_node = pcmk__inject_node_state_change(cib, node, false); pcmk__assert(cib_node != NULL); rc = cib->cmds->modify(cib, PCMK_XE_STATUS, cib_node, cib_sync_call); pcmk__assert(rc == pcmk_ok); pcmk__xml_free(cib_node); xpath = pcmk__assert_asprintf("//" PCMK__XE_NODE_STATE "[@" PCMK_XA_UNAME "='%s']" "/" PCMK__XE_LRM, node); cib->cmds->remove(cib, xpath, NULL, cib_xpath|cib_sync_call); free(xpath); xpath = pcmk__assert_asprintf("//" PCMK__XE_NODE_STATE "[@" PCMK_XA_UNAME "='%s']" "/" PCMK__XE_TRANSIENT_ATTRIBUTES, node); cib->cmds->remove(cib, xpath, NULL, cib_xpath|cib_sync_call); free(xpath); } for (iter = injections->node_fail; iter != NULL; iter = iter->next) { const char *node = (const char *) iter->data; out->message(out, "inject-modify-node", "Failing", node); cib_node = pcmk__inject_node_state_change(cib, node, true); pcmk__xe_set(cib_node, PCMK__XA_IN_CCM, PCMK_VALUE_FALSE); pcmk__assert(cib_node != NULL); rc = cib->cmds->modify(cib, PCMK_XE_STATUS, cib_node, cib_sync_call); pcmk__assert(rc == pcmk_ok); pcmk__xml_free(cib_node); } for (iter = injections->ticket_grant; iter != NULL; iter = iter->next) { const char *ticket_id = (const char *) iter->data; out->message(out, "inject-modify-ticket", "Granting", ticket_id); rc = set_ticket_state_attr(out, ticket_id, PCMK__XA_GRANTED, true, cib); pcmk__assert(rc == pcmk_rc_ok); } for (iter = injections->ticket_revoke; iter != NULL; iter = iter->next) { const char *ticket_id = (const char *) iter->data; out->message(out, "inject-modify-ticket", "Revoking", ticket_id); rc = set_ticket_state_attr(out, ticket_id, PCMK__XA_GRANTED, false, cib); pcmk__assert(rc == pcmk_rc_ok); } for (iter = injections->ticket_standby; iter != NULL; iter = iter->next) { const char *ticket_id = (const char *) iter->data; out->message(out, "inject-modify-ticket", "Standby", ticket_id); rc = set_ticket_state_attr(out, ticket_id, PCMK_XA_STANDBY, true, cib); pcmk__assert(rc == pcmk_rc_ok); } for (iter = injections->ticket_activate; iter != NULL; iter = iter->next) { const char *ticket_id = (const char *) iter->data; out->message(out, "inject-modify-ticket", "Activating", ticket_id); rc = set_ticket_state_attr(out, ticket_id, PCMK_XA_STANDBY, false, cib); pcmk__assert(rc == pcmk_rc_ok); } for (iter = injections->op_inject; iter != NULL; iter = iter->next) { inject_action(out, (const char *) iter->data, cib, scheduler); } if (!out->is_quiet(out)) { out->end_list(out); } } void pcmk_free_injections(pcmk_injections_t *injections) { if (injections == NULL) { return; } g_list_free_full(injections->node_up, g_free); g_list_free_full(injections->node_down, g_free); g_list_free_full(injections->node_fail, g_free); g_list_free_full(injections->op_fail, g_free); g_list_free_full(injections->op_inject, g_free); g_list_free_full(injections->ticket_grant, g_free); g_list_free_full(injections->ticket_revoke, g_free); g_list_free_full(injections->ticket_standby, g_free); g_list_free_full(injections->ticket_activate, g_free); free(injections->quorum); free(injections->watchdog); free(injections); } diff --git a/lib/pacemaker/pcmk_sched_actions.c b/lib/pacemaker/pcmk_sched_actions.c index b6fae5186c..39ef6d68ea 100644 --- a/lib/pacemaker/pcmk_sched_actions.c +++ b/lib/pacemaker/pcmk_sched_actions.c @@ -1,1968 +1,1969 @@ /* * 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 // bool, true, false #include #include #include #include // xmlNode #include #include // crm_meta_value() #include #include #include "libpacemaker_private.h" /*! * \internal * \brief Get the action flags relevant to ordering constraints * * \param[in,out] action Action to check * \param[in] node Node that *other* action in the ordering is on * (used only for clone resource actions) * * \return Action flags that should be used for orderings */ static uint32_t action_flags_for_ordering(pcmk_action_t *action, const pcmk_node_t *node) { bool runnable = false; uint32_t flags; // For non-resource actions, return the action flags if (action->rsc == NULL) { return action->flags; } /* For non-clone resources, or a clone action not assigned to a node, * return the flags as determined by the resource method without a node * specified. */ flags = action->rsc->priv->cmds->action_flags(action, NULL); if ((node == NULL) || !pcmk__is_clone(action->rsc)) { return flags; } /* Otherwise (i.e., for clone resource actions on a specific node), first * remember whether the non-node-specific action is runnable. */ runnable = pcmk__is_set(flags, pcmk__action_runnable); // Then recheck the resource method with the node flags = action->rsc->priv->cmds->action_flags(action, node); /* For clones in ordering constraints, the node-specific "runnable" doesn't * matter, just the non-node-specific setting (i.e., is the action runnable * anywhere). * * This applies only to runnable, and only for ordering constraints. This * function shouldn't be used for other types of constraints without * changes. Not very satisfying, but it's logical and appears to work well. */ if (runnable && !pcmk__is_set(flags, pcmk__action_runnable)) { pcmk__set_raw_action_flags(flags, action->rsc->id, pcmk__action_runnable); } return flags; } /*! * \internal * \brief Get action UUID that should be used with a resource ordering * * When an action is ordered relative to an action for a collective resource * (clone, group, or bundle), it actually needs to be ordered after all * instances of the collective have completed the relevant action (for example, * given "start CLONE then start RSC", RSC must wait until all instances of * CLONE have started). Given the UUID and resource of the first action in an * ordering, this returns the UUID of the action that should actually be used * for ordering (for example, "CLONE_started_0" instead of "CLONE_start_0"). * * \param[in] first_uuid UUID of first action in ordering * \param[in] first_rsc Resource of first action in ordering * * \return Newly allocated copy of UUID to use with ordering * \note It is the caller's responsibility to free the return value. */ static char * action_uuid_for_ordering(const char *first_uuid, const pcmk_resource_t *first_rsc) { guint interval_ms = 0; char *uuid = NULL; char *rid = NULL; char *first_task_str = NULL; enum pcmk__action_type first_task = pcmk__action_unspecified; enum pcmk__action_type remapped_task = pcmk__action_unspecified; // Only non-notify actions for collective resources need remapping if ((strstr(first_uuid, PCMK_ACTION_NOTIFY) != NULL) || (first_rsc->priv->variant < pcmk__rsc_variant_group)) { goto done; } // Only non-recurring actions need remapping pcmk__assert(parse_op_key(first_uuid, &rid, &first_task_str, &interval_ms)); if (interval_ms > 0) { goto done; } first_task = pcmk__parse_action(first_task_str); switch (first_task) { case pcmk__action_stop: case pcmk__action_start: case pcmk__action_notify: case pcmk__action_promote: case pcmk__action_demote: remapped_task = first_task + 1; break; case pcmk__action_stopped: case pcmk__action_started: case pcmk__action_notified: case pcmk__action_promoted: case pcmk__action_demoted: remapped_task = first_task; break; case pcmk__action_monitor: case pcmk__action_shutdown: case pcmk__action_fence: break; default: pcmk__err("Unknown action '%s' in ordering", first_task_str); break; } if (remapped_task != pcmk__action_unspecified) { /* If a clone or bundle has notifications enabled, the ordering will be * relative to when notifications have been sent for the remapped task. */ if (pcmk__is_set(first_rsc->flags, pcmk__rsc_notify) && (pcmk__is_clone(first_rsc) || pcmk__is_bundled(first_rsc))) { uuid = pcmk__notify_key(rid, "confirmed-post", pcmk__action_text(remapped_task)); } else { uuid = pcmk__op_key(rid, pcmk__action_text(remapped_task), 0); } pcmk__rsc_trace(first_rsc, "Remapped action UUID %s to %s for ordering purposes", first_uuid, uuid); } done: free(first_task_str); free(rid); return (uuid != NULL)? uuid : pcmk__str_copy(first_uuid); } /*! * \internal * \brief Get actual action that should be used with an ordering * * When an action is ordered relative to an action for a collective resource * (clone, group, or bundle), it actually needs to be ordered after all * instances of the collective have completed the relevant action (for example, * given "start CLONE then start RSC", RSC must wait until all instances of * CLONE have started). Given the first action in an ordering, this returns the * the action that should actually be used for ordering (for example, the * started action instead of the start action). * * \param[in] action First action in an ordering * * \return Actual action that should be used for the ordering */ static pcmk_action_t * action_for_ordering(pcmk_action_t *action) { pcmk_action_t *result = action; pcmk_resource_t *rsc = action->rsc; if (rsc == NULL) { return result; } if ((rsc->priv->variant >= pcmk__rsc_variant_group) && (action->uuid != NULL)) { char *uuid = action_uuid_for_ordering(action->uuid, rsc); result = find_first_action(rsc->priv->actions, uuid, NULL, NULL); if (result == NULL) { pcmk__warn("Not remapping %s to %s because %s does not have " "remapped action", action->uuid, uuid, rsc->id); result = action; } free(uuid); } return result; } /*! * \internal * \brief Wrapper for update_ordered_actions() method for readability * * \param[in,out] rsc Resource to call method for * \param[in,out] first 'First' action in an ordering * \param[in,out] then 'Then' action in an ordering * \param[in] node If not NULL, limit scope of ordering to this * node (only used when interleaving instances) * \param[in] flags Action flags for \p first for ordering purposes * \param[in] filter Action flags to limit scope of certain updates * (may include pcmk__action_optional to affect only * mandatory actions, and pe_action_runnable to * affect only runnable actions) * \param[in] type Group of enum pcmk__action_relation_flags to apply * \param[in,out] scheduler Scheduler data * * \return Group of enum pcmk__updated flags indicating what was updated */ static inline uint32_t update(pcmk_resource_t *rsc, pcmk_action_t *first, pcmk_action_t *then, const pcmk_node_t *node, uint32_t flags, uint32_t filter, uint32_t type, pcmk_scheduler_t *scheduler) { return rsc->priv->cmds->update_ordered_actions(first, then, node, flags, filter, type, scheduler); } /*! * \internal * \brief Update flags for ordering's actions appropriately for ordering's flags * * \param[in,out] first First action in an ordering * \param[in,out] then Then action in an ordering * \param[in] first_flags Action flags for \p first for ordering purposes * \param[in] then_flags Action flags for \p then for ordering purposes * \param[in,out] order Action wrapper for \p first in ordering * \param[in,out] scheduler Scheduler data * * \return Group of enum pcmk__updated flags */ static uint32_t update_action_for_ordering_flags(pcmk_action_t *first, pcmk_action_t *then, uint32_t first_flags, uint32_t then_flags, pcmk__related_action_t *order, pcmk_scheduler_t *scheduler) { uint32_t changed = pcmk__updated_none; /* The node will only be used for clones. If interleaved, node will be NULL, * otherwise the ordering scope will be limited to the node. Normally, the * whole 'then' clone should restart if 'first' is restarted, so then->node * is needed. */ pcmk_node_t *node = then->node; if (pcmk__is_set(order->flags, pcmk__ar_first_implies_same_node_then)) { /* For unfencing, only instances of 'then' on the same node as 'first' * (the unfencing operation) should restart, so reset node to * first->node, at which point this case is handled like a normal * pcmk__ar_first_implies_then. */ pcmk__clear_relation_flags(order->flags, pcmk__ar_first_implies_same_node_then); pcmk__set_relation_flags(order->flags, pcmk__ar_first_implies_then); node = first->node; pcmk__rsc_trace(then->rsc, "%s then %s: mapped " "pcmk__ar_first_implies_same_node_then to " "pcmk__ar_first_implies_then on %s", first->uuid, then->uuid, pcmk__node_name(node)); } if (pcmk__is_set(order->flags, pcmk__ar_first_implies_then)) { if (then->rsc != NULL) { changed |= update(then->rsc, first, then, node, first_flags & pcmk__action_optional, pcmk__action_optional, pcmk__ar_first_implies_then, scheduler); } else if (!pcmk__is_set(first_flags, pcmk__action_optional) && pcmk__is_set(then->flags, pcmk__action_optional)) { pcmk__clear_action_flags(then, pcmk__action_optional); pcmk__set_updated_flags(changed, first, pcmk__updated_then); } pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_first_implies_then", first->uuid, then->uuid, (changed? "changed" : "unchanged")); } if (pcmk__is_set(order->flags, pcmk__ar_intermediate_stop) && (then->rsc != NULL)) { enum pcmk__action_flags restart = pcmk__action_optional |pcmk__action_runnable; changed |= update(then->rsc, first, then, node, first_flags, restart, pcmk__ar_intermediate_stop, scheduler); pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_intermediate_stop", first->uuid, then->uuid, (changed? "changed" : "unchanged")); } if (pcmk__is_set(order->flags, pcmk__ar_then_implies_first)) { if (first->rsc != NULL) { changed |= update(first->rsc, first, then, node, first_flags, pcmk__action_optional, pcmk__ar_then_implies_first, scheduler); } else if (!pcmk__is_set(first_flags, pcmk__action_optional) && pcmk__is_set(first->flags, pcmk__action_runnable)) { pcmk__clear_action_flags(first, pcmk__action_runnable); pcmk__set_updated_flags(changed, first, pcmk__updated_first); } pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_then_implies_first", first->uuid, then->uuid, (changed? "changed" : "unchanged")); } if (pcmk__is_set(order->flags, pcmk__ar_promoted_then_implies_first)) { if (then->rsc != NULL) { changed |= update(then->rsc, first, then, node, first_flags & pcmk__action_optional, pcmk__action_optional, pcmk__ar_promoted_then_implies_first, scheduler); } pcmk__rsc_trace(then->rsc, "%s then %s: %s after " "pcmk__ar_promoted_then_implies_first", first->uuid, then->uuid, (changed? "changed" : "unchanged")); } if (pcmk__is_set(order->flags, pcmk__ar_min_runnable)) { if (then->rsc != NULL) { changed |= update(then->rsc, first, then, node, first_flags, pcmk__action_runnable, pcmk__ar_min_runnable, scheduler); } else if (pcmk__is_set(first_flags, pcmk__action_runnable)) { // We have another runnable instance of "first" then->runnable_before++; /* Mark "then" as runnable if it requires a certain number of * "before" instances to be runnable, and they now are. */ if ((then->runnable_before >= then->required_runnable_before) && !pcmk__is_set(then->flags, pcmk__action_runnable)) { pcmk__set_action_flags(then, pcmk__action_runnable); pcmk__set_updated_flags(changed, first, pcmk__updated_then); } } pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_min_runnable", first->uuid, then->uuid, (changed? "changed" : "unchanged")); } if (pcmk__is_set(order->flags, pcmk__ar_nested_remote_probe) && (then->rsc != NULL)) { if (!pcmk__is_set(first_flags, pcmk__action_runnable) && (first->rsc != NULL) && (first->rsc->priv->active_nodes != NULL)) { pcmk__rsc_trace(then->rsc, "%s then %s: ignoring because first is stopping", first->uuid, then->uuid); order->flags = pcmk__ar_none; } else { changed |= update(then->rsc, first, then, node, first_flags, pcmk__action_runnable, pcmk__ar_unrunnable_first_blocks, scheduler); } pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_nested_remote_probe", first->uuid, then->uuid, (changed? "changed" : "unchanged")); } if (pcmk__is_set(order->flags, pcmk__ar_unrunnable_first_blocks)) { if (then->rsc != NULL) { changed |= update(then->rsc, first, then, node, first_flags, pcmk__action_runnable, pcmk__ar_unrunnable_first_blocks, scheduler); } else if (!pcmk__is_set(first_flags, pcmk__action_runnable) && pcmk__is_set(then->flags, pcmk__action_runnable)) { pcmk__clear_action_flags(then, pcmk__action_runnable); pcmk__set_updated_flags(changed, first, pcmk__updated_then); } pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_unrunnable_first_blocks", first->uuid, then->uuid, (changed? "changed" : "unchanged")); } if (pcmk__is_set(order->flags, pcmk__ar_unmigratable_then_blocks)) { if (then->rsc != NULL) { changed |= update(then->rsc, first, then, node, first_flags, pcmk__action_optional, pcmk__ar_unmigratable_then_blocks, scheduler); } pcmk__rsc_trace(then->rsc, "%s then %s: %s after " "pcmk__ar_unmigratable_then_blocks", first->uuid, then->uuid, (changed? "changed" : "unchanged")); } if (pcmk__is_set(order->flags, pcmk__ar_first_else_then)) { if (then->rsc != NULL) { changed |= update(then->rsc, first, then, node, first_flags, pcmk__action_optional, pcmk__ar_first_else_then, scheduler); } pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_first_else_then", first->uuid, then->uuid, (changed? "changed" : "unchanged")); } if (pcmk__is_set(order->flags, pcmk__ar_ordered)) { if (then->rsc != NULL) { changed |= update(then->rsc, first, then, node, first_flags, pcmk__action_runnable, pcmk__ar_ordered, scheduler); } pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_ordered", first->uuid, then->uuid, (changed? "changed" : "unchanged")); } if (pcmk__is_set(order->flags, pcmk__ar_asymmetric)) { if (then->rsc != NULL) { changed |= update(then->rsc, first, then, node, first_flags, pcmk__action_runnable, pcmk__ar_asymmetric, scheduler); } pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_asymmetric", first->uuid, then->uuid, (changed? "changed" : "unchanged")); } if (pcmk__is_set(first->flags, pcmk__action_runnable) && pcmk__is_set(order->flags, pcmk__ar_first_implies_then_graphed) && !pcmk__is_set(first_flags, pcmk__action_optional)) { pcmk__rsc_trace(then->rsc, "%s will be in graph because %s is required", then->uuid, first->uuid); pcmk__set_action_flags(then, pcmk__action_always_in_graph); // Don't bother marking 'then' as changed just for this } if (pcmk__is_set(order->flags, pcmk__ar_then_implies_first_graphed) && !pcmk__is_set(then_flags, pcmk__action_optional)) { pcmk__rsc_trace(then->rsc, "%s will be in graph because %s is required", first->uuid, then->uuid); pcmk__set_action_flags(first, pcmk__action_always_in_graph); // Don't bother marking 'first' as changed just for this } if (pcmk__any_flags_set(order->flags, pcmk__ar_first_implies_then |pcmk__ar_then_implies_first |pcmk__ar_intermediate_stop) && (first->rsc != NULL) && !pcmk__is_set(first->rsc->flags, pcmk__rsc_managed) && pcmk__is_set(first->rsc->flags, pcmk__rsc_blocked) && !pcmk__is_set(first->flags, pcmk__action_runnable) && pcmk__str_eq(first->task, PCMK_ACTION_STOP, pcmk__str_none)) { /* @TODO This seems odd; why wouldn't an unrunnable "first" already * block "then" before this? Note that the unmanaged-stop-{1,2} * scheduler regression tests and the test CIB for T209 have tests for * "stop then stop" relations that would be good for checking any * changes. */ if (pcmk__is_set(then->flags, pcmk__action_runnable)) { pcmk__clear_action_flags(then, pcmk__action_runnable); pcmk__set_updated_flags(changed, first, pcmk__updated_then); } pcmk__rsc_trace(then->rsc, "%s then %s: %s after checking whether first " "is blocked, unmanaged, unrunnable stop", first->uuid, then->uuid, (changed? "changed" : "unchanged")); } return changed; } // Convenience macros for logging action properties #define action_type_str(flags) \ (pcmk__is_set((flags), pcmk__action_pseudo)? "pseudo-action" : "action") #define action_optional_str(flags) \ (pcmk__is_set((flags), pcmk__action_optional)? "optional" : "required") #define action_runnable_str(flags) \ (pcmk__is_set((flags), pcmk__action_runnable)? "runnable" : "unrunnable") #define action_node_str(a) \ (((a)->node == NULL)? "no node" : (a)->node->priv->name) /*! * \internal * \brief Update an action's flags for all orderings where it is "then" * * \param[in,out] then Action to update * \param[in,out] scheduler Scheduler data */ void pcmk__update_action_for_orderings(pcmk_action_t *then, pcmk_scheduler_t *scheduler) { GList *lpc = NULL; uint32_t changed = pcmk__updated_none; int last_flags = then->flags; pcmk__rsc_trace(then->rsc, "Updating %s %s (%s %s) on %s", action_type_str(then->flags), then->uuid, action_optional_str(then->flags), action_runnable_str(then->flags), action_node_str(then)); if (then->required_runnable_before > 0) { /* Initialize current known "runnable before" actions. As * update_action_for_ordering_flags() is called for each of then's * before actions, this number will increment as runnable 'first' * actions are encountered. */ then->runnable_before = 0; /* The pcmk__ar_min_runnable clause of * update_action_for_ordering_flags() (called below) * will reset runnable if appropriate. */ pcmk__clear_action_flags(then, pcmk__action_runnable); } for (lpc = then->actions_before; lpc != NULL; lpc = lpc->next) { pcmk__related_action_t *other = lpc->data; pcmk_action_t *first = other->action; pcmk_node_t *then_node = then->node; pcmk_node_t *first_node = first->node; const uint32_t target = pcmk__rsc_node_assigned; if ((first->rsc != NULL) && pcmk__is_group(first->rsc) && pcmk__str_eq(first->task, PCMK_ACTION_START, pcmk__str_none)) { first_node = first->rsc->priv->fns->location(first->rsc, NULL, target); if (first_node != NULL) { pcmk__rsc_trace(first->rsc, "Found %s for 'first' %s", pcmk__node_name(first_node), first->uuid); } } if (pcmk__is_group(then->rsc) && pcmk__str_eq(then->task, PCMK_ACTION_START, pcmk__str_none)) { then_node = then->rsc->priv->fns->location(then->rsc, NULL, target); if (then_node != NULL) { pcmk__rsc_trace(then->rsc, "Found %s for 'then' %s", pcmk__node_name(then_node), then->uuid); } } // Disable constraint if it only applies when on same node, but isn't if (pcmk__is_set(other->flags, pcmk__ar_if_on_same_node) && (first_node != NULL) && (then_node != NULL) && !pcmk__same_node(first_node, then_node)) { pcmk__rsc_trace(then->rsc, "Disabled ordering %s on %s then %s on %s: " "not same node", other->action->uuid, pcmk__node_name(first_node), then->uuid, pcmk__node_name(then_node)); other->flags = pcmk__ar_none; continue; } pcmk__clear_updated_flags(changed, then, pcmk__updated_first); if ((first->rsc != NULL) && pcmk__is_set(other->flags, pcmk__ar_then_cancels_first) && !pcmk__is_set(then->flags, pcmk__action_optional)) { /* 'then' is required, so we must abandon 'first' * (e.g. a required stop cancels any agent reload). */ pcmk__set_action_flags(other->action, pcmk__action_optional); if (!strcmp(first->task, PCMK_ACTION_RELOAD_AGENT)) { pcmk__clear_rsc_flags(first->rsc, pcmk__rsc_reload); } } if ((first->rsc != NULL) && (then->rsc != NULL) && (first->rsc != then->rsc) && !is_parent(then->rsc, first->rsc)) { first = action_for_ordering(first); } if (first != other->action) { pcmk__rsc_trace(then->rsc, "Ordering %s after %s instead of %s", then->uuid, first->uuid, other->action->uuid); } pcmk__rsc_trace(then->rsc, "%s (%#.6x) then %s (%#.6x): type=%#.6x node=%s", first->uuid, first->flags, then->uuid, then->flags, other->flags, action_node_str(first)); if (first == other->action) { /* 'first' was not remapped (e.g. from 'start' to 'running'), which * could mean it is a non-resource action, a primitive resource * action, or already expanded. */ uint32_t first_flags, then_flags; first_flags = action_flags_for_ordering(first, then_node); then_flags = action_flags_for_ordering(then, first_node); changed |= update_action_for_ordering_flags(first, then, first_flags, then_flags, other, scheduler); /* 'first' was for a complex resource (clone, group, etc), * create a new dependency if necessary */ } else if (order_actions(first, then, other->flags)) { /* This was the first time 'first' and 'then' were associated, * start again to get the new actions_before list */ pcmk__set_updated_flags(changed, then, pcmk__updated_then); pcmk__rsc_trace(then->rsc, "Disabled ordering %s then %s in favor of %s " "then %s", other->action->uuid, then->uuid, first->uuid, then->uuid); other->flags = pcmk__ar_none; } if (pcmk__is_set(changed, pcmk__updated_first)) { pcmk__trace("Re-processing %s and its 'after' actions " "because it changed", first->uuid); for (GList *lpc2 = first->actions_after; lpc2 != NULL; lpc2 = lpc2->next) { pcmk__related_action_t *other = lpc2->data; pcmk__update_action_for_orderings(other->action, scheduler); } pcmk__update_action_for_orderings(first, scheduler); } } if (then->required_runnable_before > 0) { if (last_flags == then->flags) { pcmk__clear_updated_flags(changed, then, pcmk__updated_then); } else { pcmk__set_updated_flags(changed, then, pcmk__updated_then); } } if (pcmk__is_set(changed, pcmk__updated_then)) { pcmk__trace("Re-processing %s and its 'after' actions because it " "changed", then->uuid); if (pcmk__is_set(last_flags, pcmk__action_runnable) && !pcmk__is_set(then->flags, pcmk__action_runnable)) { pcmk__block_colocation_dependents(then); } pcmk__update_action_for_orderings(then, scheduler); for (lpc = then->actions_after; lpc != NULL; lpc = lpc->next) { pcmk__related_action_t *other = lpc->data; pcmk__update_action_for_orderings(other->action, scheduler); } } } static inline bool is_primitive_action(const pcmk_action_t *action) { return (action != NULL) && pcmk__is_primitive(action->rsc); } /*! * \internal * \brief Clear a single action flag and set reason text * * \param[in,out] action Action whose flag should be cleared * \param[in] flag Action flag that should be cleared * \param[in] reason Action that is the reason why flag is being cleared */ #define clear_action_flag_because(action, flag, reason) do { \ if (pcmk__is_set((action)->flags, (flag))) { \ pcmk__clear_action_flags(action, flag); \ if ((action)->rsc != (reason)->rsc) { \ char *reason_text = pe__action2reason((reason), (flag)); \ pe_action_set_reason((action), reason_text, false); \ free(reason_text); \ } \ } \ } while (0) /*! * \internal * \brief Update actions in an asymmetric ordering * * If the "first" action in an asymmetric ordering is unrunnable, make the * "second" action unrunnable as well, if appropriate. * * \param[in] first 'First' action in an asymmetric ordering * \param[in,out] then 'Then' action in an asymmetric ordering */ static void handle_asymmetric_ordering(const pcmk_action_t *first, pcmk_action_t *then) { /* Only resource actions after an unrunnable 'first' action need updates for * asymmetric ordering. */ if ((then->rsc == NULL) || pcmk__is_set(first->flags, pcmk__action_runnable)) { return; } // Certain optional 'then' actions are unaffected by unrunnable 'first' if (pcmk__is_set(then->flags, pcmk__action_optional)) { enum rsc_role_e then_rsc_role; then_rsc_role = then->rsc->priv->fns->state(then->rsc, true); if ((then_rsc_role == pcmk_role_stopped) && pcmk__str_eq(then->task, PCMK_ACTION_STOP, pcmk__str_none)) { /* If 'then' should stop after 'first' but is already stopped, the * ordering is irrelevant. */ return; } else if ((then_rsc_role >= pcmk_role_started) && pcmk__str_eq(then->task, PCMK_ACTION_START, pcmk__str_none) && pe__rsc_running_on_only(then->rsc, then->node)) { /* Similarly if 'then' should start after 'first' but is already * started on a single node. */ return; } } // 'First' can't run, so 'then' can't either clear_action_flag_because(then, pcmk__action_optional, first); clear_action_flag_because(then, pcmk__action_runnable, first); } /*! * \internal * \brief Set action bits appropriately when pcmk__ar_intermediate_stop is used * * \param[in,out] first 'First' action in ordering * \param[in,out] then 'Then' action in ordering * \param[in] filter What action flags to care about * * \note pcmk__ar_intermediate_stop is set for "stop resource before starting * it" and "stop later group member before stopping earlier group member" */ static void handle_restart_ordering(pcmk_action_t *first, pcmk_action_t *then, uint32_t filter) { const char *reason = NULL; pcmk__assert(is_primitive_action(first) && is_primitive_action(then)); // We need to update the action in two cases: // ... if 'then' is required if (pcmk__is_set(filter, pcmk__action_optional) && !pcmk__is_set(then->flags, pcmk__action_optional)) { reason = "restart"; } /* ... if 'then' is unrunnable action on same resource (if a resource * should restart but can't start, we still want to stop) */ if (pcmk__is_set(filter, pcmk__action_runnable) && !pcmk__is_set(then->flags, pcmk__action_runnable) && pcmk__is_set(then->rsc->flags, pcmk__rsc_managed) && (first->rsc == then->rsc)) { reason = "stop"; } if (reason == NULL) { return; } pcmk__rsc_trace(first->rsc, "Handling %s -> %s for %s", first->uuid, then->uuid, reason); // Make 'first' required if it is runnable if (pcmk__is_set(first->flags, pcmk__action_runnable)) { clear_action_flag_because(first, pcmk__action_optional, then); } // Make 'first' required if 'then' is required if (!pcmk__is_set(then->flags, pcmk__action_optional)) { clear_action_flag_because(first, pcmk__action_optional, then); } // Make 'first' unmigratable if 'then' is unmigratable if (!pcmk__is_set(then->flags, pcmk__action_migratable)) { clear_action_flag_because(first, pcmk__action_migratable, then); } // Make 'then' unrunnable if 'first' is required but unrunnable if (!pcmk__is_set(first->flags, pcmk__action_optional) && !pcmk__is_set(first->flags, pcmk__action_runnable)) { clear_action_flag_because(then, pcmk__action_runnable, first); } } /*! * \internal * \brief Update two actions according to an ordering between them * * Given information about an ordering of two actions, update the actions' flags * (and runnable_before members if appropriate) as appropriate for the ordering. * Effects may cascade to other orderings involving the actions as well. * * \param[in,out] first 'First' action in an ordering * \param[in,out] then 'Then' action in an ordering * \param[in] node If not NULL, limit scope of ordering to this node * (ignored) * \param[in] flags Action flags for \p first for ordering purposes * \param[in] filter Action flags to limit scope of certain updates (may * include pcmk__action_optional to affect only * mandatory actions, and pcmk__action_runnable to * affect only runnable actions) * \param[in] type Group of enum pcmk__action_relation_flags to apply * \param[in,out] scheduler Scheduler data * * \return Group of enum pcmk__updated flags indicating what was updated */ uint32_t pcmk__update_ordered_actions(pcmk_action_t *first, pcmk_action_t *then, const pcmk_node_t *node, uint32_t flags, uint32_t filter, uint32_t type, pcmk_scheduler_t *scheduler) { uint32_t changed = pcmk__updated_none; uint32_t then_flags = 0U; uint32_t first_flags = 0U; pcmk__assert((first != NULL) && (then != NULL) && (scheduler != NULL)); then_flags = then->flags; first_flags = first->flags; if (pcmk__is_set(type, pcmk__ar_asymmetric)) { handle_asymmetric_ordering(first, then); } if (pcmk__is_set(type, pcmk__ar_then_implies_first) && !pcmk__is_set(then_flags, pcmk__action_optional)) { // Then is required, and implies first should be, too if (pcmk__is_set(filter, pcmk__action_optional) && !pcmk__is_set(flags, pcmk__action_optional) && pcmk__is_set(first_flags, pcmk__action_optional)) { clear_action_flag_because(first, pcmk__action_optional, then); } if (pcmk__is_set(flags, pcmk__action_migratable) && !pcmk__is_set(then->flags, pcmk__action_migratable)) { clear_action_flag_because(first, pcmk__action_migratable, then); } } if (pcmk__is_set(type, pcmk__ar_promoted_then_implies_first) && (then->rsc != NULL) && (then->rsc->priv->orig_role == pcmk_role_promoted) && pcmk__is_set(filter, pcmk__action_optional) && !pcmk__is_set(then->flags, pcmk__action_optional)) { clear_action_flag_because(first, pcmk__action_optional, then); if (pcmk__is_set(first->flags, pcmk__action_migratable) && !pcmk__is_set(then->flags, pcmk__action_migratable)) { clear_action_flag_because(first, pcmk__action_migratable, then); } } if (pcmk__is_set(type, pcmk__ar_unmigratable_then_blocks) && pcmk__is_set(filter, pcmk__action_optional)) { if (!pcmk__all_flags_set(then->flags, pcmk__action_migratable |pcmk__action_runnable)) { clear_action_flag_because(first, pcmk__action_runnable, then); } if (!pcmk__is_set(then->flags, pcmk__action_optional)) { clear_action_flag_because(first, pcmk__action_optional, then); } } if (pcmk__is_set(type, pcmk__ar_first_else_then) && pcmk__is_set(filter, pcmk__action_optional) && !pcmk__is_set(first->flags, pcmk__action_runnable)) { clear_action_flag_because(then, pcmk__action_migratable, first); pcmk__clear_action_flags(then, pcmk__action_pseudo); } if (pcmk__is_set(type, pcmk__ar_unrunnable_first_blocks) && pcmk__is_set(filter, pcmk__action_runnable) && pcmk__is_set(then->flags, pcmk__action_runnable) && !pcmk__is_set(flags, pcmk__action_runnable)) { clear_action_flag_because(then, pcmk__action_runnable, first); clear_action_flag_because(then, pcmk__action_migratable, first); } if (pcmk__is_set(type, pcmk__ar_first_implies_then) && pcmk__is_set(filter, pcmk__action_optional) && pcmk__is_set(then->flags, pcmk__action_optional) && !pcmk__is_set(flags, pcmk__action_optional) && !pcmk__is_set(first->flags, pcmk__action_migratable)) { clear_action_flag_because(then, pcmk__action_optional, first); } if (pcmk__is_set(type, pcmk__ar_intermediate_stop)) { handle_restart_ordering(first, then, filter); } if (then_flags != then->flags) { pcmk__set_updated_flags(changed, first, pcmk__updated_then); pcmk__rsc_trace(then->rsc, "%s on %s: flags are now %#.6x (was %#.6x) " "because of 'first' %s (%#.6x)", then->uuid, pcmk__node_name(then->node), then->flags, then_flags, first->uuid, first->flags); if ((then->rsc != NULL) && (then->rsc->priv->parent != NULL)) { // Required to handle "X_stop then X_start" for cloned groups pcmk__update_action_for_orderings(then, scheduler); } } if (first_flags != first->flags) { pcmk__set_updated_flags(changed, first, pcmk__updated_first); pcmk__rsc_trace(first->rsc, "%s on %s: flags are now %#.6x (was %#.6x) " "because of 'then' %s (%#.6x)", first->uuid, pcmk__node_name(first->node), first->flags, first_flags, then->uuid, then->flags); } return changed; } /*! * \internal * \brief Trace-log an action (optionally with its dependent actions) * * \param[in] pre_text If not NULL, prefix the log with this plus ": " * \param[in] action Action to log * \param[in] details If true, recursively log dependent actions */ void pcmk__log_action(const char *pre_text, const pcmk_action_t *action, bool details) { const char *node_uname = NULL; const char *node_uuid = NULL; const char *desc = NULL; CRM_CHECK(action != NULL, return); if (!pcmk__is_set(action->flags, pcmk__action_pseudo)) { if (action->node != NULL) { node_uname = action->node->priv->name; node_uuid = action->node->priv->id; } else { node_uname = ""; } } switch (pcmk__parse_action(action->task)) { case pcmk__action_fence: case pcmk__action_shutdown: if (pcmk__is_set(action->flags, pcmk__action_pseudo)) { desc = "Pseudo "; } else if (pcmk__is_set(action->flags, pcmk__action_optional)) { desc = "Optional "; } else if (!pcmk__is_set(action->flags, pcmk__action_runnable)) { desc = "!!Non-Startable!! "; } else { desc = "(Provisional) "; } pcmk__trace("%s%s%sAction %d: %s%s%s%s%s%s", pcmk__s(pre_text, ""), ((pre_text != NULL)? ": " : ""), desc, action->id, action->uuid, ((node_uname != NULL)? "\ton " : ""), pcmk__s(node_uname, ""), ((node_uuid != NULL)? "\t\t(" : ""), pcmk__s(node_uuid, ""), ((node_uuid != NULL)? ")" : "")); break; default: if (pcmk__is_set(action->flags, pcmk__action_optional)) { desc = "Optional "; } else if (pcmk__is_set(action->flags, pcmk__action_pseudo)) { desc = "Pseudo "; } else if (!pcmk__is_set(action->flags, pcmk__action_runnable)) { desc = "!!Non-Startable!! "; } else { desc = "(Provisional) "; } pcmk__trace("%s%s%sAction %d: %s %s%s%s%s%s%s", pcmk__s(pre_text, ""), ((pre_text != NULL)? ": " : ""), desc, action->id, action->uuid, ((action->rsc != NULL)? action->rsc->id : ""), ((node_uname != NULL)? "\ton " : ""), pcmk__s(node_uname, ""), ((node_uuid != NULL)? "\t\t(" : ""), pcmk__s(node_uuid, ""), ((node_uuid != NULL)? ")" : "")); break; } if (details) { const GList *iter = NULL; const pcmk__related_action_t *other = NULL; pcmk__trace("\t\t====== Preceding Actions"); for (iter = action->actions_before; iter != NULL; iter = iter->next) { other = (const pcmk__related_action_t *) iter->data; pcmk__log_action("\t\t", other->action, false); } pcmk__trace("\t\t====== Subsequent Actions"); for (iter = action->actions_after; iter != NULL; iter = iter->next) { other = (const pcmk__related_action_t *) iter->data; pcmk__log_action("\t\t", other->action, false); } pcmk__trace("\t\t====== End"); } else { pcmk__trace("\t\t(before=%u, after=%u)", g_list_length(action->actions_before), g_list_length(action->actions_after)); } } /*! * \internal * \brief Create a new shutdown action for a node * * \param[in,out] node Node being shut down * * \return Newly created shutdown action for \p node */ pcmk_action_t * pcmk__new_shutdown_action(pcmk_node_t *node) { char *shutdown_id = NULL; pcmk_action_t *shutdown_op = NULL; pcmk__assert(node != NULL); shutdown_id = pcmk__assert_asprintf("%s-%s", PCMK_ACTION_DO_SHUTDOWN, node->priv->name); shutdown_op = custom_action(NULL, shutdown_id, PCMK_ACTION_DO_SHUTDOWN, node, FALSE, node->priv->scheduler); pcmk__order_stops_before_shutdown(node, shutdown_op); pcmk__insert_meta(shutdown_op, PCMK__META_OP_NO_WAIT, PCMK_VALUE_TRUE); return shutdown_op; } /*! * \internal * \brief Calculate and add an operation digest to XML * * Calculate an operation digest, which enables us to later determine when a * restart is needed due to the resource's parameters being changed, and add it * to given XML. * * \param[in] op Operation result from executor * \param[in,out] update XML to add digest to */ static void add_op_digest_to_xml(const lrmd_event_data_t *op, xmlNode *update) { char *digest = NULL; xmlNode *args_xml = NULL; if (op->params == NULL) { return; } args_xml = pcmk__xe_create(NULL, PCMK_XE_PARAMETERS); g_hash_table_foreach(op->params, hash2field, args_xml); pcmk__filter_op_for_digest(args_xml); digest = pcmk__digest_operation(args_xml); pcmk__xe_set(update, PCMK__XA_OP_DIGEST, digest); pcmk__xml_free(args_xml); free(digest); } #define FAKE_TE_ID "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" /*! * \internal * \brief Create XML for resource operation history update * * \param[in,out] parent Parent XML node to add to * \param[in,out] op Operation event data * \param[in] caller_version DC feature set * \param[in] target_rc Expected result of operation * \param[in] node Name of node on which operation was performed * \param[in] origin Arbitrary description of update source * * \return Newly created XML node for history update */ xmlNode * pcmk__create_history_xml(xmlNode *parent, lrmd_event_data_t *op, const char *caller_version, int target_rc, const char *node, const char *origin) { char *key = NULL; char *magic = NULL; char *op_id = NULL; char *op_id_additional = NULL; char *local_user_data = NULL; const char *exit_reason = NULL; xmlNode *xml_op = NULL; const char *task = NULL; CRM_CHECK(op != NULL, return NULL); pcmk__trace("Creating history XML for %s-interval %s action for %s on %s " "(DC version: %s, origin: %s)", pcmk__readable_interval(op->interval_ms), op->op_type, op->rsc_id, pcmk__s(node, "no node"), caller_version, origin); task = op->op_type; /* Record a successful agent reload as a start, and a failed one as a * monitor, to make life easier for the scheduler when determining the * current state. * * @COMPAT We should check "reload" here only if the operation was for a * pre-OCF-1.1 resource agent, but we don't know that here, and we should * only ever get results for actions scheduled by us, so we can reasonably * assume any "reload" is actually a pre-1.1 agent reload. * * @TODO This remapping can make log messages with task confusing for users * (for example, an "Initiating reload ..." followed by "... start ... * confirmed"). Either do this remapping in the scheduler if possible, or * store the original task in a new XML attribute for later logging. */ if (pcmk__str_any_of(task, PCMK_ACTION_RELOAD, PCMK_ACTION_RELOAD_AGENT, NULL)) { if (op->op_status == PCMK_EXEC_DONE) { task = PCMK_ACTION_START; } else { task = PCMK_ACTION_MONITOR; } } key = pcmk__op_key(op->rsc_id, task, op->interval_ms); if (pcmk__str_eq(task, PCMK_ACTION_NOTIFY, pcmk__str_none)) { const char *n_type = crm_meta_value(op->params, "notify_type"); const char *n_task = crm_meta_value(op->params, "notify_operation"); CRM_LOG_ASSERT(n_type != NULL); CRM_LOG_ASSERT(n_task != NULL); op_id = pcmk__notify_key(op->rsc_id, n_type, n_task); if (op->op_status != PCMK_EXEC_PENDING) { /* Ignore notify errors. * * @TODO It might be better to keep the correct result here, and * ignore it in process_graph_event(). */ lrmd__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); } /* Migration history is preserved separately, which usually matters for * multiple nodes and is important for future cluster transitions. */ } else if (pcmk__str_any_of(op->op_type, PCMK_ACTION_MIGRATE_TO, PCMK_ACTION_MIGRATE_FROM, NULL)) { op_id = strdup(key); } else if (did_rsc_op_fail(op, target_rc)) { op_id = pcmk__op_key(op->rsc_id, "last_failure", 0); if (op->interval_ms == 0) { /* Ensure 'last' gets updated, in case PCMK_META_RECORD_PENDING is * true */ op_id_additional = pcmk__op_key(op->rsc_id, "last", 0); } exit_reason = op->exit_reason; } else if (op->interval_ms > 0) { op_id = strdup(key); } else { op_id = pcmk__op_key(op->rsc_id, "last", 0); } again: xml_op = pcmk__xe_first_child(parent, PCMK__XE_LRM_RSC_OP, PCMK_XA_ID, op_id); if (xml_op == NULL) { xml_op = pcmk__xe_create(parent, PCMK__XE_LRM_RSC_OP); } if (op->user_data == NULL) { pcmk__debug("Generating fake transition key for: " PCMK__OP_FMT " %d from %s", op->rsc_id, op->op_type, op->interval_ms, op->call_id, origin); local_user_data = pcmk__transition_key(-1, op->call_id, target_rc, FAKE_TE_ID); op->user_data = local_user_data; } if (magic == NULL) { magic = pcmk__assert_asprintf("%d:%d;%s", op->op_status, op->rc, (const char *) op->user_data); } pcmk__xe_set(xml_op, PCMK_XA_ID, op_id); pcmk__xe_set(xml_op, PCMK__XA_OPERATION_KEY, key); pcmk__xe_set(xml_op, PCMK_XA_OPERATION, task); pcmk__xe_set(xml_op, PCMK_XA_CRM_DEBUG_ORIGIN, origin); pcmk__xe_set(xml_op, PCMK_XA_CRM_FEATURE_SET, caller_version); pcmk__xe_set(xml_op, PCMK__XA_TRANSITION_KEY, op->user_data); pcmk__xe_set(xml_op, PCMK__XA_TRANSITION_MAGIC, magic); pcmk__xe_set(xml_op, PCMK_XA_EXIT_REASON, pcmk__s(exit_reason, "")); pcmk__xe_set(xml_op, PCMK__META_ON_NODE, node); // For context during triage pcmk__xe_set_int(xml_op, PCMK__XA_CALL_ID, op->call_id); pcmk__xe_set_int(xml_op, PCMK__XA_RC_CODE, op->rc); pcmk__xe_set_int(xml_op, PCMK__XA_OP_STATUS, op->op_status); pcmk__xe_set_guint(xml_op, PCMK_META_INTERVAL, op->interval_ms); if ((op->t_run > 0) || (op->t_rcchange > 0) || (op->exec_time > 0) || (op->queue_time > 0)) { pcmk__trace("Timing data (" PCMK__OP_FMT "): " "last=%lld change=%lld exec=%u queue=%u", op->rsc_id, op->op_type, op->interval_ms, (long long) op->t_run, (long long) op->t_rcchange, op->exec_time, op->queue_time); if ((op->interval_ms > 0) && (op->t_rcchange > 0)) { // Recurring ops may have changed rc after initial run pcmk__xe_set_time(xml_op, PCMK_XA_LAST_RC_CHANGE, op->t_rcchange); } else { pcmk__xe_set_time(xml_op, PCMK_XA_LAST_RC_CHANGE, op->t_run); } pcmk__xe_set_int(xml_op, PCMK_XA_EXEC_TIME, op->exec_time); pcmk__xe_set_int(xml_op, PCMK_XA_QUEUE_TIME, op->queue_time); } if (pcmk__str_any_of(op->op_type, PCMK_ACTION_MIGRATE_TO, PCMK_ACTION_MIGRATE_FROM, NULL)) { /* Record PCMK__META_MIGRATE_SOURCE and PCMK__META_MIGRATE_TARGET always * for migrate ops. */ const char *name = PCMK__META_MIGRATE_SOURCE; pcmk__xe_set(xml_op, name, crm_meta_value(op->params, name)); name = PCMK__META_MIGRATE_TARGET; pcmk__xe_set(xml_op, name, crm_meta_value(op->params, name)); } add_op_digest_to_xml(op, xml_op); if (op_id_additional) { free(op_id); op_id = op_id_additional; op_id_additional = NULL; goto again; } if (local_user_data) { free(local_user_data); op->user_data = NULL; } free(magic); free(op_id); free(key); return xml_op; } /*! * \internal * \brief Check whether an action shutdown-locks a resource to a node * * If the PCMK_OPT_SHUTDOWN_LOCK cluster property is set, resources will not be * recovered on a different node if cleanly stopped, and may start only on that * same node. This function checks whether that applies to a given action, so * that the transition graph can be marked appropriately. * * \param[in] action Action to check * * \return true if \p action locks its resource to the action's node, * otherwise false */ bool pcmk__action_locks_rsc_to_node(const pcmk_action_t *action) { // Only resource actions taking place on resource's lock node are locked if ((action == NULL) || (action->rsc == NULL) || !pcmk__same_node(action->node, action->rsc->priv->lock_node)) { return false; } /* During shutdown, only stops are locked (otherwise, another action such as * a demote would cause the controller to clear the lock) */ if (action->node->details->shutdown && (action->task != NULL) && (strcmp(action->task, PCMK_ACTION_STOP) != 0)) { return false; } return true; } /* lowest to highest */ static gint sort_action_id(gconstpointer a, gconstpointer b) { const pcmk__related_action_t *action_wrapper2 = a; const pcmk__related_action_t *action_wrapper1 = b; if (a == NULL) { return 1; } if (b == NULL) { return -1; } if (action_wrapper1->action->id < action_wrapper2->action->id) { return 1; } if (action_wrapper1->action->id > action_wrapper2->action->id) { return -1; } return 0; } /*! * \internal * \brief Remove any duplicate action inputs, merging action flags * * \param[in,out] action Action whose inputs should be checked */ void pcmk__deduplicate_action_inputs(pcmk_action_t *action) { GList *item = NULL; GList *next = NULL; pcmk__related_action_t *last_input = NULL; action->actions_before = g_list_sort(action->actions_before, sort_action_id); for (item = action->actions_before; item != NULL; item = next) { pcmk__related_action_t *input = item->data; next = item->next; if ((last_input != NULL) && (input->action->id == last_input->action->id)) { pcmk__trace("Input %s (%d) duplicate skipped for action %s (%d)", input->action->uuid, input->action->id, action->uuid, action->id); /* For the purposes of scheduling, the ordering flags no longer * matter, but crm_simulate looks at certain ones when creating a * dot graph. Combining the flags is sufficient for that purpose. */ pcmk__set_relation_flags(last_input->flags, input->flags); if (input->graphed) { last_input->graphed = true; } free(item->data); action->actions_before = g_list_delete_link(action->actions_before, item); } else { last_input = input; input->graphed = false; } } } /*! * \internal * \brief Output all scheduled actions * * \param[in,out] scheduler Scheduler data */ void pcmk__output_actions(pcmk_scheduler_t *scheduler) { pcmk__output_t *out = scheduler->priv->out; // Output node (non-resource) actions for (GList *iter = scheduler->priv->actions; iter != NULL; iter = iter->next) { char *node_name = NULL; char *task = NULL; pcmk_action_t *action = (pcmk_action_t *) iter->data; if (action->rsc != NULL) { continue; // Resource actions will be output later } else if (pcmk__is_set(action->flags, pcmk__action_optional)) { continue; // This action was not scheduled } if (pcmk__str_eq(action->task, PCMK_ACTION_DO_SHUTDOWN, pcmk__str_none)) { task = strdup("Shutdown"); } else if (pcmk__str_eq(action->task, PCMK_ACTION_STONITH, pcmk__str_none)) { const char *op = g_hash_table_lookup(action->meta, PCMK__META_STONITH_ACTION); task = pcmk__assert_asprintf("Fence (%s)", op); } else { continue; // Don't display other node action types } if (pcmk__is_guest_or_bundle_node(action->node)) { const pcmk_resource_t *remote = action->node->priv->remote; node_name = pcmk__assert_asprintf("%s (resource: %s)", pcmk__node_name(action->node), remote->priv->launcher->id); } else if (action->node != NULL) { node_name = pcmk__assert_asprintf("%s", pcmk__node_name(action->node)); } out->message(out, "node-action", task, node_name, action->reason); free(node_name); free(task); } // Output resource actions for (GList *iter = scheduler->priv->resources; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; rsc->priv->cmds->output_actions(rsc); } } /*! * \internal * \brief Get action name needed to compare digest for configuration changes * * \param[in] task Action name from history * \param[in] interval_ms Action interval (in milliseconds) * * \return Action name whose digest should be compared */ static const char * task_for_digest(const char *task, guint interval_ms) { /* Certain actions need to be compared against the parameters used to start * the resource. */ if ((interval_ms == 0) && pcmk__str_any_of(task, PCMK_ACTION_MONITOR, PCMK_ACTION_MIGRATE_FROM, PCMK_ACTION_PROMOTE, NULL)) { task = PCMK_ACTION_START; } return task; } /*! * \internal * \brief Check whether only sanitized parameters to an action changed * * When collecting CIB files for troubleshooting, crm_report will mask * sensitive resource parameters. If simulations were run using that, affected * resources would appear to need a restart, which would complicate * troubleshooting. To avoid that, we save a "secure digest" of non-sensitive * parameters. This function used that digest to check whether only masked * parameters are different. * * \param[in] xml_op Resource history entry with secure digest * \param[in] digest_data Operation digest information being compared * \param[in] scheduler Scheduler data * * \return true if only sanitized parameters changed, otherwise false */ static bool only_sanitized_changed(const xmlNode *xml_op, const pcmk__op_digest_t *digest_data, const pcmk_scheduler_t *scheduler) { const char *digest_secure = NULL; if (!pcmk__is_set(scheduler->flags, pcmk__sched_sanitized)) { // The scheduler is not being run as a simulation return false; } digest_secure = pcmk__xe_get(xml_op, PCMK__XA_OP_SECURE_DIGEST); return (digest_data->rc != pcmk__digest_match) && (digest_secure != NULL) && (digest_data->digest_secure_calc != NULL) && (strcmp(digest_data->digest_secure_calc, digest_secure) == 0); } /*! * \internal * \brief Force a restart due to a configuration change * * \param[in,out] rsc Resource that action is for * \param[in] task Name of action whose configuration changed * \param[in] interval_ms Action interval (in milliseconds) * \param[in,out] node Node where resource should be restarted */ static void force_restart(pcmk_resource_t *rsc, const char *task, guint interval_ms, pcmk_node_t *node) { char *key = pcmk__op_key(rsc->id, task, interval_ms); pcmk_action_t *required = custom_action(rsc, key, task, NULL, FALSE, rsc->priv->scheduler); pe_action_set_reason(required, "resource definition change", true); trigger_unfencing(rsc, node, "Device parameters changed", NULL, rsc->priv->scheduler); } /*! * \internal * \brief Schedule a reload of a resource on a node * * \param[in,out] data Resource to reload * \param[in] user_data Where resource should be reloaded */ static void schedule_reload(gpointer data, gpointer user_data) { pcmk_resource_t *rsc = data; const pcmk_node_t *node = user_data; pcmk_action_t *reload = NULL; // For collective resources, just call recursively for children if (rsc->priv->variant > pcmk__rsc_variant_primitive) { g_list_foreach(rsc->priv->children, schedule_reload, user_data); return; } // Skip the reload in certain situations if ((node == NULL) || !pcmk__is_set(rsc->flags, pcmk__rsc_managed) || pcmk__is_set(rsc->flags, pcmk__rsc_failed)) { const bool managed = pcmk__is_set(rsc->flags, pcmk__rsc_managed); const bool failed = pcmk__is_set(rsc->flags, pcmk__rsc_failed); pcmk__rsc_trace(rsc, "Skip reload of %s:%s%s %s", rsc->id, (managed? "" : " unmanaged"), (failed? " failed" : ""), (node == NULL)? "inactive" : node->priv->name); return; } /* If a resource's configuration changed while a start was pending, * force a full restart instead of a reload. */ if (pcmk__is_set(rsc->flags, pcmk__rsc_start_pending)) { pcmk__rsc_trace(rsc, "%s: preventing agent reload because start pending", rsc->id); custom_action(rsc, stop_key(rsc), PCMK_ACTION_STOP, node, FALSE, rsc->priv->scheduler); return; } // Schedule the reload pcmk__set_rsc_flags(rsc, pcmk__rsc_reload); reload = custom_action(rsc, reload_key(rsc), PCMK_ACTION_RELOAD_AGENT, node, FALSE, rsc->priv->scheduler); pe_action_set_reason(reload, "resource definition change", FALSE); // Set orderings so that a required stop or demote cancels the reload pcmk__new_ordering(NULL, NULL, reload, rsc, stop_key(rsc), NULL, pcmk__ar_ordered|pcmk__ar_then_cancels_first, rsc->priv->scheduler); pcmk__new_ordering(NULL, NULL, reload, rsc, demote_key(rsc), NULL, pcmk__ar_ordered|pcmk__ar_then_cancels_first, rsc->priv->scheduler); } /*! * \internal * \brief Handle any configuration change for an action * * Given an action from resource history, if the resource's configuration * changed since the action was done, schedule any actions needed (restart, * reload, unfencing, rescheduling recurring actions, etc.). * * \param[in,out] rsc Resource that action is for * \param[in,out] node Node that action was on * \param[in] xml_op Action XML from resource history * * \return true if action configuration changed, otherwise false */ bool pcmk__check_action_config(pcmk_resource_t *rsc, pcmk_node_t *node, const xmlNode *xml_op) { guint interval_ms = 0; const char *task = NULL; const pcmk__op_digest_t *digest_data = NULL; CRM_CHECK((rsc != NULL) && (node != NULL) && (xml_op != NULL), return false); task = pcmk__xe_get(xml_op, PCMK_XA_OPERATION); CRM_CHECK(task != NULL, return false); pcmk__xe_get_guint(xml_op, PCMK_META_INTERVAL, &interval_ms); // If this is a recurring action, check whether it has been orphaned if (interval_ms > 0) { if (pcmk__find_action_config(rsc, task, interval_ms, false) != NULL) { pcmk__rsc_trace(rsc, "%s-interval %s for %s on %s is in configuration", pcmk__readable_interval(interval_ms), task, rsc->id, pcmk__node_name(node)); } else if (pcmk__is_set(rsc->priv->scheduler->flags, pcmk__sched_cancel_removed_actions)) { pcmk__schedule_cancel(rsc, pcmk__xe_get(xml_op, PCMK__XA_CALL_ID), task, interval_ms, node, "orphan"); return true; } else { pcmk__rsc_debug(rsc, "%s-interval %s for %s on %s is orphaned", pcmk__readable_interval(interval_ms), task, rsc->id, pcmk__node_name(node)); return true; } } pcmk__trace("Checking %s-interval %s for %s on %s for configuration " "changes", pcmk__readable_interval(interval_ms), task, rsc->id, pcmk__node_name(node)); task = task_for_digest(task, interval_ms); digest_data = rsc_action_digest_cmp(rsc, xml_op, node, rsc->priv->scheduler); if (only_sanitized_changed(xml_op, digest_data, rsc->priv->scheduler)) { if (!pcmk__is_daemon && (rsc->priv->scheduler->priv->out != NULL)) { pcmk__output_t *out = rsc->priv->scheduler->priv->out; out->info(out, "Only 'private' parameters to %s-interval %s for %s " "on %s changed: %s", pcmk__readable_interval(interval_ms), task, rsc->id, pcmk__node_name(node), pcmk__xe_get(xml_op, PCMK__XA_TRANSITION_MAGIC)); } return false; } switch (digest_data->rc) { case pcmk__digest_restart: - crm_log_xml_debug(digest_data->params_restart, "params:restart"); + pcmk__log_xml_debug(digest_data->params_restart, "params:restart"); force_restart(rsc, task, interval_ms, node); return true; case pcmk__digest_unknown: case pcmk__digest_mismatch: // Changes that can potentially be handled by an agent reload if (interval_ms > 0) { /* Recurring actions aren't reloaded per se, they are just * re-scheduled so the next run uses the new parameters. * The old instance will be cancelled automatically. */ - crm_log_xml_debug(digest_data->params_all, "params:reschedule"); + pcmk__log_xml_debug(digest_data->params_all, + "params:reschedule"); pcmk__reschedule_recurring(rsc, task, interval_ms, node); } else if (pcmk__xe_get(xml_op, PCMK__XA_OP_RESTART_DIGEST) != NULL) { // Agent supports reload, so use it trigger_unfencing(rsc, node, "Device parameters changed (reload)", NULL, rsc->priv->scheduler); - crm_log_xml_debug(digest_data->params_all, "params:reload"); + pcmk__log_xml_debug(digest_data->params_all, "params:reload"); schedule_reload((gpointer) rsc, (gpointer) node); } else { pcmk__rsc_trace(rsc, "Restarting %s " "because agent doesn't support reload", rsc->id); - crm_log_xml_debug(digest_data->params_restart, - "params:restart"); + pcmk__log_xml_debug(digest_data->params_restart, + "params:restart"); force_restart(rsc, task, interval_ms, node); } return true; default: break; } return false; } /*! * \internal * \brief Create a list of resource's action history entries, sorted by call ID * * \param[in] rsc_entry Resource's \c PCMK__XE_LRM_RSC_OP status XML * \param[out] start_index Where to store index of start-like action, if any * \param[out] stop_index Where to store index of stop action, if any */ static GList * rsc_history_as_list(const xmlNode *rsc_entry, int *start_index, int *stop_index) { GList *ops = NULL; for (xmlNode *rsc_op = pcmk__xe_first_child(rsc_entry, PCMK__XE_LRM_RSC_OP, NULL, NULL); rsc_op != NULL; rsc_op = pcmk__xe_next(rsc_op, PCMK__XE_LRM_RSC_OP)) { ops = g_list_prepend(ops, rsc_op); } ops = g_list_sort(ops, sort_op_by_callid); calculate_active_ops(ops, start_index, stop_index); return ops; } /*! * \internal * \brief Process a resource's action history from the CIB status * * Given a resource's action history, if the resource's configuration * changed since the actions were done, schedule any actions needed (restart, * reload, unfencing, rescheduling recurring actions, clean-up, etc.). * (This also cancels recurring actions for maintenance mode, which is not * entirely related but convenient to do here.) * * \param[in] rsc_entry Resource's \c PCMK__XE_LRM_RSC_OP status XML * \param[in,out] rsc Resource whose history is being processed * \param[in,out] node Node whose history is being processed */ static void process_rsc_history(const xmlNode *rsc_entry, pcmk_resource_t *rsc, pcmk_node_t *node) { int offset = -1; int stop_index = 0; int start_index = 0; GList *sorted_op_list = NULL; if (pcmk__is_set(rsc->flags, pcmk__rsc_removed)) { if (pcmk__is_anonymous_clone(pe__const_top_resource(rsc, false))) { /* @TODO Should this be done for bundled primitives as well? Added * by 2ac43ae31 */ pcmk__rsc_trace(rsc, "Skipping configuration check " "for orphaned clone instance %s", rsc->id); } else { pcmk__rsc_trace(rsc, "Skipping configuration check and scheduling " "clean-up for orphaned resource %s", rsc->id); pcmk__schedule_cleanup(rsc, node, false); } return; } if (pe_find_node_id(rsc->priv->active_nodes, node->priv->id) == NULL) { if (pcmk__rsc_agent_changed(rsc, node, rsc_entry, false)) { pcmk__schedule_cleanup(rsc, node, false); } pcmk__rsc_trace(rsc, "Skipping configuration check for %s " "because no longer active on %s", rsc->id, pcmk__node_name(node)); return; } pcmk__rsc_trace(rsc, "Checking for configuration changes for %s on %s", rsc->id, pcmk__node_name(node)); if (pcmk__rsc_agent_changed(rsc, node, rsc_entry, true)) { pcmk__schedule_cleanup(rsc, node, false); } sorted_op_list = rsc_history_as_list(rsc_entry, &start_index, &stop_index); if (start_index < stop_index) { return; // Resource is stopped } for (GList *iter = sorted_op_list; iter != NULL; iter = iter->next) { xmlNode *rsc_op = (xmlNode *) iter->data; const char *task = NULL; guint interval_ms = 0; if (++offset < start_index) { // Skip actions that happened before a start continue; } task = pcmk__xe_get(rsc_op, PCMK_XA_OPERATION); pcmk__xe_get_guint(rsc_op, PCMK_META_INTERVAL, &interval_ms); if ((interval_ms > 0) && (pcmk__is_set(rsc->flags, pcmk__rsc_maintenance) || node->details->maintenance)) { // Maintenance mode cancels recurring operations pcmk__schedule_cancel(rsc, pcmk__xe_get(rsc_op, PCMK__XA_CALL_ID), task, interval_ms, node, "maintenance mode"); } else if ((interval_ms > 0) || pcmk__strcase_any_of(task, PCMK_ACTION_MONITOR, PCMK_ACTION_START, PCMK_ACTION_PROMOTE, PCMK_ACTION_MIGRATE_FROM, NULL)) { /* If a resource operation failed, and the operation's definition * has changed, clear any fail count so they can be retried fresh. */ if (pe__bundle_needs_remote_name(rsc)) { /* We haven't assigned resources to nodes yet, so if the * REMOTE_CONTAINER_HACK is used, we may calculate the digest * based on the literal "#uname" value rather than the properly * substituted value. That would mistakenly make the action * definition appear to have been changed. Defer the check until * later in this case. */ pcmk__add_param_check(rsc_op, rsc, node, pcmk__check_active); } else if (pcmk__check_action_config(rsc, node, rsc_op) && (pe_get_failcount(node, rsc, NULL, pcmk__fc_effective, NULL) != 0)) { pe__clear_failcount(rsc, node, "action definition changed", rsc->priv->scheduler); } } } g_list_free(sorted_op_list); } /*! * \internal * \brief Process a node's action history from the CIB status * * Given a node's resource history, if the resource's configuration changed * since the actions were done, schedule any actions needed (restart, * reload, unfencing, rescheduling recurring actions, clean-up, etc.). * (This also cancels recurring actions for maintenance mode, which is not * entirely related but convenient to do here.) * * \param[in,out] node Node whose history is being processed * \param[in] lrm_rscs Node's \c PCMK__XE_LRM_RESOURCES from CIB status XML */ static void process_node_history(pcmk_node_t *node, const xmlNode *lrm_rscs) { pcmk__trace("Processing node history for %s", pcmk__node_name(node)); for (const xmlNode *rsc_entry = pcmk__xe_first_child(lrm_rscs, PCMK__XE_LRM_RESOURCE, NULL, NULL); rsc_entry != NULL; rsc_entry = pcmk__xe_next(rsc_entry, PCMK__XE_LRM_RESOURCE)) { if (rsc_entry->children != NULL) { GList *result = pcmk__rscs_matching_id(pcmk__xe_id(rsc_entry), node->priv->scheduler); for (GList *iter = result; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; if (pcmk__is_primitive(rsc)) { process_rsc_history(rsc_entry, rsc, node); } } g_list_free(result); } } } // XPath to find a node's resource history #define XPATH_NODE_HISTORY "/" PCMK_XE_CIB "/" PCMK_XE_STATUS \ "/" PCMK__XE_NODE_STATE \ "[@" PCMK_XA_UNAME "='%s']" \ "/" PCMK__XE_LRM "/" PCMK__XE_LRM_RESOURCES /*! * \internal * \brief Process any resource configuration changes in the CIB status * * Go through all nodes' resource history, and if a resource's configuration * changed since its actions were done, schedule any actions needed (restart, * reload, unfencing, rescheduling recurring actions, clean-up, etc.). * (This also cancels recurring actions for maintenance mode, which is not * entirely related but convenient to do here.) * * \param[in,out] scheduler Scheduler data */ void pcmk__handle_rsc_config_changes(pcmk_scheduler_t *scheduler) { pcmk__trace("Check resource and action configuration for changes"); /* Rather than iterate through the status section, iterate through the nodes * and search for the appropriate status subsection for each. This skips * orphaned nodes and lets us eliminate some cases before searching the XML. */ for (GList *iter = scheduler->nodes; iter != NULL; iter = iter->next) { pcmk_node_t *node = (pcmk_node_t *) iter->data; /* Don't bother checking actions for a node that can't run actions ... * unless it's in maintenance mode, in which case we still need to * cancel any existing recurring monitors. */ if (node->details->maintenance || pcmk__node_available(node, false, false)) { char *xpath = NULL; xmlNode *history = NULL; xpath = pcmk__assert_asprintf(XPATH_NODE_HISTORY, node->priv->name); history = pcmk__xpath_find_one(scheduler->input->doc, xpath, PCMK__LOG_NEVER); free(xpath); process_node_history(node, history); } } } diff --git a/lib/pacemaker/pcmk_ticket.c b/lib/pacemaker/pcmk_ticket.c index d4b26d6ab0..df49ff24b9 100644 --- a/lib/pacemaker/pcmk_ticket.c +++ b/lib/pacemaker/pcmk_ticket.c @@ -1,565 +1,565 @@ /* * Copyright 2024-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 "libpacemaker_private.h" static int build_ticket_modify_xml(cib_t *cib, const char *ticket_id, xmlNode **ticket_state_xml, xmlNode **xml_top) { int rc = pcmk__get_ticket_state(cib, ticket_id, ticket_state_xml); if (rc == pcmk_rc_ok || rc == pcmk_rc_duplicate_id) { /* Ticket(s) found - return their state */ *xml_top = *ticket_state_xml; } else if (rc == ENXIO) { /* No ticket found - build the XML needed to create it */ xmlNode *xml_obj = NULL; *xml_top = pcmk__xe_create(NULL, PCMK_XE_STATUS); xml_obj = pcmk__xe_create(*xml_top, PCMK_XE_TICKETS); *ticket_state_xml = pcmk__xe_create(xml_obj, PCMK__XE_TICKET_STATE); pcmk__xe_set(*ticket_state_xml, PCMK_XA_ID, ticket_id); rc = pcmk_rc_ok; } else { /* Some other error occurred - clean up and return */ pcmk__xml_free(*ticket_state_xml); } return rc; } static void add_attribute_xml(pcmk_scheduler_t *scheduler, const char *ticket_id, GHashTable *attr_set, xmlNode **ticket_state_xml) { GHashTableIter hash_iter; char *key = NULL; char *value = NULL; pcmk__ticket_t *ticket = NULL; ticket = g_hash_table_lookup(scheduler->priv->ticket_constraints, ticket_id); g_hash_table_iter_init(&hash_iter, attr_set); while (g_hash_table_iter_next(&hash_iter, (gpointer *) & key, (gpointer *) & value)) { pcmk__xe_set(*ticket_state_xml, key, value); if (pcmk__str_eq(key, PCMK__XA_GRANTED, pcmk__str_none) && ((ticket == NULL) || !pcmk__is_set(ticket->flags, pcmk__ticket_granted)) && pcmk__is_true(value)) { char *now = pcmk__ttoa(time(NULL)); pcmk__xe_set(*ticket_state_xml, PCMK_XA_LAST_GRANTED, now); free(now); } } } int pcmk__get_ticket_state(cib_t *cib, const char *ticket_id, xmlNode **state) { int rc = pcmk_rc_ok; xmlNode *xml_search = NULL; char *xpath = NULL; pcmk__assert((cib != NULL) && (state != NULL)); // cppcheck doesn't understand the above pcmk__assert line // cppcheck-suppress ctunullpointer *state = NULL; if (ticket_id != NULL) { xpath = pcmk__assert_asprintf("/" PCMK_XE_CIB "/" PCMK_XE_STATUS "/" PCMK_XE_TICKETS "/" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"%s\"]", ticket_id); } else { xpath = pcmk__assert_asprintf("/" PCMK_XE_CIB "/" PCMK_XE_STATUS "/" PCMK_XE_TICKETS); } rc = cib->cmds->query(cib, xpath, &xml_search, cib_sync_call|cib_xpath); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { - crm_log_xml_debug(xml_search, "Match"); + pcmk__log_xml_debug(xml_search, "Match"); if (xml_search->children != NULL && ticket_id != NULL) { rc = pcmk_rc_duplicate_id; } } free(xpath); *state = xml_search; return rc; } int pcmk__ticket_constraints(pcmk__output_t *out, cib_t *cib, const char *ticket_id) { int rc = pcmk_rc_ok; xmlNode *result = NULL; const char *xpath_base = NULL; char *xpath = NULL; pcmk__assert((out != NULL) && (cib != NULL)); xpath_base = pcmk_cib_xpath_for(PCMK_XE_CONSTRAINTS); pcmk__assert(xpath_base != NULL); if (ticket_id != NULL) { xpath = pcmk__assert_asprintf("%s/" PCMK_XE_RSC_TICKET "[@" PCMK_XA_TICKET "=\"%s\"]", xpath_base, ticket_id); } else { xpath = pcmk__assert_asprintf("%s/" PCMK_XE_RSC_TICKET, xpath_base); } rc = cib->cmds->query(cib, xpath, &result, cib_sync_call|cib_xpath); rc = pcmk_legacy2rc(rc); if (result != NULL) { out->message(out, "ticket-constraints", result); pcmk__xml_free(result); } free(xpath); return rc; } int pcmk_ticket_constraints(xmlNodePtr *xml, const char *ticket_id) { pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; cib_t *cib = NULL; rc = pcmk__setup_output_cib_sched(&out, &cib, NULL, xml); if (rc != pcmk_rc_ok) { goto done; } rc = pcmk__ticket_constraints(out, cib, ticket_id); done: if (cib != NULL) { cib__clean_up_connection(&cib); } pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); return rc; } static int delete_single_ticket(xmlNode *child, void *userdata) { int rc = pcmk_rc_ok; cib_t *cib = (cib_t *) userdata; rc = cib->cmds->remove(cib, PCMK_XE_STATUS, child, cib_sync_call); rc = pcmk_legacy2rc(rc); return rc; } int pcmk__ticket_delete(pcmk__output_t *out, cib_t *cib, pcmk_scheduler_t *scheduler, const char *ticket_id, bool force) { int rc = pcmk_rc_ok; xmlNode *state = NULL; pcmk__assert((cib != NULL) && (scheduler != NULL)); if (ticket_id == NULL) { return EINVAL; } if (!force) { pcmk__ticket_t *ticket = NULL; ticket = g_hash_table_lookup(scheduler->priv->ticket_constraints, ticket_id); if (ticket == NULL) { return ENXIO; } if (pcmk__is_set(ticket->flags, pcmk__ticket_granted)) { return EACCES; } } rc = pcmk__get_ticket_state(cib, ticket_id, &state); if (rc == pcmk_rc_duplicate_id) { out->info(out, "Multiple " PCMK__XE_TICKET_STATE "s match ticket=%s", ticket_id); } else if (rc == ENXIO) { return pcmk_rc_ok; } else if (rc != pcmk_rc_ok) { return rc; } - crm_log_xml_debug(state, "Delete"); + pcmk__log_xml_debug(state, "Delete"); if (rc == pcmk_rc_duplicate_id) { rc = pcmk__xe_foreach_child(state, NULL, delete_single_ticket, cib); } else { rc = delete_single_ticket(state, cib); } if (rc == pcmk_rc_ok) { out->info(out, "Cleaned up %s", ticket_id); } pcmk__xml_free(state); return rc; } int pcmk_ticket_delete(xmlNodePtr *xml, const char *ticket_id, bool force) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; cib_t *cib = NULL; int rc = pcmk_rc_ok; rc = pcmk__setup_output_cib_sched(&out, &cib, &scheduler, xml); if (rc != pcmk_rc_ok) { goto done; } rc = pcmk__ticket_delete(out, cib, scheduler, ticket_id, force); done: if (cib != NULL) { cib__clean_up_connection(&cib); } pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); pcmk_free_scheduler(scheduler); return rc; } int pcmk__ticket_get_attr(pcmk__output_t *out, pcmk_scheduler_t *scheduler, const char *ticket_id, const char *attr_name, const char *attr_default) { int rc = pcmk_rc_ok; const char *attr_value = NULL; pcmk__ticket_t *ticket = NULL; pcmk__assert((out != NULL) && (scheduler != NULL)); if (ticket_id == NULL || attr_name == NULL) { return EINVAL; } ticket = g_hash_table_lookup(scheduler->priv->ticket_constraints, ticket_id); if (ticket != NULL) { attr_value = g_hash_table_lookup(ticket->state, attr_name); } if (attr_value != NULL) { out->message(out, "ticket-attribute", ticket_id, attr_name, attr_value); } else if (attr_default != NULL) { out->message(out, "ticket-attribute", ticket_id, attr_name, attr_default); } else { rc = ENXIO; } return rc; } int pcmk_ticket_get_attr(xmlNodePtr *xml, const char *ticket_id, const char *attr_name, const char *attr_default) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; rc = pcmk__setup_output_cib_sched(&out, NULL, &scheduler, xml); if (rc != pcmk_rc_ok) { goto done; } rc = pcmk__ticket_get_attr(out, scheduler, ticket_id, attr_name, attr_default); done: pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); pcmk_free_scheduler(scheduler); return rc; } int pcmk__ticket_info(pcmk__output_t *out, pcmk_scheduler_t *scheduler, const char *ticket_id, bool details, bool raw) { int rc = pcmk_rc_ok; pcmk__assert((out != NULL) && (scheduler != NULL)); if (ticket_id != NULL) { GHashTable *tickets = NULL; pcmk__ticket_t *ticket = NULL; ticket = g_hash_table_lookup(scheduler->priv->ticket_constraints, ticket_id); if (ticket == NULL) { return ENXIO; } /* The ticket-list message expects a GHashTable, so we'll construct * one with just this single item. */ tickets = pcmk__strkey_table(free, NULL); g_hash_table_insert(tickets, strdup(ticket->id), ticket); out->message(out, "ticket-list", tickets, false, raw, details); g_hash_table_destroy(tickets); } else { out->message(out, "ticket-list", scheduler->priv->ticket_constraints, false, raw, details); } return rc; } int pcmk_ticket_info(xmlNodePtr *xml, const char *ticket_id) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; rc = pcmk__setup_output_cib_sched(&out, NULL, &scheduler, xml); if (rc != pcmk_rc_ok) { goto done; } pe__register_messages(out); /* XML output (which is the only format supported by public API functions * due to the use of pcmk__xml_output_new above) always prints all details, * so just pass false for the last two arguments. */ rc = pcmk__ticket_info(out, scheduler, ticket_id, false, false); done: pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); pcmk_free_scheduler(scheduler); return rc; } int pcmk__ticket_remove_attr(pcmk__output_t *out, cib_t *cib, pcmk_scheduler_t *scheduler, const char *ticket_id, GList *attr_delete, bool force) { xmlNode *ticket_state_xml = NULL; xmlNode *xml_top = NULL; int rc = pcmk_rc_ok; pcmk__assert((out != NULL) && (cib != NULL) && (scheduler != NULL)); if (ticket_id == NULL) { return EINVAL; } /* Nothing to do */ if (attr_delete == NULL) { return pcmk_rc_ok; } rc = build_ticket_modify_xml(cib, ticket_id, &ticket_state_xml, &xml_top); if (rc == pcmk_rc_duplicate_id) { out->info(out, "Multiple " PCMK__XE_TICKET_STATE "s match ticket=%s", ticket_id); } else if (rc != pcmk_rc_ok) { pcmk__xml_free(ticket_state_xml); return rc; } for (GList *list_iter = attr_delete; list_iter != NULL; list_iter = list_iter->next) { const char *key = list_iter->data; if (!force && pcmk__str_eq(key, PCMK__XA_GRANTED, pcmk__str_none)) { pcmk__xml_free(ticket_state_xml); return EACCES; } pcmk__xe_remove_attr(ticket_state_xml, key); } - crm_log_xml_debug(xml_top, "Replace"); + pcmk__log_xml_debug(xml_top, "Replace"); rc = cib->cmds->replace(cib, PCMK_XE_STATUS, ticket_state_xml, cib_sync_call); rc = pcmk_legacy2rc(rc); pcmk__xml_free(xml_top); return rc; } int pcmk_ticket_remove_attr(xmlNodePtr *xml, const char *ticket_id, GList *attr_delete, bool force) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; cib_t *cib = NULL; rc = pcmk__setup_output_cib_sched(&out, &cib, &scheduler, xml); if (rc != pcmk_rc_ok) { goto done; } rc = pcmk__ticket_remove_attr(out, cib, scheduler, ticket_id, attr_delete, force); done: if (cib != NULL) { cib__clean_up_connection(&cib); } pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); pcmk_free_scheduler(scheduler); return rc; } int pcmk__ticket_set_attr(pcmk__output_t *out, cib_t *cib, pcmk_scheduler_t *scheduler, const char *ticket_id, GHashTable *attr_set, bool force) { xmlNode *ticket_state_xml = NULL; xmlNode *xml_top = NULL; int rc = pcmk_rc_ok; pcmk__assert((out != NULL) && (cib != NULL) && (scheduler != NULL)); if (ticket_id == NULL) { return EINVAL; } /* Nothing to do */ if (attr_set == NULL || g_hash_table_size(attr_set) == 0) { return pcmk_rc_ok; } rc = build_ticket_modify_xml(cib, ticket_id, &ticket_state_xml, &xml_top); if (rc == pcmk_rc_duplicate_id) { out->info(out, "Multiple " PCMK__XE_TICKET_STATE "s match ticket=%s", ticket_id); } else if (rc != pcmk_rc_ok) { pcmk__xml_free(ticket_state_xml); return rc; } if (!force && g_hash_table_lookup(attr_set, PCMK__XA_GRANTED)) { pcmk__xml_free(ticket_state_xml); return EACCES; } add_attribute_xml(scheduler, ticket_id, attr_set, &ticket_state_xml); - crm_log_xml_debug(xml_top, "Update"); + pcmk__log_xml_debug(xml_top, "Update"); rc = cib->cmds->modify(cib, PCMK_XE_STATUS, xml_top, cib_sync_call); rc = pcmk_legacy2rc(rc); pcmk__xml_free(xml_top); return rc; } int pcmk_ticket_set_attr(xmlNodePtr *xml, const char *ticket_id, GHashTable *attr_set, bool force) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; cib_t *cib = NULL; rc = pcmk__setup_output_cib_sched(&out, &cib, &scheduler, xml); if (rc != pcmk_rc_ok) { goto done; } rc = pcmk__ticket_set_attr(out, cib, scheduler, ticket_id, attr_set, force); done: if (cib != NULL) { cib__clean_up_connection(&cib); } pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); pcmk_free_scheduler(scheduler); return rc; } int pcmk__ticket_state(pcmk__output_t *out, cib_t *cib, const char *ticket_id) { xmlNode *state_xml = NULL; int rc = pcmk_rc_ok; pcmk__assert((out != NULL) && (cib != NULL)); rc = pcmk__get_ticket_state(cib, ticket_id, &state_xml); if (rc == pcmk_rc_duplicate_id) { out->info(out, "Multiple " PCMK__XE_TICKET_STATE "s match ticket=%s", ticket_id); } if (state_xml != NULL) { out->message(out, "ticket-state", state_xml); pcmk__xml_free(state_xml); } return rc; } int pcmk_ticket_state(xmlNodePtr *xml, const char *ticket_id) { pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; cib_t *cib = NULL; rc = pcmk__setup_output_cib_sched(&out, &cib, NULL, xml); if (rc != pcmk_rc_ok) { goto done; } rc = pcmk__ticket_state(out, cib, ticket_id); done: if (cib != NULL) { cib__clean_up_connection(&cib); } pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); return rc; } diff --git a/lib/pengine/unpack.c b/lib/pengine/unpack.c index e6bf22f216..df88025555 100644 --- a/lib/pengine/unpack.c +++ b/lib/pengine/unpack.c @@ -1,5150 +1,5150 @@ /* * 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 // xmlNode #include // xmlXPathObject, etc. #include #include #include // PCMK_SCORE_INFINITY #include #include #include #include #include CRM_TRACE_INIT_DATA(pe_status); // A (parsed) resource action history entry struct action_history { pcmk_resource_t *rsc; // Resource that history is for pcmk_node_t *node; // Node that history is for xmlNode *xml; // History entry XML // Parsed from entry XML const char *id; // XML ID of history entry const char *key; // Operation key of action const char *task; // Action name const char *exit_reason; // Exit reason given for result guint interval_ms; // Action interval int call_id; // Call ID of action int expected_exit_status; // Expected exit status of action int exit_status; // Actual exit status of action int execution_status; // Execution status of action }; /* This uses pcmk__set_flags_as()/pcmk__clear_flags_as() directly rather than * use pcmk__set_scheduler_flags()/pcmk__clear_scheduler_flags() so that the * flag is stringified more readably in log messages. */ #define set_config_flag(scheduler, option, flag) do { \ GHashTable *config_hash = (scheduler)->priv->options; \ const char *scf_value = pcmk__cluster_option(config_hash, (option)); \ \ if (scf_value != NULL) { \ if (pcmk__is_true(scf_value)) { \ (scheduler)->flags = pcmk__set_flags_as(__func__, __LINE__, \ PCMK__LOG_TRACE, \ "Scheduler", \ crm_system_name, \ (scheduler)->flags, \ (flag), #flag); \ } else { \ (scheduler)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ PCMK__LOG_TRACE, \ "Scheduler", \ crm_system_name, \ (scheduler)->flags, \ (flag), #flag); \ } \ } \ } while(0) static void unpack_rsc_op(pcmk_resource_t *rsc, pcmk_node_t *node, xmlNode *xml_op, xmlNode **last_failure, enum pcmk__on_fail *failed); static void determine_remote_online_status(pcmk_scheduler_t *scheduler, pcmk_node_t *this_node); static void add_node_attrs(const xmlNode *xml_obj, pcmk_node_t *node, bool overwrite, pcmk_scheduler_t *scheduler); static void determine_online_status(const xmlNode *node_state, pcmk_node_t *this_node, pcmk_scheduler_t *scheduler); static void unpack_node_lrm(pcmk_node_t *node, const xmlNode *xml, pcmk_scheduler_t *scheduler); /*! * \internal * \brief Check whether a node is a dangling guest node * * \param[in] node Node to check * * \return true if \p node had a Pacemaker Remote connection resource with a * launcher that was removed from the CIB, otherwise false. */ static bool is_dangling_guest_node(pcmk_node_t *node) { return pcmk__is_pacemaker_remote_node(node) && (node->priv->remote != NULL) && (node->priv->remote->priv->launcher == NULL) && pcmk__is_set(node->priv->remote->flags, pcmk__rsc_removed_launched); } /*! * \brief Schedule a fence action for a node * * \param[in,out] scheduler Scheduler data * \param[in,out] node Node to fence * \param[in] reason Text description of why fencing is needed * \param[in] priority_delay Whether to consider * \c PCMK_OPT_PRIORITY_FENCING_DELAY */ void pe_fence_node(pcmk_scheduler_t *scheduler, pcmk_node_t *node, const char *reason, bool priority_delay) { CRM_CHECK(node, return); if (pcmk__is_guest_or_bundle_node(node)) { // Fence a guest or bundle node by marking its launcher as failed pcmk_resource_t *rsc = node->priv->remote->priv->launcher; if (!pcmk__is_set(rsc->flags, pcmk__rsc_failed)) { if (!pcmk__is_set(rsc->flags, pcmk__rsc_managed)) { pcmk__notice("Not fencing guest node %s (otherwise would " "because %s): its guest resource %s is unmanaged", pcmk__node_name(node), reason, rsc->id); } else { pcmk__sched_warn(scheduler, "Guest node %s will be fenced " "(by recovering its guest resource %s): %s", pcmk__node_name(node), rsc->id, reason); /* We don't mark the node as unclean because that would prevent the * node from running resources. We want to allow it to run resources * in this transition if the recovery succeeds. */ pcmk__set_node_flags(node, pcmk__node_remote_reset); pcmk__set_rsc_flags(rsc, pcmk__rsc_failed|pcmk__rsc_stop_if_failed); } } } else if (is_dangling_guest_node(node)) { pcmk__info("Cleaning up dangling connection for guest node %s: fencing " "was already done because %s, and guest resource no longer " "exists", pcmk__node_name(node), reason); pcmk__set_rsc_flags(node->priv->remote, pcmk__rsc_failed|pcmk__rsc_stop_if_failed); } else if (pcmk__is_remote_node(node)) { pcmk_resource_t *rsc = node->priv->remote; if ((rsc != NULL) && !pcmk__is_set(rsc->flags, pcmk__rsc_managed)) { pcmk__notice("Not fencing remote node %s (otherwise would because " "%s): connection is unmanaged", pcmk__node_name(node), reason); } else if (!pcmk__is_set(node->priv->flags, pcmk__node_remote_reset)) { pcmk__set_node_flags(node, pcmk__node_remote_reset); pcmk__sched_warn(scheduler, "Remote node %s %s: %s", pcmk__node_name(node), pe_can_fence(scheduler, node)? "will be fenced" : "is unclean", reason); } node->details->unclean = TRUE; // No need to apply PCMK_OPT_PRIORITY_FENCING_DELAY for remote nodes pe_fence_op(node, NULL, TRUE, reason, FALSE, scheduler); } else if (node->details->unclean) { const char *fenced_s = "also is unclean"; if (pe_can_fence(scheduler, node)) { fenced_s = "would also be fenced"; } pcmk__trace("Cluster node %s %s because %s", pcmk__node_name(node), fenced_s, reason); } else { pcmk__sched_warn(scheduler, "Cluster node %s %s: %s", pcmk__node_name(node), pe_can_fence(scheduler, node)? "will be fenced" : "is unclean", reason); node->details->unclean = TRUE; pe_fence_op(node, NULL, TRUE, reason, priority_delay, scheduler); } } // @TODO xpaths can't handle templates, rules, or id-refs // nvpair with provides or requires set to unfencing #define XPATH_UNFENCING_NVPAIR PCMK_XE_NVPAIR \ "[(@" PCMK_XA_NAME "='" PCMK_STONITH_PROVIDES "'" \ "or @" PCMK_XA_NAME "='" PCMK_META_REQUIRES "') " \ "and @" PCMK_XA_VALUE "='" PCMK_VALUE_UNFENCING "']" // unfencing in rsc_defaults or any resource #define XPATH_ENABLE_UNFENCING \ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RESOURCES \ "//" PCMK_XE_META_ATTRIBUTES "/" XPATH_UNFENCING_NVPAIR \ "|/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RSC_DEFAULTS \ "/" PCMK_XE_META_ATTRIBUTES "/" XPATH_UNFENCING_NVPAIR static void set_if_xpath(uint64_t flag, const char *xpath, pcmk_scheduler_t *scheduler) { xmlXPathObject *result = NULL; if (!pcmk__is_set(scheduler->flags, flag)) { result = pcmk__xpath_search(scheduler->input->doc, xpath); if (pcmk__xpath_num_results(result) > 0) { pcmk__set_scheduler_flags(scheduler, flag); } xmlXPathFreeObject(result); } } gboolean unpack_config(xmlNode *config, pcmk_scheduler_t *scheduler) { const char *value = NULL; GHashTable *config_hash = pcmk__strkey_table(free, free); const pcmk_rule_input_t rule_input = { .now = scheduler->priv->now, }; scheduler->priv->options = config_hash; pe__unpack_dataset_nvpairs(config, PCMK_XE_CLUSTER_PROPERTY_SET, &rule_input, config_hash, PCMK_VALUE_CIB_BOOTSTRAP_OPTIONS, scheduler); pcmk__validate_cluster_options(config_hash); set_config_flag(scheduler, PCMK_OPT_ENABLE_STARTUP_PROBES, pcmk__sched_probe_resources); if (!pcmk__is_set(scheduler->flags, pcmk__sched_probe_resources)) { pcmk__info("Startup probes: disabled (dangerous)"); } value = pcmk__cluster_option(config_hash, PCMK_OPT_HAVE_WATCHDOG); if (pcmk__is_true(value)) { pcmk__info("Watchdog-based self-fencing will be performed via SBD if " "fencing is required and " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " is nonzero"); pcmk__set_scheduler_flags(scheduler, pcmk__sched_have_fencing); } /* Set certain flags via xpath here, so they can be used before the relevant * configuration sections are unpacked. */ set_if_xpath(pcmk__sched_enable_unfencing, XPATH_ENABLE_UNFENCING, scheduler); value = pcmk__cluster_option(config_hash, PCMK_OPT_STONITH_TIMEOUT); pcmk_parse_interval_spec(value, &(scheduler->priv->fence_timeout_ms)); pcmk__debug("Default fencing action timeout: %s", pcmk__readable_interval(scheduler->priv->fence_timeout_ms)); set_config_flag(scheduler, PCMK_OPT_STONITH_ENABLED, pcmk__sched_fencing_enabled); if (pcmk__is_set(scheduler->flags, pcmk__sched_fencing_enabled)) { pcmk__debug("STONITH of failed nodes is enabled"); } else { pcmk__debug("STONITH of failed nodes is disabled"); } scheduler->priv->fence_action = pcmk__cluster_option(config_hash, PCMK_OPT_STONITH_ACTION); pcmk__trace("STONITH will %s nodes", scheduler->priv->fence_action); set_config_flag(scheduler, PCMK_OPT_CONCURRENT_FENCING, pcmk__sched_concurrent_fencing); if (pcmk__is_set(scheduler->flags, pcmk__sched_concurrent_fencing)) { pcmk__debug("Concurrent fencing is enabled"); } else { pcmk__debug("Concurrent fencing is disabled"); } value = pcmk__cluster_option(config_hash, PCMK_OPT_PRIORITY_FENCING_DELAY); if (value) { guint *delay_ms = &(scheduler->priv->priority_fencing_ms); pcmk_parse_interval_spec(value, delay_ms); pcmk__trace("Priority fencing delay is %s", pcmk__readable_interval(*delay_ms)); } set_config_flag(scheduler, PCMK_OPT_STOP_ALL_RESOURCES, pcmk__sched_stop_all); pcmk__debug("Stop all active resources: %s", pcmk__flag_text(scheduler->flags, pcmk__sched_stop_all)); set_config_flag(scheduler, PCMK_OPT_SYMMETRIC_CLUSTER, pcmk__sched_symmetric_cluster); if (pcmk__is_set(scheduler->flags, pcmk__sched_symmetric_cluster)) { pcmk__debug("Cluster is symmetric" " - resources can run anywhere by " "default"); } value = pcmk__cluster_option(config_hash, PCMK_OPT_NO_QUORUM_POLICY); if (pcmk__str_eq(value, PCMK_VALUE_IGNORE, pcmk__str_casei)) { scheduler->no_quorum_policy = pcmk_no_quorum_ignore; } else if (pcmk__str_eq(value, PCMK_VALUE_FREEZE, pcmk__str_casei)) { scheduler->no_quorum_policy = pcmk_no_quorum_freeze; } else if (pcmk__str_eq(value, PCMK_VALUE_DEMOTE, pcmk__str_casei)) { scheduler->no_quorum_policy = pcmk_no_quorum_demote; } else if (pcmk__strcase_any_of(value, PCMK_VALUE_FENCE, PCMK_VALUE_FENCE_LEGACY, NULL)) { if (pcmk__is_set(scheduler->flags, pcmk__sched_fencing_enabled)) { int do_panic = 0; pcmk__xe_get_int(scheduler->input, PCMK_XA_NO_QUORUM_PANIC, &do_panic); if (do_panic || pcmk__is_set(scheduler->flags, pcmk__sched_quorate)) { scheduler->no_quorum_policy = pcmk_no_quorum_fence; } else { pcmk__notice("Resetting " PCMK_OPT_NO_QUORUM_POLICY " to " "'" PCMK_VALUE_STOP "': cluster has never had " "quorum"); scheduler->no_quorum_policy = pcmk_no_quorum_stop; } } else { pcmk__config_err("Resetting " PCMK_OPT_NO_QUORUM_POLICY " to 'stop' because fencing is disabled"); scheduler->no_quorum_policy = pcmk_no_quorum_stop; } } else { scheduler->no_quorum_policy = pcmk_no_quorum_stop; } switch (scheduler->no_quorum_policy) { case pcmk_no_quorum_freeze: pcmk__debug("On loss of quorum: Freeze resources that require " "quorum"); break; case pcmk_no_quorum_stop: pcmk__debug("On loss of quorum: Stop resources that require " "quorum"); break; case pcmk_no_quorum_demote: pcmk__debug("On loss of quorum: Demote promotable resources and " "stop other resources"); break; case pcmk_no_quorum_fence: pcmk__notice("On loss of quorum: Fence all remaining nodes"); break; case pcmk_no_quorum_ignore: pcmk__notice("On loss of quorum: Ignore"); break; } set_config_flag(scheduler, PCMK_OPT_STOP_ORPHAN_RESOURCES, pcmk__sched_stop_removed_resources); if (pcmk__is_set(scheduler->flags, pcmk__sched_stop_removed_resources)) { pcmk__trace("Orphan resources are stopped"); } else { pcmk__trace("Orphan resources are ignored"); } set_config_flag(scheduler, PCMK_OPT_STOP_ORPHAN_ACTIONS, pcmk__sched_cancel_removed_actions); if (pcmk__is_set(scheduler->flags, pcmk__sched_cancel_removed_actions)) { pcmk__trace("Orphan resource actions are stopped"); } else { pcmk__trace("Orphan resource actions are ignored"); } set_config_flag(scheduler, PCMK_OPT_MAINTENANCE_MODE, pcmk__sched_in_maintenance); pcmk__trace("Maintenance mode: %s", pcmk__flag_text(scheduler->flags, pcmk__sched_in_maintenance)); set_config_flag(scheduler, PCMK_OPT_START_FAILURE_IS_FATAL, pcmk__sched_start_failure_fatal); if (pcmk__is_set(scheduler->flags, pcmk__sched_start_failure_fatal)) { pcmk__trace("Start failures are always fatal"); } else { pcmk__trace("Start failures are handled by failcount"); } if (pcmk__is_set(scheduler->flags, pcmk__sched_fencing_enabled)) { set_config_flag(scheduler, PCMK_OPT_STARTUP_FENCING, pcmk__sched_startup_fencing); } if (pcmk__is_set(scheduler->flags, pcmk__sched_startup_fencing)) { pcmk__trace("Unseen nodes will be fenced"); } else { pcmk__warn_once(pcmk__wo_blind, "Blind faith: not fencing unseen nodes"); } pe__unpack_node_health_scores(scheduler); scheduler->priv->placement_strategy = pcmk__cluster_option(config_hash, PCMK_OPT_PLACEMENT_STRATEGY); pcmk__trace("Placement strategy: %s", scheduler->priv->placement_strategy); set_config_flag(scheduler, PCMK_OPT_SHUTDOWN_LOCK, pcmk__sched_shutdown_lock); if (pcmk__is_set(scheduler->flags, pcmk__sched_shutdown_lock)) { value = pcmk__cluster_option(config_hash, PCMK_OPT_SHUTDOWN_LOCK_LIMIT); pcmk_parse_interval_spec(value, &(scheduler->priv->shutdown_lock_ms)); pcmk__trace("Resources will be locked to nodes that were cleanly " "shut down (locks expire after %s)", pcmk__readable_interval(scheduler->priv->shutdown_lock_ms)); } else { pcmk__trace("Resources will not be locked to nodes that were cleanly " "shut down"); } value = pcmk__cluster_option(config_hash, PCMK_OPT_NODE_PENDING_TIMEOUT); pcmk_parse_interval_spec(value, &(scheduler->priv->node_pending_ms)); if (scheduler->priv->node_pending_ms == 0U) { pcmk__trace("Do not fence pending nodes"); } else { pcmk__trace("Fence pending nodes after %s", pcmk__readable_interval(scheduler->priv->node_pending_ms)); } return TRUE; } /*! * \internal * \brief Create a new node object in scheduler data * * \param[in] id ID of new node * \param[in] uname Name of new node * \param[in] type Type of new node * \param[in] score Score of new node * \param[in,out] scheduler Scheduler data * * \return Newly created node object * \note The returned object is part of the scheduler data and should not be * freed separately. */ pcmk_node_t * pe_create_node(const char *id, const char *uname, const char *type, int score, pcmk_scheduler_t *scheduler) { enum pcmk__node_variant variant = pcmk__node_variant_cluster; pcmk_node_t *new_node = NULL; if (pcmk_find_node(scheduler, uname) != NULL) { pcmk__config_warn("More than one node entry has name '%s'", uname); } if (pcmk__str_eq(type, PCMK_VALUE_MEMBER, pcmk__str_null_matches|pcmk__str_casei)) { variant = pcmk__node_variant_cluster; } else if (pcmk__str_eq(type, PCMK_VALUE_REMOTE, pcmk__str_casei)) { variant = pcmk__node_variant_remote; } else { pcmk__config_err("Ignoring node %s with unrecognized type '%s'", pcmk__s(uname, "without name"), type); return NULL; } new_node = calloc(1, sizeof(pcmk_node_t)); if (new_node == NULL) { pcmk__sched_err(scheduler, "Could not allocate memory for node %s", uname); return NULL; } new_node->assign = calloc(1, sizeof(struct pcmk__node_assignment)); new_node->details = calloc(1, sizeof(struct pcmk__node_details)); new_node->priv = calloc(1, sizeof(pcmk__node_private_t)); if ((new_node->assign == NULL) || (new_node->details == NULL) || (new_node->priv == NULL)) { free(new_node->assign); free(new_node->details); free(new_node->priv); free(new_node); pcmk__sched_err(scheduler, "Could not allocate memory for node %s", uname); return NULL; } pcmk__trace("Creating node for entry %s/%s", uname, id); new_node->assign->score = score; new_node->priv->id = id; new_node->priv->name = uname; new_node->priv->flags = pcmk__node_probes_allowed; new_node->details->online = FALSE; new_node->details->shutdown = FALSE; new_node->details->running_rsc = NULL; new_node->priv->scheduler = scheduler; new_node->priv->variant = variant; new_node->priv->attrs = pcmk__strkey_table(free, free); new_node->priv->utilization = pcmk__strkey_table(free, free); new_node->priv->digest_cache = pcmk__strkey_table(free, pe__free_digests); if (pcmk__is_pacemaker_remote_node(new_node)) { pcmk__insert_dup(new_node->priv->attrs, CRM_ATTR_KIND, "remote"); pcmk__set_scheduler_flags(scheduler, pcmk__sched_have_remote_nodes); } else { pcmk__insert_dup(new_node->priv->attrs, CRM_ATTR_KIND, "cluster"); } scheduler->nodes = g_list_insert_sorted(scheduler->nodes, new_node, pe__cmp_node_name); return new_node; } static const char * expand_remote_rsc_meta(xmlNode *xml_obj, xmlNode *parent, pcmk_scheduler_t *data) { xmlNode *attr_set = NULL; xmlNode *attr = NULL; const char *container_id = pcmk__xe_id(xml_obj); const char *remote_name = NULL; const char *remote_server = NULL; const char *remote_port = NULL; const char *connect_timeout = "60s"; const char *remote_allow_migrate=NULL; const char *is_managed = NULL; // @TODO This doesn't handle rules or id-ref for (attr_set = pcmk__xe_first_child(xml_obj, PCMK_XE_META_ATTRIBUTES, NULL, NULL); attr_set != NULL; attr_set = pcmk__xe_next(attr_set, PCMK_XE_META_ATTRIBUTES)) { for (attr = pcmk__xe_first_child(attr_set, NULL, NULL, NULL); attr != NULL; attr = pcmk__xe_next(attr, NULL)) { const char *value = pcmk__xe_get(attr, PCMK_XA_VALUE); const char *name = pcmk__xe_get(attr, PCMK_XA_NAME); if (name == NULL) { // Sanity continue; } if (strcmp(name, PCMK_META_REMOTE_NODE) == 0) { remote_name = value; } else if (strcmp(name, PCMK_META_REMOTE_ADDR) == 0) { remote_server = value; } else if (strcmp(name, PCMK_META_REMOTE_PORT) == 0) { remote_port = value; } else if (strcmp(name, PCMK_META_REMOTE_CONNECT_TIMEOUT) == 0) { connect_timeout = value; } else if (strcmp(name, PCMK_META_REMOTE_ALLOW_MIGRATE) == 0) { remote_allow_migrate = value; } else if (strcmp(name, PCMK_META_IS_MANAGED) == 0) { is_managed = value; } } } if (remote_name == NULL) { return NULL; } if (pe_find_resource(data->priv->resources, remote_name) != NULL) { return NULL; } pe_create_remote_xml(parent, remote_name, container_id, remote_allow_migrate, is_managed, connect_timeout, remote_server, remote_port); return remote_name; } static void handle_startup_fencing(pcmk_scheduler_t *scheduler, pcmk_node_t *new_node) { if ((new_node->priv->variant == pcmk__node_variant_remote) && (new_node->priv->remote == NULL)) { /* Ignore fencing for remote nodes that don't have a connection resource * associated with them. This happens when remote node entries get left * in the nodes section after the connection resource is removed. */ return; } if (pcmk__is_set(scheduler->flags, pcmk__sched_startup_fencing)) { // All nodes are unclean until we've seen their status entry new_node->details->unclean = TRUE; } else { // Blind faith ... new_node->details->unclean = FALSE; } } gboolean unpack_nodes(xmlNode *xml_nodes, pcmk_scheduler_t *scheduler) { xmlNode *xml_obj = NULL; pcmk_node_t *new_node = NULL; const char *id = NULL; const char *uname = NULL; const char *type = NULL; for (xml_obj = pcmk__xe_first_child(xml_nodes, PCMK_XE_NODE, NULL, NULL); xml_obj != NULL; xml_obj = pcmk__xe_next(xml_obj, PCMK_XE_NODE)) { int score = 0; int rc = pcmk__xe_get_score(xml_obj, PCMK_XA_SCORE, &score, 0); new_node = NULL; id = pcmk__xe_get(xml_obj, PCMK_XA_ID); uname = pcmk__xe_get(xml_obj, PCMK_XA_UNAME); type = pcmk__xe_get(xml_obj, PCMK_XA_TYPE); pcmk__trace("Processing node %s/%s", uname, id); if (id == NULL) { pcmk__config_err("Ignoring <" PCMK_XE_NODE "> entry in configuration without id"); continue; } if (rc != pcmk_rc_ok) { // Not possible with schema validation enabled pcmk__config_warn("Using 0 as score for node %s " "because '%s' is not a valid score: %s", pcmk__s(uname, "without name"), pcmk__xe_get(xml_obj, PCMK_XA_SCORE), pcmk_rc_str(rc)); } new_node = pe_create_node(id, uname, type, score, scheduler); if (new_node == NULL) { return FALSE; } handle_startup_fencing(scheduler, new_node); add_node_attrs(xml_obj, new_node, FALSE, scheduler); pcmk__trace("Done with node %s", pcmk__xe_get(xml_obj, PCMK_XA_UNAME)); } return TRUE; } static void unpack_launcher(pcmk_resource_t *rsc, pcmk_scheduler_t *scheduler) { const char *launcher_id = NULL; if (rsc->priv->children != NULL) { g_list_foreach(rsc->priv->children, (GFunc) unpack_launcher, scheduler); return; } launcher_id = g_hash_table_lookup(rsc->priv->meta, PCMK__META_CONTAINER); if ((launcher_id != NULL) && !pcmk__str_eq(launcher_id, rsc->id, pcmk__str_none)) { pcmk_resource_t *launcher = pe_find_resource(scheduler->priv->resources, launcher_id); if (launcher != NULL) { rsc->priv->launcher = launcher; launcher->priv->launched = g_list_append(launcher->priv->launched, rsc); pcmk__rsc_trace(rsc, "Resource %s's launcher is %s", rsc->id, launcher_id); } else { pcmk__config_err("Resource %s: Unknown " PCMK__META_CONTAINER " %s", rsc->id, launcher_id); } } } gboolean unpack_remote_nodes(xmlNode *xml_resources, pcmk_scheduler_t *scheduler) { xmlNode *xml_obj = NULL; /* Create remote nodes and guest nodes from the resource configuration * before unpacking resources. */ for (xml_obj = pcmk__xe_first_child(xml_resources, NULL, NULL, NULL); xml_obj != NULL; xml_obj = pcmk__xe_next(xml_obj, NULL)) { const char *new_node_id = NULL; /* Check for remote nodes, which are defined by ocf:pacemaker:remote * primitives. */ if (xml_contains_remote_node(xml_obj)) { new_node_id = pcmk__xe_id(xml_obj); /* The pcmk_find_node() check ensures we don't iterate over an * expanded node that has already been added to the node list */ if (new_node_id && (pcmk_find_node(scheduler, new_node_id) == NULL)) { pcmk__trace("Found remote node %s defined by resource %s", new_node_id, pcmk__xe_id(xml_obj)); pe_create_node(new_node_id, new_node_id, PCMK_VALUE_REMOTE, 0, scheduler); } continue; } /* Check for guest nodes, which are defined by special meta-attributes * of a primitive of any type (for example, VirtualDomain or Xen). */ if (pcmk__xe_is(xml_obj, PCMK_XE_PRIMITIVE)) { /* This will add an ocf:pacemaker:remote primitive to the * configuration for the guest node's connection, to be unpacked * later. */ new_node_id = expand_remote_rsc_meta(xml_obj, xml_resources, scheduler); if (new_node_id && (pcmk_find_node(scheduler, new_node_id) == NULL)) { pcmk__trace("Found guest node %s in resource %s", new_node_id, pcmk__xe_id(xml_obj)); pe_create_node(new_node_id, new_node_id, PCMK_VALUE_REMOTE, 0, scheduler); } continue; } /* Check for guest nodes inside a group. Clones are currently not * supported as guest nodes. */ if (pcmk__xe_is(xml_obj, PCMK_XE_GROUP)) { xmlNode *xml_obj2 = NULL; for (xml_obj2 = pcmk__xe_first_child(xml_obj, NULL, NULL, NULL); xml_obj2 != NULL; xml_obj2 = pcmk__xe_next(xml_obj2, NULL)) { new_node_id = expand_remote_rsc_meta(xml_obj2, xml_resources, scheduler); if (new_node_id && (pcmk_find_node(scheduler, new_node_id) == NULL)) { pcmk__trace("Found guest node %s in resource %s inside " "group %s", new_node_id, pcmk__xe_id(xml_obj2), pcmk__xe_id(xml_obj)); pe_create_node(new_node_id, new_node_id, PCMK_VALUE_REMOTE, 0, scheduler); } } } } return TRUE; } /* Call this after all the nodes and resources have been * unpacked, but before the status section is read. * * A remote node's online status is reflected by the state * of the remote node's connection resource. We need to link * the remote node to this connection resource so we can have * easy access to the connection resource during the scheduler calculations. */ static void link_rsc2remotenode(pcmk_scheduler_t *scheduler, pcmk_resource_t *new_rsc) { pcmk_node_t *remote_node = NULL; if (!pcmk__is_set(new_rsc->flags, pcmk__rsc_is_remote_connection)) { return; } if (pcmk__is_set(scheduler->flags, pcmk__sched_location_only)) { /* remote_nodes and remote_resources are not linked in quick location calculations */ return; } remote_node = pcmk_find_node(scheduler, new_rsc->id); CRM_CHECK(remote_node != NULL, return); pcmk__rsc_trace(new_rsc, "Linking remote connection resource %s to %s", new_rsc->id, pcmk__node_name(remote_node)); remote_node->priv->remote = new_rsc; if (new_rsc->priv->launcher == NULL) { /* Handle start-up fencing for remote nodes (as opposed to guest nodes) * the same as is done for cluster nodes. */ handle_startup_fencing(scheduler, remote_node); } else { /* pe_create_node() marks the new node as "remote" or "cluster"; now * that we know the node is a guest node, update it correctly. */ pcmk__insert_dup(remote_node->priv->attrs, CRM_ATTR_KIND, "container"); } } /*! * \internal * \brief Parse configuration XML for resource information * * \param[in] xml_resources Top of resource configuration XML * \param[in,out] scheduler Scheduler data * * \return TRUE * * \note unpack_remote_nodes() MUST be called before this, so that the nodes can * be used when pe__unpack_resource() calls resource_location() */ gboolean unpack_resources(const xmlNode *xml_resources, pcmk_scheduler_t *scheduler) { xmlNode *xml_obj = NULL; GList *gIter = NULL; scheduler->priv->templates = pcmk__strkey_table(free, pcmk__free_idref); for (xml_obj = pcmk__xe_first_child(xml_resources, NULL, NULL, NULL); xml_obj != NULL; xml_obj = pcmk__xe_next(xml_obj, NULL)) { pcmk_resource_t *new_rsc = NULL; const char *id = pcmk__xe_id(xml_obj); if (pcmk__str_empty(id)) { pcmk__config_err("Ignoring <%s> resource without ID", xml_obj->name); continue; } if (pcmk__xe_is(xml_obj, PCMK_XE_TEMPLATE)) { if (g_hash_table_lookup_extended(scheduler->priv->templates, id, NULL, NULL) == FALSE) { /* Record the template's ID for the knowledge of its existence anyway. */ pcmk__insert_dup(scheduler->priv->templates, id, NULL); } continue; } pcmk__trace("Unpacking <%s " PCMK_XA_ID "='%s'>", xml_obj->name, id); if (pe__unpack_resource(xml_obj, &new_rsc, NULL, scheduler) == pcmk_rc_ok) { scheduler->priv->resources = g_list_append(scheduler->priv->resources, new_rsc); pcmk__rsc_trace(new_rsc, "Added resource %s", new_rsc->id); } else { pcmk__config_err("Ignoring <%s> resource '%s' " "because configuration is invalid", xml_obj->name, id); } } for (gIter = scheduler->priv->resources; gIter != NULL; gIter = gIter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) gIter->data; unpack_launcher(rsc, scheduler); link_rsc2remotenode(scheduler, rsc); } scheduler->priv->resources = g_list_sort(scheduler->priv->resources, pe__cmp_rsc_priority); if (pcmk__is_set(scheduler->flags, pcmk__sched_location_only)) { /* Ignore */ } else if (pcmk__is_set(scheduler->flags, pcmk__sched_fencing_enabled) && !pcmk__is_set(scheduler->flags, pcmk__sched_have_fencing)) { pcmk__config_err("Resource start-up disabled since no STONITH resources have been defined"); pcmk__config_err("Either configure some or disable STONITH with the " PCMK_OPT_STONITH_ENABLED " option"); pcmk__config_err("NOTE: Clusters with shared data need STONITH to ensure data integrity"); } return TRUE; } /*! * \internal * \brief Validate the levels in a fencing topology * * \param[in] xml \c PCMK_XE_FENCING_TOPOLOGY element */ void pcmk__validate_fencing_topology(const xmlNode *xml) { if (xml == NULL) { return; } CRM_CHECK(pcmk__xe_is(xml, PCMK_XE_FENCING_TOPOLOGY), return); for (const xmlNode *level = pcmk__xe_first_child(xml, PCMK_XE_FENCING_LEVEL, NULL, NULL); level != NULL; level = pcmk__xe_next(level, PCMK_XE_FENCING_LEVEL)) { const char *id = pcmk__xe_id(level); int index = 0; if (pcmk__str_empty(id)) { pcmk__config_err("Ignoring fencing level without ID"); continue; } if (pcmk__xe_get_int(level, PCMK_XA_INDEX, &index) != pcmk_rc_ok) { pcmk__config_err("Ignoring fencing level %s with invalid index", id); continue; } if ((index < ST__LEVEL_MIN) || (index > ST__LEVEL_MAX)) { pcmk__config_err("Ignoring fencing level %s with out-of-range " "index %d", id, index); } } } gboolean unpack_tags(xmlNode *xml_tags, pcmk_scheduler_t *scheduler) { xmlNode *xml_tag = NULL; scheduler->priv->tags = pcmk__strkey_table(free, pcmk__free_idref); for (xml_tag = pcmk__xe_first_child(xml_tags, PCMK_XE_TAG, NULL, NULL); xml_tag != NULL; xml_tag = pcmk__xe_next(xml_tag, PCMK_XE_TAG)) { xmlNode *xml_obj_ref = NULL; const char *tag_id = pcmk__xe_id(xml_tag); if (tag_id == NULL) { pcmk__config_err("Ignoring <%s> without " PCMK_XA_ID, (const char *) xml_tag->name); continue; } for (xml_obj_ref = pcmk__xe_first_child(xml_tag, PCMK_XE_OBJ_REF, NULL, NULL); xml_obj_ref != NULL; xml_obj_ref = pcmk__xe_next(xml_obj_ref, PCMK_XE_OBJ_REF)) { const char *obj_ref = pcmk__xe_id(xml_obj_ref); if (obj_ref == NULL) { pcmk__config_err("Ignoring <%s> for tag '%s' without " PCMK_XA_ID, xml_obj_ref->name, tag_id); continue; } pcmk__add_idref(scheduler->priv->tags, tag_id, obj_ref); } } return TRUE; } /*! * \internal * \brief Unpack a ticket state entry * * \param[in] xml_ticket XML ticket state to unpack * \param[in,out] userdata Scheduler data * * \return pcmk_rc_ok (to always continue unpacking further entries) */ static int unpack_ticket_state(xmlNode *xml_ticket, void *userdata) { pcmk_scheduler_t *scheduler = userdata; const char *ticket_id = NULL; const char *granted = NULL; const char *last_granted = NULL; const char *standby = NULL; xmlAttrPtr xIter = NULL; pcmk__ticket_t *ticket = NULL; ticket_id = pcmk__xe_id(xml_ticket); if (pcmk__str_empty(ticket_id)) { return pcmk_rc_ok; } pcmk__trace("Processing ticket state for %s", ticket_id); ticket = g_hash_table_lookup(scheduler->priv->ticket_constraints, ticket_id); if (ticket == NULL) { ticket = ticket_new(ticket_id, scheduler); if (ticket == NULL) { return pcmk_rc_ok; } } for (xIter = xml_ticket->properties; xIter; xIter = xIter->next) { const char *prop_name = (const char *)xIter->name; const char *prop_value = pcmk__xml_attr_value(xIter); if (pcmk__str_eq(prop_name, PCMK_XA_ID, pcmk__str_none)) { continue; } pcmk__insert_dup(ticket->state, prop_name, prop_value); } granted = g_hash_table_lookup(ticket->state, PCMK__XA_GRANTED); if (pcmk__is_true(granted)) { pcmk__set_ticket_flags(ticket, pcmk__ticket_granted); pcmk__info("We have ticket '%s'", ticket->id); } else { pcmk__clear_ticket_flags(ticket, pcmk__ticket_granted); pcmk__info("We do not have ticket '%s'", ticket->id); } last_granted = g_hash_table_lookup(ticket->state, PCMK_XA_LAST_GRANTED); if (last_granted) { long long last_granted_ll = 0LL; int rc = pcmk__scan_ll(last_granted, &last_granted_ll, 0LL); if (rc != pcmk_rc_ok) { pcmk__warn("Using %lld instead of invalid " PCMK_XA_LAST_GRANTED " value '%s' in state for ticket %s: %s", last_granted_ll, last_granted, ticket->id, pcmk_rc_str(rc)); } ticket->last_granted = (time_t) last_granted_ll; } standby = g_hash_table_lookup(ticket->state, PCMK_XA_STANDBY); if (pcmk__is_true(standby)) { pcmk__set_ticket_flags(ticket, pcmk__ticket_standby); if (pcmk__is_set(ticket->flags, pcmk__ticket_granted)) { pcmk__info("Granted ticket '%s' is in standby-mode", ticket->id); } } else { pcmk__clear_ticket_flags(ticket, pcmk__ticket_standby); } pcmk__trace("Done with ticket state for %s", ticket_id); return pcmk_rc_ok; } static void unpack_handle_remote_attrs(pcmk_node_t *this_node, const xmlNode *state, pcmk_scheduler_t *scheduler) { const char *discovery = NULL; const xmlNode *attrs = NULL; pcmk_resource_t *rsc = NULL; int maint = 0; if (!pcmk__xe_is(state, PCMK__XE_NODE_STATE)) { return; } if ((this_node == NULL) || !pcmk__is_pacemaker_remote_node(this_node)) { return; } pcmk__trace("Processing Pacemaker Remote node %s", pcmk__node_name(this_node)); pcmk__scan_min_int(pcmk__xe_get(state, PCMK__XA_NODE_IN_MAINTENANCE), &maint, 0); if (maint) { pcmk__set_node_flags(this_node, pcmk__node_remote_maint); } else { pcmk__clear_node_flags(this_node, pcmk__node_remote_maint); } rsc = this_node->priv->remote; if (!pcmk__is_set(this_node->priv->flags, pcmk__node_remote_reset)) { this_node->details->unclean = FALSE; pcmk__set_node_flags(this_node, pcmk__node_seen); } attrs = pcmk__xe_first_child(state, PCMK__XE_TRANSIENT_ATTRIBUTES, NULL, NULL); add_node_attrs(attrs, this_node, TRUE, scheduler); if (pe__shutdown_requested(this_node)) { pcmk__info("%s is shutting down", pcmk__node_name(this_node)); this_node->details->shutdown = TRUE; } if (pcmk__is_true(pcmk__node_attr(this_node, PCMK_NODE_ATTR_STANDBY, NULL, pcmk__rsc_node_current))) { pcmk__info("%s is in standby mode", pcmk__node_name(this_node)); pcmk__set_node_flags(this_node, pcmk__node_standby); } if (pcmk__is_true(pcmk__node_attr(this_node, PCMK_NODE_ATTR_MAINTENANCE, NULL, pcmk__rsc_node_current)) || ((rsc != NULL) && !pcmk__is_set(rsc->flags, pcmk__rsc_managed))) { pcmk__info("%s is in maintenance mode", pcmk__node_name(this_node)); this_node->details->maintenance = TRUE; } discovery = pcmk__node_attr(this_node, PCMK__NODE_ATTR_RESOURCE_DISCOVERY_ENABLED, NULL, pcmk__rsc_node_current); if ((discovery != NULL) && !pcmk__is_true(discovery)) { pcmk__warn_once(pcmk__wo_rdisc_enabled, "Support for the " PCMK__NODE_ATTR_RESOURCE_DISCOVERY_ENABLED " node attribute is deprecated and will be removed" " (and behave as 'true') in a future release."); if (pcmk__is_remote_node(this_node) && !pcmk__is_set(scheduler->flags, pcmk__sched_fencing_enabled)) { pcmk__config_warn("Ignoring " PCMK__NODE_ATTR_RESOURCE_DISCOVERY_ENABLED " attribute on Pacemaker Remote node %s" " because fencing is disabled", pcmk__node_name(this_node)); } else { /* This is either a remote node with fencing enabled, or a guest * node. We don't care whether fencing is enabled when fencing guest * nodes, because they are "fenced" by recovering their containing * resource. */ pcmk__info("%s has resource discovery disabled", pcmk__node_name(this_node)); pcmk__clear_node_flags(this_node, pcmk__node_probes_allowed); } } } /*! * \internal * \brief Unpack a cluster node's transient attributes * * \param[in] state CIB node state XML * \param[in,out] node Cluster node whose attributes are being unpacked * \param[in,out] scheduler Scheduler data */ static void unpack_transient_attributes(const xmlNode *state, pcmk_node_t *node, pcmk_scheduler_t *scheduler) { const char *discovery = NULL; const xmlNode *attrs = pcmk__xe_first_child(state, PCMK__XE_TRANSIENT_ATTRIBUTES, NULL, NULL); add_node_attrs(attrs, node, TRUE, scheduler); if (pcmk__is_true(pcmk__node_attr(node, PCMK_NODE_ATTR_STANDBY, NULL, pcmk__rsc_node_current))) { pcmk__info("%s is in standby mode", pcmk__node_name(node)); pcmk__set_node_flags(node, pcmk__node_standby); } if (pcmk__is_true(pcmk__node_attr(node, PCMK_NODE_ATTR_MAINTENANCE, NULL, pcmk__rsc_node_current))) { pcmk__info("%s is in maintenance mode", pcmk__node_name(node)); node->details->maintenance = TRUE; } discovery = pcmk__node_attr(node, PCMK__NODE_ATTR_RESOURCE_DISCOVERY_ENABLED, NULL, pcmk__rsc_node_current); if ((discovery != NULL) && !pcmk__is_true(discovery)) { pcmk__config_warn("Ignoring " PCMK__NODE_ATTR_RESOURCE_DISCOVERY_ENABLED " attribute for %s because disabling resource" " discovery is not allowed for cluster nodes", pcmk__node_name(node)); } } /*! * \internal * \brief Unpack a node state entry (first pass) * * Unpack one node state entry from status. This unpacks information from the * \C PCMK__XE_NODE_STATE element itself and node attributes inside it, but not * the resource history inside it. Multiple passes through the status are needed * to fully unpack everything. * * \param[in] state CIB node state XML * \param[in,out] scheduler Scheduler data */ static void unpack_node_state(const xmlNode *state, pcmk_scheduler_t *scheduler) { const char *id = NULL; const char *uname = NULL; pcmk_node_t *this_node = NULL; id = pcmk__xe_get(state, PCMK_XA_ID); if (id == NULL) { pcmk__config_err("Ignoring invalid " PCMK__XE_NODE_STATE " entry without " PCMK_XA_ID); pcmk__log_xml_info(state, "missing-id"); return; } uname = pcmk__xe_get(state, PCMK_XA_UNAME); if (uname == NULL) { /* If a joining peer makes the cluster acquire the quorum from Corosync * but has not joined the controller CPG membership yet, it's possible * that the created PCMK__XE_NODE_STATE entry doesn't have a * PCMK_XA_UNAME yet. Recognize the node as pending and wait for it to * join CPG. */ pcmk__trace("Handling " PCMK__XE_NODE_STATE " entry with id=\"%s\" " "without " PCMK_XA_UNAME, id); } this_node = pe_find_node_any(scheduler->nodes, id, uname); if (this_node == NULL) { pcmk__notice("Ignoring recorded state for removed node with name %s " "and " PCMK_XA_ID " %s", pcmk__s(uname, "unknown"), id); return; } if (pcmk__is_pacemaker_remote_node(this_node)) { int remote_fenced = 0; /* We can't determine the online status of Pacemaker Remote nodes until * after all resource history has been unpacked. In this first pass, we * do need to mark whether the node has been fenced, as this plays a * role during unpacking cluster node resource state. */ pcmk__scan_min_int(pcmk__xe_get(state, PCMK__XA_NODE_FENCED), &remote_fenced, 0); if (remote_fenced) { pcmk__set_node_flags(this_node, pcmk__node_remote_fenced); } else { pcmk__clear_node_flags(this_node, pcmk__node_remote_fenced); } return; } unpack_transient_attributes(state, this_node, scheduler); /* Provisionally mark this cluster node as clean. We have at least seen it * in the current cluster's lifetime. */ this_node->details->unclean = FALSE; pcmk__set_node_flags(this_node, pcmk__node_seen); pcmk__trace("Determining online status of cluster node %s (id %s)", pcmk__node_name(this_node), id); determine_online_status(state, this_node, scheduler); if (!pcmk__is_set(scheduler->flags, pcmk__sched_quorate) && this_node->details->online && (scheduler->no_quorum_policy == pcmk_no_quorum_fence)) { /* Everything else should flow from this automatically * (at least until the scheduler becomes able to migrate off * healthy resources) */ pe_fence_node(scheduler, this_node, "cluster does not have quorum", FALSE); } } /*! * \internal * \brief Unpack nodes' resource history as much as possible * * Unpack as many nodes' resource history as possible in one pass through the * status. We need to process Pacemaker Remote nodes' connections/containers * before unpacking their history; the connection/container history will be * in another node's history, so it might take multiple passes to unpack * everything. * * \param[in] status CIB XML status section * \param[in] fence If true, treat any not-yet-unpacked nodes as unseen * \param[in,out] scheduler Scheduler data * * \return Standard Pacemaker return code (specifically pcmk_rc_ok if done, * or EAGAIN if more unpacking remains to be done) */ static int unpack_node_history(const xmlNode *status, bool fence, pcmk_scheduler_t *scheduler) { int rc = pcmk_rc_ok; // Loop through all PCMK__XE_NODE_STATE entries in CIB status for (const xmlNode *state = pcmk__xe_first_child(status, PCMK__XE_NODE_STATE, NULL, NULL); state != NULL; state = pcmk__xe_next(state, PCMK__XE_NODE_STATE)) { const char *id = pcmk__xe_id(state); const char *uname = pcmk__xe_get(state, PCMK_XA_UNAME); pcmk_node_t *this_node = NULL; if ((id == NULL) || (uname == NULL)) { // Warning already logged in first pass through status section pcmk__trace("Not unpacking resource history from malformed " PCMK__XE_NODE_STATE " without id and/or uname"); continue; } this_node = pe_find_node_any(scheduler->nodes, id, uname); if (this_node == NULL) { // Warning already logged in first pass through status section pcmk__trace("Not unpacking resource history for node %s because " "no longer in configuration", id); continue; } if (pcmk__is_set(this_node->priv->flags, pcmk__node_unpacked)) { pcmk__trace("Not unpacking resource history for node %s because " "already unpacked", id); continue; } if (fence) { // We're processing all remaining nodes } else if (pcmk__is_guest_or_bundle_node(this_node)) { /* We can unpack a guest node's history only after we've unpacked * other resource history to the point that we know that the node's * connection and containing resource are both up. */ const pcmk_resource_t *remote = this_node->priv->remote; const pcmk_resource_t *launcher = remote->priv->launcher; if ((remote->priv->orig_role != pcmk_role_started) || (launcher->priv->orig_role != pcmk_role_started)) { pcmk__trace("Not unpacking resource history for guest node %s " "because launcher and connection are not known to " "be up", id); continue; } } else if (pcmk__is_remote_node(this_node)) { /* We can unpack a remote node's history only after we've unpacked * other resource history to the point that we know that the node's * connection is up, with the exception of when shutdown locks are * in use. */ pcmk_resource_t *rsc = this_node->priv->remote; if ((rsc == NULL) || (!pcmk__is_set(scheduler->flags, pcmk__sched_shutdown_lock) && (rsc->priv->orig_role != pcmk_role_started))) { pcmk__trace("Not unpacking resource history for remote node %s " "because connection is not known to be up", id); continue; } /* If fencing and shutdown locks are disabled and we're not processing * unseen nodes, then we don't want to unpack offline nodes until online * nodes have been unpacked. This allows us to number active clone * instances first. */ } else if (!pcmk__any_flags_set(scheduler->flags, pcmk__sched_fencing_enabled |pcmk__sched_shutdown_lock) && !this_node->details->online) { pcmk__trace("Not unpacking resource history for offline " "cluster node %s", id); continue; } if (pcmk__is_pacemaker_remote_node(this_node)) { determine_remote_online_status(scheduler, this_node); unpack_handle_remote_attrs(this_node, state, scheduler); } pcmk__trace("Unpacking resource history for %snode %s", (fence? "unseen " : ""), id); pcmk__set_node_flags(this_node, pcmk__node_unpacked); unpack_node_lrm(this_node, state, scheduler); rc = EAGAIN; // Other node histories might depend on this one } return rc; } /* remove nodes that are down, stopping */ /* create positive rsc_to_node constraints between resources and the nodes they are running on */ /* anything else? */ gboolean unpack_status(xmlNode *status, pcmk_scheduler_t *scheduler) { xmlNode *state = NULL; pcmk__trace("Beginning unpack"); if (scheduler->priv->ticket_constraints == NULL) { scheduler->priv->ticket_constraints = pcmk__strkey_table(free, destroy_ticket); } for (state = pcmk__xe_first_child(status, NULL, NULL, NULL); state != NULL; state = pcmk__xe_next(state, NULL)) { if (pcmk__xe_is(state, PCMK_XE_TICKETS)) { pcmk__xe_foreach_child(state, PCMK__XE_TICKET_STATE, unpack_ticket_state, scheduler); } else if (pcmk__xe_is(state, PCMK__XE_NODE_STATE)) { unpack_node_state(state, scheduler); } } while (unpack_node_history(status, FALSE, scheduler) == EAGAIN) { pcmk__trace("Another pass through node resource histories is needed"); } // Now catch any nodes we didn't see unpack_node_history(status, pcmk__is_set(scheduler->flags, pcmk__sched_fencing_enabled), scheduler); /* Now that we know where resources are, we can schedule stops of containers * with failed bundle connections */ if (scheduler->priv->stop_needed != NULL) { for (GList *item = scheduler->priv->stop_needed; item != NULL; item = item->next) { pcmk_resource_t *container = item->data; pcmk_node_t *node = pcmk__current_node(container); if (node) { stop_action(container, node, FALSE); } } g_list_free(scheduler->priv->stop_needed); scheduler->priv->stop_needed = NULL; } /* Now that we know status of all Pacemaker Remote connections and nodes, * we can stop connections for node shutdowns, and check the online status * of remote/guest nodes that didn't have any node history to unpack. */ for (GList *gIter = scheduler->nodes; gIter != NULL; gIter = gIter->next) { pcmk_node_t *this_node = gIter->data; if (!pcmk__is_pacemaker_remote_node(this_node)) { continue; } if (this_node->details->shutdown && (this_node->priv->remote != NULL)) { pe__set_next_role(this_node->priv->remote, pcmk_role_stopped, "remote shutdown"); } if (!pcmk__is_set(this_node->priv->flags, pcmk__node_unpacked)) { determine_remote_online_status(scheduler, this_node); } } return TRUE; } /*! * \internal * \brief Unpack node's time when it became a member at the cluster layer * * \param[in] node_state Node's \c PCMK__XE_NODE_STATE entry * \param[in,out] scheduler Scheduler data * * \return Epoch time when node became a cluster member * (or scheduler effective time for legacy entries) if a member, * 0 if not a member, or -1 if no valid information available */ static long long unpack_node_member(const xmlNode *node_state, pcmk_scheduler_t *scheduler) { const char *member_time = pcmk__xe_get(node_state, PCMK__XA_IN_CCM); bool is_member = false; if (member_time == NULL) { return -1LL; } if (pcmk__parse_bool(member_time, &is_member) != pcmk_rc_ok) { long long when_member = 0LL; if ((pcmk__scan_ll(member_time, &when_member, 0LL) != pcmk_rc_ok) || (when_member < 0LL)) { pcmk__warn("Unrecognized value '%s' for " PCMK__XA_IN_CCM " in " PCMK__XE_NODE_STATE " entry", member_time); return -1LL; } return when_member; } /* If in_ccm=0, we'll return 0 here. If in_ccm=1, either the entry was * recorded as a boolean for a DC < 2.1.7, or the node is pending shutdown * and has left the CPG, in which case it was set to 1 to avoid fencing for * PCMK_OPT_NODE_PENDING_TIMEOUT. * * We return the effective time for in_ccm=1 because what's important to * avoid fencing is that effective time minus this value is less than the * pending node timeout. */ return is_member? (long long) pcmk__scheduler_epoch_time(scheduler) : 0LL; } /*! * \internal * \brief Unpack node's time when it became online in process group * * \param[in] node_state Node's \c PCMK__XE_NODE_STATE entry * * \return Epoch time when node became online in process group (or 0 if not * online, or 1 for legacy online entries) */ static long long unpack_node_online(const xmlNode *node_state) { const char *peer_time = pcmk__xe_get(node_state, PCMK_XA_CRMD); // @COMPAT Entries recorded for DCs < 2.1.7 have "online" or "offline" if (pcmk__str_eq(peer_time, PCMK_VALUE_OFFLINE, pcmk__str_casei|pcmk__str_null_matches)) { return 0LL; } else if (pcmk__str_eq(peer_time, PCMK_VALUE_ONLINE, pcmk__str_casei)) { return 1LL; } else { long long when_online = 0LL; if ((pcmk__scan_ll(peer_time, &when_online, 0LL) != pcmk_rc_ok) || (when_online < 0)) { pcmk__warn("Unrecognized value '%s' for " PCMK_XA_CRMD " in " PCMK__XE_NODE_STATE " entry, assuming offline", peer_time); return 0LL; } return when_online; } } /*! * \internal * \brief Unpack node attribute for user-requested fencing * * \param[in] node Node to check * \param[in] node_state Node's \c PCMK__XE_NODE_STATE entry in CIB status * * \return \c true if fencing has been requested for \p node, otherwise \c false */ static bool unpack_node_terminate(const pcmk_node_t *node, const xmlNode *node_state) { bool value_b = false; long long value_ll = 0LL; int rc = pcmk_rc_ok; const char *value_s = pcmk__node_attr(node, PCMK_NODE_ATTR_TERMINATE, NULL, pcmk__rsc_node_current); // Value may be boolean or an epoch time if ((value_s != NULL) && (pcmk__parse_bool(value_s, &value_b) == pcmk_rc_ok)) { return value_b; } rc = pcmk__scan_ll(value_s, &value_ll, 0LL); if (rc == pcmk_rc_ok) { return (value_ll > 0); } pcmk__warn("Ignoring unrecognized value '%s' for " PCMK_NODE_ATTR_TERMINATE "node attribute for %s: %s", value_s, pcmk__node_name(node), pcmk_rc_str(rc)); return false; } static gboolean determine_online_status_no_fencing(pcmk_scheduler_t *scheduler, const xmlNode *node_state, pcmk_node_t *this_node) { gboolean online = FALSE; const char *join = pcmk__xe_get(node_state, PCMK__XA_JOIN); const char *exp_state = pcmk__xe_get(node_state, PCMK_XA_EXPECTED); long long when_member = unpack_node_member(node_state, scheduler); long long when_online = unpack_node_online(node_state); if (when_member <= 0) { pcmk__trace("Node %s is %sdown", pcmk__node_name(this_node), ((when_member < 0)? "presumed " : "")); } else if (when_online > 0) { if (pcmk__str_eq(join, CRMD_JOINSTATE_MEMBER, pcmk__str_casei)) { online = TRUE; } else { pcmk__debug("Node %s is not ready to run resources: %s", pcmk__node_name(this_node), join); } } else if (!pcmk__is_set(this_node->priv->flags, pcmk__node_expected_up)) { pcmk__trace("Node %s controller is down: " "member@%lld online@%lld join=%s expected=%s", pcmk__node_name(this_node), when_member, when_online, pcmk__s(join, ""), pcmk__s(exp_state, "")); } else { /* mark it unclean */ pe_fence_node(scheduler, this_node, "peer is unexpectedly down", FALSE); pcmk__info("Node %s member@%lld online@%lld join=%s expected=%s", pcmk__node_name(this_node), when_member, when_online, pcmk__s(join, ""), pcmk__s(exp_state, "")); } return online; } /*! * \internal * \brief Check whether a node has taken too long to join controller group * * \param[in,out] scheduler Scheduler data * \param[in] node Node to check * \param[in] when_member Epoch time when node became a cluster member * \param[in] when_online Epoch time when node joined controller group * * \return true if node has been pending (on the way up) longer than * \c PCMK_OPT_NODE_PENDING_TIMEOUT, otherwise false * \note This will also update the cluster's recheck time if appropriate. */ static inline bool pending_too_long(pcmk_scheduler_t *scheduler, const pcmk_node_t *node, long long when_member, long long when_online) { if ((scheduler->priv->node_pending_ms > 0U) && (when_member > 0) && (when_online <= 0)) { // There is a timeout on pending nodes, and node is pending time_t timeout = when_member + pcmk__timeout_ms2s(scheduler->priv->node_pending_ms); if (pcmk__scheduler_epoch_time(node->priv->scheduler) >= timeout) { return true; // Node has timed out } // Node is pending, but still has time pcmk__update_recheck_time(timeout, scheduler, "pending node timeout"); } return false; } static bool determine_online_status_fencing(pcmk_scheduler_t *scheduler, const xmlNode *node_state, pcmk_node_t *this_node) { bool termination_requested = unpack_node_terminate(this_node, node_state); const char *join = pcmk__xe_get(node_state, PCMK__XA_JOIN); const char *exp_state = pcmk__xe_get(node_state, PCMK_XA_EXPECTED); long long when_member = unpack_node_member(node_state, scheduler); long long when_online = unpack_node_online(node_state); /* - PCMK__XA_JOIN ::= member|down|pending|banned - PCMK_XA_EXPECTED ::= member|down @COMPAT with entries recorded for DCs < 2.1.7 - PCMK__XA_IN_CCM ::= true|false - PCMK_XA_CRMD ::= online|offline Since crm_feature_set 3.18.0 (pacemaker-2.1.7): - PCMK__XA_IN_CCM ::= |0 Since when node has been a cluster member. A value 0 of means the node is not a cluster member. - PCMK_XA_CRMD ::= |0 Since when peer has been online in CPG. A value 0 means the peer is offline in CPG. */ pcmk__trace("Node %s member@%lld online@%lld join=%s expected=%s%s", pcmk__node_name(this_node), when_member, when_online, pcmk__s(join, ""), pcmk__s(exp_state, ""), (termination_requested? " (termination requested)" : "")); if (this_node->details->shutdown) { pcmk__debug("%s is shutting down", pcmk__node_name(this_node)); /* Slightly different criteria since we can't shut down a dead peer */ return (when_online > 0); } if (when_member < 0) { pe_fence_node(scheduler, this_node, "peer has not been seen by the cluster", FALSE); return false; } if (pcmk__str_eq(join, CRMD_JOINSTATE_NACK, pcmk__str_none)) { pe_fence_node(scheduler, this_node, "peer failed Pacemaker membership criteria", FALSE); } else if (termination_requested) { if ((when_member <= 0) && (when_online <= 0) && pcmk__str_eq(join, CRMD_JOINSTATE_DOWN, pcmk__str_none)) { pcmk__info("%s was fenced as requested", pcmk__node_name(this_node)); return false; } pe_fence_node(scheduler, this_node, "fencing was requested", false); } else if (pcmk__str_eq(exp_state, CRMD_JOINSTATE_DOWN, pcmk__str_null_matches)) { if (pending_too_long(scheduler, this_node, when_member, when_online)) { pe_fence_node(scheduler, this_node, "peer pending timed out on joining the process group", FALSE); } else if ((when_member > 0) || (when_online > 0)) { pcmk__info("- %s is not ready to run resources", pcmk__node_name(this_node)); pcmk__set_node_flags(this_node, pcmk__node_standby); this_node->details->pending = TRUE; } else { pcmk__trace("%s is down or still coming up", pcmk__node_name(this_node)); } } else if (when_member <= 0) { // Consider PCMK_OPT_PRIORITY_FENCING_DELAY for lost nodes pe_fence_node(scheduler, this_node, "peer is no longer part of the cluster", TRUE); } else if (when_online <= 0) { pe_fence_node(scheduler, this_node, "peer process is no longer available", FALSE); /* Everything is running at this point, now check join state */ } else if (pcmk__str_eq(join, CRMD_JOINSTATE_MEMBER, pcmk__str_none)) { pcmk__info("%s is active", pcmk__node_name(this_node)); } else if (pcmk__str_any_of(join, CRMD_JOINSTATE_PENDING, CRMD_JOINSTATE_DOWN, NULL)) { pcmk__info("%s is not ready to run resources", pcmk__node_name(this_node)); pcmk__set_node_flags(this_node, pcmk__node_standby); this_node->details->pending = TRUE; } else { pe_fence_node(scheduler, this_node, "peer was in an unknown state", FALSE); } return (when_member > 0); } static void determine_remote_online_status(pcmk_scheduler_t *scheduler, pcmk_node_t *this_node) { pcmk_resource_t *rsc = this_node->priv->remote; pcmk_resource_t *launcher = NULL; pcmk_node_t *host = NULL; const char *node_type = "Remote"; if (rsc == NULL) { /* This is a leftover node state entry for a former Pacemaker Remote * node whose connection resource was removed. Consider it offline. */ pcmk__trace("Pacemaker Remote node %s is considered OFFLINE because " "its connection resource has been removed from the CIB", this_node->priv->id); this_node->details->online = FALSE; return; } launcher = rsc->priv->launcher; if (launcher != NULL) { node_type = "Guest"; if (pcmk__list_of_1(rsc->priv->active_nodes)) { host = rsc->priv->active_nodes->data; } } /* If the resource is currently started, mark it online. */ if (rsc->priv->orig_role == pcmk_role_started) { this_node->details->online = TRUE; } /* consider this node shutting down if transitioning start->stop */ if ((rsc->priv->orig_role == pcmk_role_started) && (rsc->priv->next_role == pcmk_role_stopped)) { pcmk__trace("%s node %s shutting down because connection resource is " "stopping", node_type, this_node->priv->id); this_node->details->shutdown = TRUE; } /* Now check all the failure conditions. */ if ((launcher != NULL) && pcmk__is_set(launcher->flags, pcmk__rsc_failed)) { pcmk__trace("Guest node %s UNCLEAN because guest resource failed", this_node->priv->id); this_node->details->online = FALSE; pcmk__set_node_flags(this_node, pcmk__node_remote_reset); } else if (pcmk__is_set(rsc->flags, pcmk__rsc_failed)) { pcmk__trace("%s node %s OFFLINE because connection resource failed", node_type, this_node->priv->id); this_node->details->online = FALSE; } else if ((rsc->priv->orig_role == pcmk_role_stopped) || ((launcher != NULL) && (launcher->priv->orig_role == pcmk_role_stopped))) { pcmk__trace("%s node %s OFFLINE because its resource is stopped", node_type, this_node->priv->id); this_node->details->online = FALSE; pcmk__clear_node_flags(this_node, pcmk__node_remote_reset); } else if (host && (host->details->online == FALSE) && host->details->unclean) { pcmk__trace("Guest node %s UNCLEAN because host is unclean", this_node->priv->id); this_node->details->online = FALSE; pcmk__set_node_flags(this_node, pcmk__node_remote_reset); } else { pcmk__trace("%s node %s is %s", node_type, this_node->priv->id, (this_node->details->online? "ONLINE" : "OFFLINE")); } } static void determine_online_status(const xmlNode *node_state, pcmk_node_t *this_node, pcmk_scheduler_t *scheduler) { gboolean online = FALSE; const char *exp_state = pcmk__xe_get(node_state, PCMK_XA_EXPECTED); CRM_CHECK(this_node != NULL, return); this_node->details->shutdown = FALSE; if (pe__shutdown_requested(this_node)) { this_node->details->shutdown = TRUE; } else if (pcmk__str_eq(exp_state, CRMD_JOINSTATE_MEMBER, pcmk__str_casei)) { pcmk__set_node_flags(this_node, pcmk__node_expected_up); } if (!pcmk__is_set(scheduler->flags, pcmk__sched_fencing_enabled)) { online = determine_online_status_no_fencing(scheduler, node_state, this_node); } else { online = determine_online_status_fencing(scheduler, node_state, this_node); } if (online) { this_node->details->online = TRUE; } else { /* remove node from contention */ this_node->assign->score = -PCMK_SCORE_INFINITY; } if (online && this_node->details->shutdown) { /* don't run resources here */ this_node->assign->score = -PCMK_SCORE_INFINITY; } if (this_node->details->unclean) { pcmk__sched_warn(scheduler, "%s is unclean", pcmk__node_name(this_node)); } else if (!this_node->details->online) { pcmk__trace("%s is offline", pcmk__node_name(this_node)); } else if (this_node->details->shutdown) { pcmk__info("%s is shutting down", pcmk__node_name(this_node)); } else if (this_node->details->pending) { pcmk__info("%s is pending", pcmk__node_name(this_node)); } else if (pcmk__is_set(this_node->priv->flags, pcmk__node_standby)) { pcmk__info("%s is in standby", pcmk__node_name(this_node)); } else if (this_node->details->maintenance) { pcmk__info("%s is in maintenance", pcmk__node_name(this_node)); } else { pcmk__info("%s is online", pcmk__node_name(this_node)); } } /*! * \internal * \brief Find the end of a resource's name, excluding any clone suffix * * \param[in] id Resource ID to check * * \return Pointer to last character of resource's base name */ const char * pe_base_name_end(const char *id) { if (!pcmk__str_empty(id)) { const char *end = id + strlen(id) - 1; for (const char *s = end; s > id; --s) { switch (*s) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': break; case ':': return (s == end)? s : (s - 1); default: return end; } } return end; } return NULL; } /*! * \internal * \brief Get a resource name excluding any clone suffix * * \param[in] last_rsc_id Resource ID to check * * \return Pointer to newly allocated string with resource's base name * \note It is the caller's responsibility to free() the result. * This asserts on error, so callers can assume result is not NULL. */ char * clone_strip(const char *last_rsc_id) { const char *end = pe_base_name_end(last_rsc_id); char *basename = NULL; pcmk__assert(end != NULL); basename = strndup(last_rsc_id, end - last_rsc_id + 1); pcmk__assert(basename != NULL); return basename; } /*! * \internal * \brief Get the name of the first instance of a cloned resource * * \param[in] last_rsc_id Resource ID to check * * \return Pointer to newly allocated string with resource's base name plus :0 * \note It is the caller's responsibility to free() the result. * This asserts on error, so callers can assume result is not NULL. */ char * clone_zero(const char *last_rsc_id) { const char *end = pe_base_name_end(last_rsc_id); size_t base_name_len = end - last_rsc_id + 1; char *zero = NULL; pcmk__assert(end != NULL); zero = pcmk__assert_alloc(base_name_len + 3, sizeof(char)); memcpy(zero, last_rsc_id, base_name_len); zero[base_name_len] = ':'; zero[base_name_len + 1] = '0'; return zero; } static pcmk_resource_t * create_fake_resource(const char *rsc_id, const xmlNode *rsc_entry, pcmk_scheduler_t *scheduler) { pcmk_resource_t *rsc = NULL; xmlNode *xml_rsc = pcmk__xe_create(NULL, PCMK_XE_PRIMITIVE); pcmk__xe_copy_attrs(xml_rsc, rsc_entry, pcmk__xaf_none); pcmk__xe_set(xml_rsc, PCMK_XA_ID, rsc_id); - crm_log_xml_debug(xml_rsc, "Orphan resource"); + pcmk__log_xml_debug(xml_rsc, "Orphan resource"); if (pe__unpack_resource(xml_rsc, &rsc, NULL, scheduler) != pcmk_rc_ok) { return NULL; } if (xml_contains_remote_node(xml_rsc)) { pcmk_node_t *node; pcmk__debug("Detected orphaned remote node %s", rsc_id); node = pcmk_find_node(scheduler, rsc_id); if (node == NULL) { node = pe_create_node(rsc_id, rsc_id, PCMK_VALUE_REMOTE, 0, scheduler); } link_rsc2remotenode(scheduler, rsc); if (node) { pcmk__trace("Setting node %s as shutting down due to orphaned " "connection resource", rsc_id); node->details->shutdown = TRUE; } } if (pcmk__xe_get(rsc_entry, PCMK__META_CONTAINER)) { // This removed resource needs to be mapped to a launcher pcmk__trace("Launched resource %s was removed from the configuration", rsc_id); pcmk__set_rsc_flags(rsc, pcmk__rsc_removed_launched); } pcmk__set_rsc_flags(rsc, pcmk__rsc_removed); scheduler->priv->resources = g_list_append(scheduler->priv->resources, rsc); return rsc; } /*! * \internal * \brief Create orphan instance for anonymous clone resource history * * \param[in,out] parent Clone resource that orphan will be added to * \param[in] rsc_id Orphan's resource ID * \param[in] node Where orphan is active (for logging only) * \param[in,out] scheduler Scheduler data * * \return Newly added orphaned instance of \p parent */ static pcmk_resource_t * create_anonymous_orphan(pcmk_resource_t *parent, const char *rsc_id, const pcmk_node_t *node, pcmk_scheduler_t *scheduler) { pcmk_resource_t *top = pe__create_clone_child(parent, scheduler); pcmk_resource_t *orphan = NULL; // find_rsc() because we might be a cloned group orphan = top->priv->fns->find_rsc(top, rsc_id, NULL, pcmk_rsc_match_clone_only); pcmk__rsc_debug(parent, "Created orphan %s for %s: %s on %s", top->id, parent->id, rsc_id, pcmk__node_name(node)); return orphan; } /*! * \internal * \brief Check a node for an instance of an anonymous clone * * Return a child instance of the specified anonymous clone, in order of * preference: (1) the instance running on the specified node, if any; * (2) an inactive instance (i.e. within the total of \c PCMK_META_CLONE_MAX * instances); (3) a newly created orphan (that is, \c PCMK_META_CLONE_MAX * instances are already active). * * \param[in,out] scheduler Scheduler data * \param[in] node Node on which to check for instance * \param[in,out] parent Clone to check * \param[in] rsc_id Name of cloned resource in history (no instance) */ static pcmk_resource_t * find_anonymous_clone(pcmk_scheduler_t *scheduler, const pcmk_node_t *node, pcmk_resource_t *parent, const char *rsc_id) { GList *rIter = NULL; pcmk_resource_t *rsc = NULL; pcmk_resource_t *inactive_instance = NULL; gboolean skip_inactive = FALSE; pcmk__assert(pcmk__is_anonymous_clone(parent)); // Check for active (or partially active, for cloned groups) instance pcmk__rsc_trace(parent, "Looking for %s on %s in %s", rsc_id, pcmk__node_name(node), parent->id); for (rIter = parent->priv->children; (rIter != NULL) && (rsc == NULL); rIter = rIter->next) { GList *locations = NULL; pcmk_resource_t *child = rIter->data; /* Check whether this instance is already known to be active or pending * anywhere, at this stage of unpacking. Because this function is called * for a resource before the resource's individual operation history * entries are unpacked, locations will generally not contain the * desired node. * * However, there are three exceptions: * (1) when child is a cloned group and we have already unpacked the * history of another member of the group on the same node; * (2) when we've already unpacked the history of another numbered * instance on the same node (which can happen if * PCMK_META_GLOBALLY_UNIQUE was flipped from true to false); and * (3) when we re-run calculations on the same scheduler data as part of * a simulation. */ child->priv->fns->location(child, &locations, pcmk__rsc_node_current |pcmk__rsc_node_pending); if (locations) { /* We should never associate the same numbered anonymous clone * instance with multiple nodes, and clone instances can't migrate, * so there must be only one location, regardless of history. */ CRM_LOG_ASSERT(locations->next == NULL); if (pcmk__same_node((pcmk_node_t *) locations->data, node)) { /* This child instance is active on the requested node, so check * for a corresponding configured resource. We use find_rsc() * instead of child because child may be a cloned group, and we * need the particular member corresponding to rsc_id. * * If the history entry is orphaned, rsc will be NULL. */ rsc = parent->priv->fns->find_rsc(child, rsc_id, NULL, pcmk_rsc_match_clone_only); if (rsc) { /* If there are multiple instance history entries for an * anonymous clone in a single node's history (which can * happen if PCMK_META_GLOBALLY_UNIQUE is switched from true * to false), we want to consider the instances beyond the * first as orphans, even if there are inactive instance * numbers available. */ if (rsc->priv->active_nodes != NULL) { pcmk__notice("Active (now-)anonymous clone %s has " "multiple (orphan) instance histories on " "%s", parent->id, pcmk__node_name(node)); skip_inactive = TRUE; rsc = NULL; } else { pcmk__rsc_trace(parent, "Resource %s, active", rsc->id); } } } g_list_free(locations); } else { pcmk__rsc_trace(parent, "Resource %s, skip inactive", child->id); if (!skip_inactive && !inactive_instance && !pcmk__is_set(child->flags, pcmk__rsc_blocked)) { // Remember one inactive instance in case we don't find active inactive_instance = parent->priv->fns->find_rsc(child, rsc_id, NULL, pcmk_rsc_match_clone_only); /* ... but don't use it if it was already associated with a * pending action on another node */ if (inactive_instance != NULL) { const pcmk_node_t *pending_node = NULL; pending_node = inactive_instance->priv->pending_node; if ((pending_node != NULL) && !pcmk__same_node(pending_node, node)) { inactive_instance = NULL; } } } } } if ((rsc == NULL) && !skip_inactive && (inactive_instance != NULL)) { pcmk__rsc_trace(parent, "Resource %s, empty slot", inactive_instance->id); rsc = inactive_instance; } /* If the resource has PCMK_META_REQUIRES set to PCMK_VALUE_QUORUM or * PCMK_VALUE_NOTHING, and we don't have a clone instance for every node, we * don't want to consume a valid instance number for unclean nodes. Such * instances may appear to be active according to the history, but should be * considered inactive, so we can start an instance elsewhere. Treat such * instances as orphans. * * An exception is instances running on guest nodes -- since guest node * "fencing" is actually just a resource stop, requires shouldn't apply. * * @TODO Ideally, we'd use an inactive instance number if it is not needed * for any clean instances. However, we don't know that at this point. */ if ((rsc != NULL) && !pcmk__is_set(rsc->flags, pcmk__rsc_needs_fencing) && (!node->details->online || node->details->unclean) && !pcmk__is_guest_or_bundle_node(node) && !pe__is_universal_clone(parent, scheduler)) { rsc = NULL; } if (rsc == NULL) { rsc = create_anonymous_orphan(parent, rsc_id, node, scheduler); pcmk__rsc_trace(parent, "Resource %s, orphan", rsc->id); } return rsc; } static pcmk_resource_t * unpack_find_resource(pcmk_scheduler_t *scheduler, const pcmk_node_t *node, const char *rsc_id) { pcmk_resource_t *rsc = NULL; pcmk_resource_t *parent = NULL; pcmk__trace("looking for %s", rsc_id); rsc = pe_find_resource(scheduler->priv->resources, rsc_id); if (rsc == NULL) { /* If we didn't find the resource by its name in the operation history, * check it again as a clone instance. Even when PCMK_META_CLONE_MAX=0, * we create a single :0 orphan to match against here. */ char *clone0_id = clone_zero(rsc_id); pcmk_resource_t *clone0 = pe_find_resource(scheduler->priv->resources, clone0_id); if ((clone0 != NULL) && !pcmk__is_set(clone0->flags, pcmk__rsc_unique)) { rsc = clone0; parent = uber_parent(clone0); pcmk__trace("%s found as %s (%s)", rsc_id, clone0_id, parent->id); } else { pcmk__trace("%s is not known as %s either (orphan)", rsc_id, clone0_id); } free(clone0_id); } else if (rsc->priv->variant > pcmk__rsc_variant_primitive) { pcmk__trace("Resource history for %s is orphaned " "because it is no longer primitive", rsc_id); return NULL; } else { parent = uber_parent(rsc); } if (pcmk__is_anonymous_clone(parent)) { if (pcmk__is_bundled(parent)) { rsc = pe__find_bundle_replica(parent->priv->parent, node); } else { char *base = clone_strip(rsc_id); rsc = find_anonymous_clone(scheduler, node, parent, base); free(base); pcmk__assert(rsc != NULL); } } if (rsc && !pcmk__str_eq(rsc_id, rsc->id, pcmk__str_none) && !pcmk__str_eq(rsc_id, rsc->priv->history_id, pcmk__str_none)) { const bool removed = pcmk__is_set(rsc->flags, pcmk__rsc_removed); pcmk__str_update(&(rsc->priv->history_id), rsc_id); pcmk__rsc_debug(rsc, "Internally renamed %s on %s to %s%s", rsc_id, pcmk__node_name(node), rsc->id, (removed? " (ORPHAN)" : "")); } return rsc; } static pcmk_resource_t * process_orphan_resource(const xmlNode *rsc_entry, const pcmk_node_t *node, pcmk_scheduler_t *scheduler) { pcmk_resource_t *rsc = NULL; const char *rsc_id = pcmk__xe_get(rsc_entry, PCMK_XA_ID); pcmk__debug("Detected orphan resource %s on %s", rsc_id, pcmk__node_name(node)); rsc = create_fake_resource(rsc_id, rsc_entry, scheduler); if (rsc == NULL) { return NULL; } if (!pcmk__is_set(scheduler->flags, pcmk__sched_stop_removed_resources)) { pcmk__clear_rsc_flags(rsc, pcmk__rsc_managed); } else { CRM_CHECK(rsc != NULL, return NULL); pcmk__rsc_trace(rsc, "Added orphan %s", rsc->id); resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__orphan_do_not_run__", scheduler); } return rsc; } static void process_rsc_state(pcmk_resource_t *rsc, pcmk_node_t *node, enum pcmk__on_fail on_fail) { pcmk_node_t *tmpnode = NULL; char *reason = NULL; enum pcmk__on_fail save_on_fail = pcmk__on_fail_ignore; pcmk_scheduler_t *scheduler = NULL; bool known_active = false; pcmk__assert(rsc != NULL); scheduler = rsc->priv->scheduler; known_active = (rsc->priv->orig_role > pcmk_role_stopped); pcmk__rsc_trace(rsc, "Resource %s is %s on %s: on_fail=%s", rsc->id, pcmk_role_text(rsc->priv->orig_role), pcmk__node_name(node), pcmk__on_fail_text(on_fail)); /* process current state */ if (rsc->priv->orig_role != pcmk_role_unknown) { pcmk_resource_t *iter = rsc; while (iter) { if (g_hash_table_lookup(iter->priv->probed_nodes, node->priv->id) == NULL) { pcmk_node_t *n = pe__copy_node(node); pcmk__rsc_trace(rsc, "%s (%s in history) known on %s", rsc->id, pcmk__s(rsc->priv->history_id, "the same"), pcmk__node_name(n)); g_hash_table_insert(iter->priv->probed_nodes, (gpointer) n->priv->id, n); } if (pcmk__is_set(iter->flags, pcmk__rsc_unique)) { break; } iter = iter->priv->parent; } } /* If a managed resource is believed to be running, but node is down ... */ if (known_active && !node->details->online && !node->details->maintenance && pcmk__is_set(rsc->flags, pcmk__rsc_managed)) { gboolean should_fence = FALSE; /* If this is a guest node, fence it (regardless of whether fencing is * enabled, because guest node fencing is done by recovery of the * container resource rather than by the fencer). Mark the resource * we're processing as failed. When the guest comes back up, its * operation history in the CIB will be cleared, freeing the affected * resource to run again once we are sure we know its state. */ if (pcmk__is_guest_or_bundle_node(node)) { pcmk__set_rsc_flags(rsc, pcmk__rsc_failed|pcmk__rsc_stop_if_failed); should_fence = TRUE; } else if (pcmk__is_set(scheduler->flags, pcmk__sched_fencing_enabled)) { if (pcmk__is_remote_node(node) && (node->priv->remote != NULL) && !pcmk__is_set(node->priv->remote->flags, pcmk__rsc_failed)) { /* Setting unseen means that fencing of the remote node will * occur only if the connection resource is not going to start * somewhere. This allows connection resources on a failed * cluster node to move to another node without requiring the * remote nodes to be fenced as well. */ pcmk__clear_node_flags(node, pcmk__node_seen); reason = pcmk__assert_asprintf("%s is active there (fencing " "will be revoked if remote " "connection can be " "re-established elsewhere)", rsc->id); } should_fence = TRUE; } if (should_fence) { if (reason == NULL) { reason = pcmk__assert_asprintf("%s is thought to be active " "there", rsc->id); } pe_fence_node(scheduler, node, reason, FALSE); } free(reason); } /* In order to calculate priority_fencing_delay correctly, save the failure information and pass it to native_add_running(). */ save_on_fail = on_fail; if (node->details->unclean) { /* No extra processing needed * Also allows resources to be started again after a node is shot */ on_fail = pcmk__on_fail_ignore; } switch (on_fail) { case pcmk__on_fail_ignore: /* nothing to do */ break; case pcmk__on_fail_demote: pcmk__set_rsc_flags(rsc, pcmk__rsc_failed); demote_action(rsc, node, FALSE); break; case pcmk__on_fail_fence_node: /* treat it as if it is still running * but also mark the node as unclean */ reason = pcmk__assert_asprintf("%s failed there", rsc->id); pe_fence_node(scheduler, node, reason, FALSE); free(reason); break; case pcmk__on_fail_standby_node: pcmk__set_node_flags(node, pcmk__node_standby|pcmk__node_fail_standby); break; case pcmk__on_fail_block: /* is_managed == FALSE will prevent any * actions being sent for the resource */ pcmk__clear_rsc_flags(rsc, pcmk__rsc_managed); pcmk__set_rsc_flags(rsc, pcmk__rsc_blocked); break; case pcmk__on_fail_ban: /* make sure it comes up somewhere else * or not at all */ resource_location(rsc, node, -PCMK_SCORE_INFINITY, "__action_migration_auto__", scheduler); break; case pcmk__on_fail_stop: pe__set_next_role(rsc, pcmk_role_stopped, PCMK_META_ON_FAIL "=" PCMK_VALUE_STOP); break; case pcmk__on_fail_restart: if (known_active) { pcmk__set_rsc_flags(rsc, pcmk__rsc_failed|pcmk__rsc_stop_if_failed); stop_action(rsc, node, FALSE); } break; case pcmk__on_fail_restart_container: pcmk__set_rsc_flags(rsc, pcmk__rsc_failed|pcmk__rsc_stop_if_failed); if ((rsc->priv->launcher != NULL) && pcmk__is_bundled(rsc)) { /* A bundle's remote connection can run on a different node than * the bundle's container. We don't necessarily know where the * container is running yet, so remember it and add a stop * action for it later. */ scheduler->priv->stop_needed = g_list_prepend(scheduler->priv->stop_needed, rsc->priv->launcher); } else if (rsc->priv->launcher != NULL) { stop_action(rsc->priv->launcher, node, FALSE); } else if (known_active) { stop_action(rsc, node, FALSE); } break; case pcmk__on_fail_reset_remote: pcmk__set_rsc_flags(rsc, pcmk__rsc_failed|pcmk__rsc_stop_if_failed); if (pcmk__is_set(scheduler->flags, pcmk__sched_fencing_enabled)) { tmpnode = NULL; if (pcmk__is_set(rsc->flags, pcmk__rsc_is_remote_connection)) { tmpnode = pcmk_find_node(scheduler, rsc->id); } if (pcmk__is_remote_node(tmpnode) && !pcmk__is_set(tmpnode->priv->flags, pcmk__node_remote_fenced)) { /* The remote connection resource failed in a way that * should result in fencing the remote node. */ pe_fence_node(scheduler, tmpnode, "remote connection is unrecoverable", FALSE); } } /* require the stop action regardless if fencing is occurring or not. */ if (known_active) { stop_action(rsc, node, FALSE); } /* if reconnect delay is in use, prevent the connection from exiting the * "STOPPED" role until the failure is cleared by the delay timeout. */ if (rsc->priv->remote_reconnect_ms > 0U) { pe__set_next_role(rsc, pcmk_role_stopped, "remote reset"); } break; } /* Ensure a remote connection failure forces an unclean Pacemaker Remote * node to be fenced. By marking the node as seen, the failure will result * in a fencing operation regardless if we're going to attempt to reconnect * in this transition. */ if (pcmk__all_flags_set(rsc->flags, pcmk__rsc_failed|pcmk__rsc_is_remote_connection)) { tmpnode = pcmk_find_node(scheduler, rsc->id); if (tmpnode && tmpnode->details->unclean) { pcmk__set_node_flags(tmpnode, pcmk__node_seen); } } if (known_active) { if (pcmk__is_set(rsc->flags, pcmk__rsc_removed)) { if (pcmk__is_set(rsc->flags, pcmk__rsc_managed)) { pcmk__notice("Removed resource %s is active on %s and will be " "stopped when possible", rsc->id, pcmk__node_name(node)); } else { pcmk__notice("Removed resource %s must be stopped manually on " "%s because " PCMK_OPT_STOP_ORPHAN_RESOURCES " is " "set to false", rsc->id, pcmk__node_name(node)); } } native_add_running(rsc, node, scheduler, (save_on_fail != pcmk__on_fail_ignore)); switch (on_fail) { case pcmk__on_fail_ignore: break; case pcmk__on_fail_demote: case pcmk__on_fail_block: pcmk__set_rsc_flags(rsc, pcmk__rsc_failed); break; default: pcmk__set_rsc_flags(rsc, pcmk__rsc_failed|pcmk__rsc_stop_if_failed); break; } } else if ((rsc->priv->history_id != NULL) && (strchr(rsc->priv->history_id, ':') != NULL)) { /* @COMPAT This is for older (<1.1.8) status sections that included * instance numbers, otherwise stopped instances are considered orphans. * * @TODO We should be able to drop this, but some old regression tests * will need to be updated. Double-check that this is not still needed * for unique clones (which may have been later converted to anonymous). */ pcmk__rsc_trace(rsc, "Clearing history ID %s for %s (stopped)", rsc->priv->history_id, rsc->id); free(rsc->priv->history_id); rsc->priv->history_id = NULL; } else { GList *possible_matches = pe__resource_actions(rsc, node, PCMK_ACTION_STOP, FALSE); GList *gIter = possible_matches; for (; gIter != NULL; gIter = gIter->next) { pcmk_action_t *stop = (pcmk_action_t *) gIter->data; pcmk__set_action_flags(stop, pcmk__action_optional); } g_list_free(possible_matches); } /* A successful stop after migrate_to on the migration source doesn't make * the partially migrated resource stopped on the migration target. */ if ((rsc->priv->orig_role == pcmk_role_stopped) && (rsc->priv->active_nodes != NULL) && (rsc->priv->partial_migration_target != NULL) && pcmk__same_node(rsc->priv->partial_migration_source, node)) { rsc->priv->orig_role = pcmk_role_started; } } /* create active recurring operations as optional */ static void process_recurring(pcmk_node_t *node, pcmk_resource_t *rsc, int start_index, int stop_index, GList *sorted_op_list, pcmk_scheduler_t *scheduler) { int counter = -1; const char *task = NULL; const char *status = NULL; GList *gIter = sorted_op_list; pcmk__assert(rsc != NULL); pcmk__rsc_trace(rsc, "%s: Start index %d, stop index = %d", rsc->id, start_index, stop_index); for (; gIter != NULL; gIter = gIter->next) { xmlNode *rsc_op = (xmlNode *) gIter->data; guint interval_ms = 0; char *key = NULL; const char *id = pcmk__xe_id(rsc_op); counter++; if (node->details->online == FALSE) { pcmk__rsc_trace(rsc, "Skipping %s on %s: node is offline", rsc->id, pcmk__node_name(node)); break; /* Need to check if there's a monitor for role="Stopped" */ } else if (start_index < stop_index && counter <= stop_index) { pcmk__rsc_trace(rsc, "Skipping %s on %s: resource is not active", id, pcmk__node_name(node)); continue; } else if (counter < start_index) { pcmk__rsc_trace(rsc, "Skipping %s on %s: old %d", id, pcmk__node_name(node), counter); continue; } pcmk__xe_get_guint(rsc_op, PCMK_META_INTERVAL, &interval_ms); if (interval_ms == 0) { pcmk__rsc_trace(rsc, "Skipping %s on %s: non-recurring", id, pcmk__node_name(node)); continue; } status = pcmk__xe_get(rsc_op, PCMK__XA_OP_STATUS); if (pcmk__str_eq(status, "-1", pcmk__str_casei)) { pcmk__rsc_trace(rsc, "Skipping %s on %s: status", id, pcmk__node_name(node)); continue; } task = pcmk__xe_get(rsc_op, PCMK_XA_OPERATION); /* create the action */ key = pcmk__op_key(rsc->id, task, interval_ms); pcmk__rsc_trace(rsc, "Creating %s on %s", key, pcmk__node_name(node)); custom_action(rsc, key, task, node, TRUE, scheduler); } } void calculate_active_ops(const GList *sorted_op_list, int *start_index, int *stop_index) { int counter = -1; int implied_monitor_start = -1; int implied_clone_start = -1; const char *task = NULL; const char *status = NULL; *stop_index = -1; *start_index = -1; for (const GList *iter = sorted_op_list; iter != NULL; iter = iter->next) { const xmlNode *rsc_op = (const xmlNode *) iter->data; counter++; task = pcmk__xe_get(rsc_op, PCMK_XA_OPERATION); status = pcmk__xe_get(rsc_op, PCMK__XA_OP_STATUS); if (pcmk__str_eq(task, PCMK_ACTION_STOP, pcmk__str_casei) && pcmk__str_eq(status, "0", pcmk__str_casei)) { *stop_index = counter; } else if (pcmk__strcase_any_of(task, PCMK_ACTION_START, PCMK_ACTION_MIGRATE_FROM, NULL)) { *start_index = counter; } else if ((implied_monitor_start <= *stop_index) && pcmk__str_eq(task, PCMK_ACTION_MONITOR, pcmk__str_casei)) { const char *rc = pcmk__xe_get(rsc_op, PCMK__XA_RC_CODE); if (pcmk__strcase_any_of(rc, "0", "8", NULL)) { implied_monitor_start = counter; } } else if (pcmk__strcase_any_of(task, PCMK_ACTION_PROMOTE, PCMK_ACTION_DEMOTE, NULL)) { implied_clone_start = counter; } } if (*start_index == -1) { if (implied_clone_start != -1) { *start_index = implied_clone_start; } else if (implied_monitor_start != -1) { *start_index = implied_monitor_start; } } } // If resource history entry has shutdown lock, remember lock node and time static void unpack_shutdown_lock(const xmlNode *rsc_entry, pcmk_resource_t *rsc, const pcmk_node_t *node, pcmk_scheduler_t *scheduler) { time_t lock_time = 0; // When lock started (i.e. node shutdown time) time_t sched_time = 0; guint shutdown_lock_ms = scheduler->priv->shutdown_lock_ms; pcmk__xe_get_time(rsc_entry, PCMK_OPT_SHUTDOWN_LOCK, &lock_time); if (lock_time == 0) { return; } sched_time = pcmk__scheduler_epoch_time(scheduler); if ((shutdown_lock_ms > 0U) && (sched_time > (lock_time + pcmk__timeout_ms2s(shutdown_lock_ms)))) { pcmk__rsc_info(rsc, "Shutdown lock for %s on %s expired", rsc->id, pcmk__node_name(node)); pe__clear_resource_history(rsc, node); } else { rsc->priv->lock_node = node; rsc->priv->lock_time = lock_time; } } /*! * \internal * \brief Unpack one \c PCMK__XE_LRM_RESOURCE entry from a node's CIB status * * \param[in,out] node Node whose status is being unpacked * \param[in] rsc_entry \c PCMK__XE_LRM_RESOURCE XML being unpacked * \param[in,out] scheduler Scheduler data * * \return Resource corresponding to the entry, or NULL if no operation history */ static pcmk_resource_t * unpack_lrm_resource(pcmk_node_t *node, const xmlNode *lrm_resource, pcmk_scheduler_t *scheduler) { GList *gIter = NULL; int stop_index = -1; int start_index = -1; enum rsc_role_e req_role = pcmk_role_unknown; const char *rsc_id = pcmk__xe_id(lrm_resource); pcmk_resource_t *rsc = NULL; GList *op_list = NULL; GList *sorted_op_list = NULL; xmlNode *rsc_op = NULL; xmlNode *last_failure = NULL; enum pcmk__on_fail on_fail = pcmk__on_fail_ignore; enum rsc_role_e saved_role = pcmk_role_unknown; if (rsc_id == NULL) { pcmk__config_err("Ignoring invalid " PCMK__XE_LRM_RESOURCE " entry: No " PCMK_XA_ID); pcmk__log_xml_info(lrm_resource, "missing-id"); return NULL; } pcmk__trace("Unpacking " PCMK__XE_LRM_RESOURCE " for %s on %s", rsc_id, pcmk__node_name(node)); /* Build a list of individual PCMK__XE_LRM_RSC_OP entries, so we can sort * them */ for (rsc_op = pcmk__xe_first_child(lrm_resource, PCMK__XE_LRM_RSC_OP, NULL, NULL); rsc_op != NULL; rsc_op = pcmk__xe_next(rsc_op, PCMK__XE_LRM_RSC_OP)) { op_list = g_list_prepend(op_list, rsc_op); } if (!pcmk__is_set(scheduler->flags, pcmk__sched_shutdown_lock)) { if (op_list == NULL) { // If there are no operations, there is nothing to do return NULL; } } /* find the resource */ rsc = unpack_find_resource(scheduler, node, rsc_id); if (rsc == NULL) { if (op_list == NULL) { // If there are no operations, there is nothing to do return NULL; } else { rsc = process_orphan_resource(lrm_resource, node, scheduler); } } pcmk__assert(rsc != NULL); // Check whether the resource is "shutdown-locked" to this node if (pcmk__is_set(scheduler->flags, pcmk__sched_shutdown_lock)) { unpack_shutdown_lock(lrm_resource, rsc, node, scheduler); } /* process operations */ saved_role = rsc->priv->orig_role; rsc->priv->orig_role = pcmk_role_unknown; sorted_op_list = g_list_sort(op_list, sort_op_by_callid); for (gIter = sorted_op_list; gIter != NULL; gIter = gIter->next) { xmlNode *rsc_op = (xmlNode *) gIter->data; unpack_rsc_op(rsc, node, rsc_op, &last_failure, &on_fail); } /* create active recurring operations as optional */ calculate_active_ops(sorted_op_list, &start_index, &stop_index); process_recurring(node, rsc, start_index, stop_index, sorted_op_list, scheduler); /* no need to free the contents */ g_list_free(sorted_op_list); process_rsc_state(rsc, node, on_fail); if (get_target_role(rsc, &req_role)) { if ((rsc->priv->next_role == pcmk_role_unknown) || (req_role < rsc->priv->next_role)) { pe__set_next_role(rsc, req_role, PCMK_META_TARGET_ROLE); } else if (req_role > rsc->priv->next_role) { pcmk__rsc_info(rsc, "%s: Not overwriting calculated next role %s" " with requested next role %s", rsc->id, pcmk_role_text(rsc->priv->next_role), pcmk_role_text(req_role)); } } if (saved_role > rsc->priv->orig_role) { rsc->priv->orig_role = saved_role; } return rsc; } static void handle_removed_launched_resources(const xmlNode *lrm_rsc_list, pcmk_scheduler_t *scheduler) { for (const xmlNode *rsc_entry = pcmk__xe_first_child(lrm_rsc_list, PCMK__XE_LRM_RESOURCE, NULL, NULL); rsc_entry != NULL; rsc_entry = pcmk__xe_next(rsc_entry, PCMK__XE_LRM_RESOURCE)) { pcmk_resource_t *rsc; pcmk_resource_t *launcher = NULL; const char *rsc_id; const char *launcher_id = NULL; launcher_id = pcmk__xe_get(rsc_entry, PCMK__META_CONTAINER); rsc_id = pcmk__xe_get(rsc_entry, PCMK_XA_ID); if ((launcher_id == NULL) || (rsc_id == NULL)) { continue; } launcher = pe_find_resource(scheduler->priv->resources, launcher_id); if (launcher == NULL) { continue; } rsc = pe_find_resource(scheduler->priv->resources, rsc_id); if ((rsc == NULL) || (rsc->priv->launcher != NULL) || !pcmk__is_set(rsc->flags, pcmk__rsc_removed_launched)) { continue; } pcmk__rsc_trace(rsc, "Mapped launcher of removed resource %s to %s", rsc->id, launcher_id); rsc->priv->launcher = launcher; launcher->priv->launched = g_list_append(launcher->priv->launched, rsc); } } /*! * \internal * \brief Unpack one node's lrm status section * * \param[in,out] node Node whose status is being unpacked * \param[in] xml CIB node state XML * \param[in,out] scheduler Scheduler data */ static void unpack_node_lrm(pcmk_node_t *node, const xmlNode *xml, pcmk_scheduler_t *scheduler) { bool found_removed_launched_resource = false; // Drill down to PCMK__XE_LRM_RESOURCES section xml = pcmk__xe_first_child(xml, PCMK__XE_LRM, NULL, NULL); if (xml == NULL) { return; } xml = pcmk__xe_first_child(xml, PCMK__XE_LRM_RESOURCES, NULL, NULL); if (xml == NULL) { return; } // Unpack each PCMK__XE_LRM_RESOURCE entry for (const xmlNode *rsc_entry = pcmk__xe_first_child(xml, PCMK__XE_LRM_RESOURCE, NULL, NULL); rsc_entry != NULL; rsc_entry = pcmk__xe_next(rsc_entry, PCMK__XE_LRM_RESOURCE)) { pcmk_resource_t *rsc = unpack_lrm_resource(node, rsc_entry, scheduler); if ((rsc != NULL) && pcmk__is_set(rsc->flags, pcmk__rsc_removed_launched)) { found_removed_launched_resource = true; } } /* Now that all resource state has been unpacked for this node, map any * removed launched resources to their launchers. */ if (found_removed_launched_resource) { handle_removed_launched_resources(xml, scheduler); } } static void set_active(pcmk_resource_t *rsc) { const pcmk_resource_t *top = pe__const_top_resource(rsc, false); if ((top != NULL) && pcmk__is_set(top->flags, pcmk__rsc_promotable)) { rsc->priv->orig_role = pcmk_role_unpromoted; } else { rsc->priv->orig_role = pcmk_role_started; } } static void set_node_score(gpointer key, gpointer value, gpointer user_data) { pcmk_node_t *node = value; int *score = user_data; node->assign->score = *score; } #define XPATH_NODE_STATE "/" PCMK_XE_CIB "/" PCMK_XE_STATUS \ "/" PCMK__XE_NODE_STATE #define SUB_XPATH_LRM_RESOURCE "/" PCMK__XE_LRM \ "/" PCMK__XE_LRM_RESOURCES \ "/" PCMK__XE_LRM_RESOURCE #define SUB_XPATH_LRM_RSC_OP "/" PCMK__XE_LRM_RSC_OP static xmlNode * find_lrm_op(const char *resource, const char *op, const char *node, const char *source, int target_rc, pcmk_scheduler_t *scheduler) { GString *xpath = NULL; xmlNode *xml = NULL; CRM_CHECK((resource != NULL) && (op != NULL) && (node != NULL), return NULL); xpath = g_string_sized_new(256); pcmk__g_strcat(xpath, XPATH_NODE_STATE "[@" PCMK_XA_UNAME "='", node, "']" SUB_XPATH_LRM_RESOURCE "[@" PCMK_XA_ID "='", resource, "']" SUB_XPATH_LRM_RSC_OP "[@" PCMK_XA_OPERATION "='", op, "'", NULL); /* Need to check against transition_magic too? */ if ((source != NULL) && (strcmp(op, PCMK_ACTION_MIGRATE_TO) == 0)) { pcmk__g_strcat(xpath, " and @" PCMK__META_MIGRATE_TARGET "='", source, "']", NULL); } else if ((source != NULL) && (strcmp(op, PCMK_ACTION_MIGRATE_FROM) == 0)) { pcmk__g_strcat(xpath, " and @" PCMK__META_MIGRATE_SOURCE "='", source, "']", NULL); } else { g_string_append_c(xpath, ']'); } xml = pcmk__xpath_find_one(scheduler->input->doc, xpath->str, LOG_DEBUG); g_string_free(xpath, TRUE); if (xml && target_rc >= 0) { int rc = PCMK_OCF_UNKNOWN_ERROR; int status = PCMK_EXEC_ERROR; pcmk__xe_get_int(xml, PCMK__XA_RC_CODE, &rc); pcmk__xe_get_int(xml, PCMK__XA_OP_STATUS, &status); if ((rc != target_rc) || (status != PCMK_EXEC_DONE)) { return NULL; } } return xml; } static xmlNode * find_lrm_resource(const char *rsc_id, const char *node_name, pcmk_scheduler_t *scheduler) { GString *xpath = NULL; xmlNode *xml = NULL; CRM_CHECK((rsc_id != NULL) && (node_name != NULL), return NULL); xpath = g_string_sized_new(256); pcmk__g_strcat(xpath, XPATH_NODE_STATE "[@" PCMK_XA_UNAME "='", node_name, "']" SUB_XPATH_LRM_RESOURCE "[@" PCMK_XA_ID "='", rsc_id, "']", NULL); xml = pcmk__xpath_find_one(scheduler->input->doc, xpath->str, LOG_DEBUG); g_string_free(xpath, TRUE); return xml; } /*! * \internal * \brief Check whether a resource has no completed action history on a node * * \param[in,out] rsc Resource to check * \param[in] node_name Node to check * * \return true if \p rsc_id is unknown on \p node_name, otherwise false */ static bool unknown_on_node(pcmk_resource_t *rsc, const char *node_name) { bool result = false; xmlXPathObject *search; char *xpath = NULL; xpath = pcmk__assert_asprintf(XPATH_NODE_STATE "[@" PCMK_XA_UNAME "='%s']" SUB_XPATH_LRM_RESOURCE "[@" PCMK_XA_ID "='%s']" SUB_XPATH_LRM_RSC_OP "[@" PCMK__XA_RC_CODE "!='%d']", node_name, rsc->id, PCMK_OCF_UNKNOWN); search = pcmk__xpath_search(rsc->priv->scheduler->input->doc, xpath); result = (pcmk__xpath_num_results(search) == 0); xmlXPathFreeObject(search); free(xpath); return result; } /*! * \internal * \brief Check whether a probe/monitor indicating the resource was not running * on a node happened after some event * * \param[in] rsc_id Resource being checked * \param[in] node_name Node being checked * \param[in] xml_op Event that monitor is being compared to * \param[in,out] scheduler Scheduler data * * \return true if such a monitor happened after event, false otherwise */ static bool monitor_not_running_after(const char *rsc_id, const char *node_name, const xmlNode *xml_op, pcmk_scheduler_t *scheduler) { /* Any probe/monitor operation on the node indicating it was not running * there */ xmlNode *monitor = find_lrm_op(rsc_id, PCMK_ACTION_MONITOR, node_name, NULL, PCMK_OCF_NOT_RUNNING, scheduler); return (monitor != NULL) && (pe__is_newer_op(monitor, xml_op) > 0); } /*! * \internal * \brief Check whether any non-monitor operation on a node happened after some * event * * \param[in] rsc_id Resource being checked * \param[in] node_name Node being checked * \param[in] xml_op Event that non-monitor is being compared to * \param[in,out] scheduler Scheduler data * * \return true if such a operation happened after event, false otherwise */ static bool non_monitor_after(const char *rsc_id, const char *node_name, const xmlNode *xml_op, pcmk_scheduler_t *scheduler) { xmlNode *lrm_resource = NULL; lrm_resource = find_lrm_resource(rsc_id, node_name, scheduler); if (lrm_resource == NULL) { return false; } for (xmlNode *op = pcmk__xe_first_child(lrm_resource, PCMK__XE_LRM_RSC_OP, NULL, NULL); op != NULL; op = pcmk__xe_next(op, PCMK__XE_LRM_RSC_OP)) { const char * task = NULL; if (op == xml_op) { continue; } task = pcmk__xe_get(op, PCMK_XA_OPERATION); if (pcmk__str_any_of(task, PCMK_ACTION_START, PCMK_ACTION_STOP, PCMK_ACTION_MIGRATE_TO, PCMK_ACTION_MIGRATE_FROM, NULL) && pe__is_newer_op(op, xml_op) > 0) { return true; } } return false; } /*! * \internal * \brief Check whether the resource has newer state on a node after a migration * attempt * * \param[in] rsc_id Resource being checked * \param[in] node_name Node being checked * \param[in] migrate_to Any migrate_to event that is being compared to * \param[in] migrate_from Any migrate_from event that is being compared to * \param[in,out] scheduler Scheduler data * * \return true if such a operation happened after event, false otherwise */ static bool newer_state_after_migrate(const char *rsc_id, const char *node_name, const xmlNode *migrate_to, const xmlNode *migrate_from, pcmk_scheduler_t *scheduler) { const xmlNode *xml_op = (migrate_from != NULL)? migrate_from : migrate_to; const char *source = pcmk__xe_get(xml_op, PCMK__META_MIGRATE_SOURCE); /* It's preferred to compare to the migrate event on the same node if * existing, since call ids are more reliable. */ if ((xml_op != migrate_to) && (migrate_to != NULL) && pcmk__str_eq(node_name, source, pcmk__str_casei)) { xml_op = migrate_to; } /* If there's any newer non-monitor operation on the node, or any newer * probe/monitor operation on the node indicating it was not running there, * the migration events potentially no longer matter for the node. */ return non_monitor_after(rsc_id, node_name, xml_op, scheduler) || monitor_not_running_after(rsc_id, node_name, xml_op, scheduler); } /*! * \internal * \brief Parse migration source and target node names from history entry * * \param[in] entry Resource history entry for a migration action * \param[in] source_node If not NULL, source must match this node * \param[in] target_node If not NULL, target must match this node * \param[out] source_name Where to store migration source node name * \param[out] target_name Where to store migration target node name * * \return Standard Pacemaker return code */ static int get_migration_node_names(const xmlNode *entry, const pcmk_node_t *source_node, const pcmk_node_t *target_node, const char **source_name, const char **target_name) { *source_name = pcmk__xe_get(entry, PCMK__META_MIGRATE_SOURCE); *target_name = pcmk__xe_get(entry, PCMK__META_MIGRATE_TARGET); if ((*source_name == NULL) || (*target_name == NULL)) { pcmk__config_err("Ignoring resource history entry %s without " PCMK__META_MIGRATE_SOURCE " and " PCMK__META_MIGRATE_TARGET, pcmk__xe_id(entry)); return pcmk_rc_unpack_error; } if ((source_node != NULL) && !pcmk__str_eq(*source_name, source_node->priv->name, pcmk__str_casei|pcmk__str_null_matches)) { pcmk__config_err("Ignoring resource history entry %s because " PCMK__META_MIGRATE_SOURCE "='%s' does not match %s", pcmk__xe_id(entry), *source_name, pcmk__node_name(source_node)); return pcmk_rc_unpack_error; } if ((target_node != NULL) && !pcmk__str_eq(*target_name, target_node->priv->name, pcmk__str_casei|pcmk__str_null_matches)) { pcmk__config_err("Ignoring resource history entry %s because " PCMK__META_MIGRATE_TARGET "='%s' does not match %s", pcmk__xe_id(entry), *target_name, pcmk__node_name(target_node)); return pcmk_rc_unpack_error; } return pcmk_rc_ok; } /* * \internal * \brief Add a migration source to a resource's list of dangling migrations * * If the migrate_to and migrate_from actions in a live migration both * succeeded, but there is no stop on the source, the migration is considered * "dangling." Add the source to the resource's dangling migration list, which * will be used to schedule a stop on the source without affecting the target. * * \param[in,out] rsc Resource involved in migration * \param[in] node Migration source */ static void add_dangling_migration(pcmk_resource_t *rsc, const pcmk_node_t *node) { pcmk__rsc_trace(rsc, "Dangling migration of %s requires stop on %s", rsc->id, pcmk__node_name(node)); rsc->priv->orig_role = pcmk_role_stopped; rsc->priv->dangling_migration_sources = g_list_prepend(rsc->priv->dangling_migration_sources, (gpointer) node); } /*! * \internal * \brief Update resource role etc. after a successful migrate_to action * * \param[in,out] history Parsed action result history */ static void unpack_migrate_to_success(struct action_history *history) { /* A complete migration sequence is: * 1. migrate_to on source node (which succeeded if we get to this function) * 2. migrate_from on target node * 3. stop on source node * * If no migrate_from has happened, the migration is considered to be * "partial". If the migrate_from succeeded but no stop has happened, the * migration is considered to be "dangling". * * If a successful migrate_to and stop have happened on the source node, we * still need to check for a partial migration, due to scenarios (easier to * produce with batch-limit=1) like: * * - A resource is migrating from node1 to node2, and a migrate_to is * initiated for it on node1. * * - node2 goes into standby mode while the migrate_to is pending, which * aborts the transition. * * - Upon completion of the migrate_to, a new transition schedules a stop * on both nodes and a start on node1. * * - If the new transition is aborted for any reason while the resource is * stopping on node1, the transition after that stop completes will see * the migrate_to and stop on the source, but it's still a partial * migration, and the resource must be stopped on node2 because it is * potentially active there due to the migrate_to. * * We also need to take into account that either node's history may be * cleared at any point in the migration process. */ int from_rc = PCMK_OCF_OK; int from_status = PCMK_EXEC_PENDING; pcmk_node_t *target_node = NULL; xmlNode *migrate_from = NULL; const char *source = NULL; const char *target = NULL; bool source_newer_op = false; bool target_newer_state = false; bool active_on_target = false; pcmk_scheduler_t *scheduler = history->rsc->priv->scheduler; // Get source and target node names from XML if (get_migration_node_names(history->xml, history->node, NULL, &source, &target) != pcmk_rc_ok) { return; } // Check for newer state on the source source_newer_op = non_monitor_after(history->rsc->id, source, history->xml, scheduler); // Check for a migrate_from action from this source on the target migrate_from = find_lrm_op(history->rsc->id, PCMK_ACTION_MIGRATE_FROM, target, source, -1, scheduler); if (migrate_from != NULL) { if (source_newer_op) { /* There's a newer non-monitor operation on the source and a * migrate_from on the target, so this migrate_to is irrelevant to * the resource's state. */ return; } pcmk__xe_get_int(migrate_from, PCMK__XA_RC_CODE, &from_rc); pcmk__xe_get_int(migrate_from, PCMK__XA_OP_STATUS, &from_status); } /* If the resource has newer state on both the source and target after the * migration events, this migrate_to is irrelevant to the resource's state. */ target_newer_state = newer_state_after_migrate(history->rsc->id, target, history->xml, migrate_from, scheduler); if (source_newer_op && target_newer_state) { return; } /* Check for dangling migration (migrate_from succeeded but stop not done). * We know there's no stop because we already returned if the target has a * migrate_from and the source has any newer non-monitor operation. */ if ((from_rc == PCMK_OCF_OK) && (from_status == PCMK_EXEC_DONE)) { add_dangling_migration(history->rsc, history->node); return; } /* Without newer state, this migrate_to implies the resource is active. * (Clones are not allowed to migrate, so role can't be promoted.) */ history->rsc->priv->orig_role = pcmk_role_started; target_node = pcmk_find_node(scheduler, target); active_on_target = !target_newer_state && (target_node != NULL) && target_node->details->online; if (from_status != PCMK_EXEC_PENDING) { // migrate_from failed on target if (active_on_target) { native_add_running(history->rsc, target_node, scheduler, TRUE); } else { // Mark resource as failed, require recovery, and prevent migration pcmk__set_rsc_flags(history->rsc, pcmk__rsc_failed|pcmk__rsc_stop_if_failed); pcmk__clear_rsc_flags(history->rsc, pcmk__rsc_migratable); } return; } // The migrate_from is pending, complete but erased, or to be scheduled /* If there is no history at all for the resource on an online target, then * it was likely cleaned. Just return, and we'll schedule a probe. Once we * have the probe result, it will be reflected in target_newer_state. */ if ((target_node != NULL) && target_node->details->online && unknown_on_node(history->rsc, target)) { return; } if (active_on_target) { pcmk_node_t *source_node = pcmk_find_node(scheduler, source); native_add_running(history->rsc, target_node, scheduler, FALSE); if ((source_node != NULL) && source_node->details->online) { /* This is a partial migration: the migrate_to completed * successfully on the source, but the migrate_from has not * completed. Remember the source and target; if the newly * chosen target remains the same when we schedule actions * later, we may continue with the migration. */ history->rsc->priv->partial_migration_target = target_node; history->rsc->priv->partial_migration_source = source_node; } } else if (!source_newer_op) { // Mark resource as failed, require recovery, and prevent migration pcmk__set_rsc_flags(history->rsc, pcmk__rsc_failed|pcmk__rsc_stop_if_failed); pcmk__clear_rsc_flags(history->rsc, pcmk__rsc_migratable); } } /*! * \internal * \brief Update resource role etc. after a failed migrate_to action * * \param[in,out] history Parsed action result history */ static void unpack_migrate_to_failure(struct action_history *history) { xmlNode *target_migrate_from = NULL; const char *source = NULL; const char *target = NULL; pcmk_scheduler_t *scheduler = history->rsc->priv->scheduler; // Get source and target node names from XML if (get_migration_node_names(history->xml, history->node, NULL, &source, &target) != pcmk_rc_ok) { return; } /* If a migration failed, we have to assume the resource is active. Clones * are not allowed to migrate, so role can't be promoted. */ history->rsc->priv->orig_role = pcmk_role_started; // Check for migrate_from on the target target_migrate_from = find_lrm_op(history->rsc->id, PCMK_ACTION_MIGRATE_FROM, target, source, PCMK_OCF_OK, scheduler); if (/* If the resource state is unknown on the target, it will likely be * probed there. * Don't just consider it running there. We will get back here anyway in * case the probe detects it's running there. */ !unknown_on_node(history->rsc, target) /* If the resource has newer state on the target after the migration * events, this migrate_to no longer matters for the target. */ && !newer_state_after_migrate(history->rsc->id, target, history->xml, target_migrate_from, scheduler)) { /* The resource has no newer state on the target, so assume it's still * active there. * (if it is up). */ pcmk_node_t *target_node = pcmk_find_node(scheduler, target); if (target_node && target_node->details->online) { native_add_running(history->rsc, target_node, scheduler, FALSE); } } else if (!non_monitor_after(history->rsc->id, source, history->xml, scheduler)) { /* We know the resource has newer state on the target, but this * migrate_to still matters for the source as long as there's no newer * non-monitor operation there. */ // Mark node as having dangling migration so we can force a stop later history->rsc->priv->dangling_migration_sources = g_list_prepend(history->rsc->priv->dangling_migration_sources, (gpointer) history->node); } } /*! * \internal * \brief Update resource role etc. after a failed migrate_from action * * \param[in,out] history Parsed action result history */ static void unpack_migrate_from_failure(struct action_history *history) { xmlNode *source_migrate_to = NULL; const char *source = NULL; const char *target = NULL; pcmk_scheduler_t *scheduler = history->rsc->priv->scheduler; // Get source and target node names from XML if (get_migration_node_names(history->xml, NULL, history->node, &source, &target) != pcmk_rc_ok) { return; } /* If a migration failed, we have to assume the resource is active. Clones * are not allowed to migrate, so role can't be promoted. */ history->rsc->priv->orig_role = pcmk_role_started; // Check for a migrate_to on the source source_migrate_to = find_lrm_op(history->rsc->id, PCMK_ACTION_MIGRATE_TO, source, target, PCMK_OCF_OK, scheduler); if (/* If the resource state is unknown on the source, it will likely be * probed there. * Don't just consider it running there. We will get back here anyway in * case the probe detects it's running there. */ !unknown_on_node(history->rsc, source) /* If the resource has newer state on the source after the migration * events, this migrate_from no longer matters for the source. */ && !newer_state_after_migrate(history->rsc->id, source, source_migrate_to, history->xml, scheduler)) { /* The resource has no newer state on the source, so assume it's still * active there (if it is up). */ pcmk_node_t *source_node = pcmk_find_node(scheduler, source); if (source_node && source_node->details->online) { native_add_running(history->rsc, source_node, scheduler, TRUE); } } } /*! * \internal * \brief Add an action to cluster's list of failed actions * * \param[in,out] history Parsed action result history */ static void record_failed_op(struct action_history *history) { const pcmk_scheduler_t *scheduler = history->rsc->priv->scheduler; if (!(history->node->details->online)) { return; } for (const xmlNode *xIter = scheduler->priv->failed->children; xIter != NULL; xIter = xIter->next) { const char *key = pcmk__xe_history_key(xIter); const char *uname = pcmk__xe_get(xIter, PCMK_XA_UNAME); if (pcmk__str_eq(history->key, key, pcmk__str_none) && pcmk__str_eq(uname, history->node->priv->name, pcmk__str_casei)) { pcmk__trace("Skipping duplicate entry %s on %s", history->key, pcmk__node_name(history->node)); return; } } pcmk__trace("Adding entry for %s on %s to failed action list", history->key, pcmk__node_name(history->node)); pcmk__xe_set(history->xml, PCMK_XA_UNAME, history->node->priv->name); pcmk__xe_set(history->xml, PCMK__XA_RSC_ID, history->rsc->id); pcmk__xml_copy(scheduler->priv->failed, history->xml); } static char * last_change_str(const xmlNode *xml_op) { time_t when; char *result = NULL; if (pcmk__xe_get_time(xml_op, PCMK_XA_LAST_RC_CHANGE, &when) == pcmk_rc_ok) { char *when_s = pcmk__epoch2str(&when, 0); const char *p = strchr(when_s, ' '); // Skip day of week to make message shorter if ((p != NULL) && (*(++p) != '\0')) { result = pcmk__str_copy(p); } free(when_s); } if (result == NULL) { result = pcmk__str_copy("unknown_time"); } return result; } /*! * \internal * \brief Ban a resource (or its clone if an anonymous instance) from all nodes * * \param[in,out] rsc Resource to ban */ static void ban_from_all_nodes(pcmk_resource_t *rsc) { int score = -PCMK_SCORE_INFINITY; const pcmk_scheduler_t *scheduler = rsc->priv->scheduler; if (rsc->priv->parent != NULL) { pcmk_resource_t *parent = uber_parent(rsc); if (pcmk__is_anonymous_clone(parent)) { /* For anonymous clones, if an operation with * PCMK_META_ON_FAIL=PCMK_VALUE_STOP fails for any instance, the * entire clone must stop. */ rsc = parent; } } // Ban the resource from all nodes pcmk__notice("%s will not be started under current conditions", rsc->id); if (rsc->priv->allowed_nodes != NULL) { g_hash_table_destroy(rsc->priv->allowed_nodes); } rsc->priv->allowed_nodes = pe__node_list2table(scheduler->nodes); g_hash_table_foreach(rsc->priv->allowed_nodes, set_node_score, &score); } /*! * \internal * \brief Get configured failure handling and role after failure for an action * * \param[in,out] history Unpacked action history entry * \param[out] on_fail Where to set configured failure handling * \param[out] fail_role Where to set to role after failure */ static void unpack_failure_handling(struct action_history *history, enum pcmk__on_fail *on_fail, enum rsc_role_e *fail_role) { xmlNode *config = pcmk__find_action_config(history->rsc, history->task, history->interval_ms, true); GHashTable *meta = pcmk__unpack_action_meta(history->rsc, history->node, history->task, history->interval_ms, config); const char *on_fail_str = g_hash_table_lookup(meta, PCMK_META_ON_FAIL); *on_fail = pcmk__parse_on_fail(history->rsc, history->task, history->interval_ms, on_fail_str); *fail_role = pcmk__role_after_failure(history->rsc, history->task, *on_fail, meta); g_hash_table_destroy(meta); } /*! * \internal * \brief Update resource role, failure handling, etc., after a failed action * * \param[in,out] history Parsed action result history * \param[in] config_on_fail Action failure handling from configuration * \param[in] fail_role Resource's role after failure of this action * \param[out] last_failure This will be set to the history XML * \param[in,out] on_fail Actual handling of action result */ static void unpack_rsc_op_failure(struct action_history *history, enum pcmk__on_fail config_on_fail, enum rsc_role_e fail_role, xmlNode **last_failure, enum pcmk__on_fail *on_fail) { bool is_probe = false; char *last_change_s = NULL; pcmk_scheduler_t *scheduler = history->rsc->priv->scheduler; *last_failure = history->xml; is_probe = pcmk_xe_is_probe(history->xml); last_change_s = last_change_str(history->xml); if (!pcmk__is_set(scheduler->flags, pcmk__sched_symmetric_cluster) && (history->exit_status == PCMK_OCF_NOT_INSTALLED)) { pcmk__trace("Unexpected result (%s%s%s) was recorded for " "%s of %s on %s at %s " QB_XS " exit-status=%d id=%s", crm_exit_str(history->exit_status), (pcmk__str_empty(history->exit_reason)? "" : ": "), pcmk__s(history->exit_reason, ""), (is_probe? "probe" : history->task), history->rsc->id, pcmk__node_name(history->node), last_change_s, history->exit_status, history->id); } else { pcmk__sched_warn(scheduler, "Unexpected result (%s%s%s) was recorded for %s of " "%s on %s at %s " QB_XS " exit-status=%d id=%s", crm_exit_str(history->exit_status), (pcmk__str_empty(history->exit_reason)? "" : ": "), pcmk__s(history->exit_reason, ""), (is_probe? "probe" : history->task), history->rsc->id, pcmk__node_name(history->node), last_change_s, history->exit_status, history->id); if (is_probe && (history->exit_status != PCMK_OCF_OK) && (history->exit_status != PCMK_OCF_NOT_RUNNING) && (history->exit_status != PCMK_OCF_RUNNING_PROMOTED)) { /* A failed (not just unexpected) probe result could mean the user * didn't know resources will be probed even where they can't run. */ pcmk__notice("If it is not possible for %s to run on %s, see the " PCMK_XA_RESOURCE_DISCOVERY " option for location " "constraints", history->rsc->id, pcmk__node_name(history->node)); } record_failed_op(history); } free(last_change_s); if (*on_fail < config_on_fail) { pcmk__rsc_trace(history->rsc, "on-fail %s -> %s for %s", pcmk__on_fail_text(*on_fail), pcmk__on_fail_text(config_on_fail), history->key); *on_fail = config_on_fail; } if (strcmp(history->task, PCMK_ACTION_STOP) == 0) { resource_location(history->rsc, history->node, -PCMK_SCORE_INFINITY, "__stop_fail__", scheduler); } else if (strcmp(history->task, PCMK_ACTION_MIGRATE_TO) == 0) { unpack_migrate_to_failure(history); } else if (strcmp(history->task, PCMK_ACTION_MIGRATE_FROM) == 0) { unpack_migrate_from_failure(history); } else if (strcmp(history->task, PCMK_ACTION_PROMOTE) == 0) { history->rsc->priv->orig_role = pcmk_role_promoted; } else if (strcmp(history->task, PCMK_ACTION_DEMOTE) == 0) { if (config_on_fail == pcmk__on_fail_block) { history->rsc->priv->orig_role = pcmk_role_promoted; pe__set_next_role(history->rsc, pcmk_role_stopped, "demote with " PCMK_META_ON_FAIL "=block"); } else if (history->exit_status == PCMK_OCF_NOT_RUNNING) { history->rsc->priv->orig_role = pcmk_role_stopped; } else { /* Staying in the promoted role would put the scheduler and * controller into a loop. Setting the role to unpromoted is not * dangerous because the resource will be stopped as part of * recovery, and any promotion will be ordered after that stop. */ history->rsc->priv->orig_role = pcmk_role_unpromoted; } } if (is_probe && (history->exit_status == PCMK_OCF_NOT_INSTALLED)) { /* leave stopped */ pcmk__rsc_trace(history->rsc, "Leaving %s stopped", history->rsc->id); history->rsc->priv->orig_role = pcmk_role_stopped; } else if (history->rsc->priv->orig_role < pcmk_role_started) { pcmk__rsc_trace(history->rsc, "Setting %s active", history->rsc->id); set_active(history->rsc); } pcmk__rsc_trace(history->rsc, "Resource %s: role=%s unclean=%s on_fail=%s fail_role=%s", history->rsc->id, pcmk_role_text(history->rsc->priv->orig_role), pcmk__btoa(history->node->details->unclean), pcmk__on_fail_text(config_on_fail), pcmk_role_text(fail_role)); if ((fail_role != pcmk_role_started) && (history->rsc->priv->next_role < fail_role)) { pe__set_next_role(history->rsc, fail_role, "failure"); } if (fail_role == pcmk_role_stopped) { ban_from_all_nodes(history->rsc); } } /*! * \internal * \brief Block a resource with a failed action if it cannot be recovered * * If resource action is a failed stop and fencing is not possible, mark the * resource as unmanaged and blocked, since recovery cannot be done. * * \param[in,out] history Parsed action history entry */ static void block_if_unrecoverable(struct action_history *history) { char *last_change_s = NULL; if (strcmp(history->task, PCMK_ACTION_STOP) != 0) { return; // All actions besides stop are always recoverable } if (pe_can_fence(history->node->priv->scheduler, history->node)) { return; // Failed stops are recoverable via fencing } last_change_s = last_change_str(history->xml); pcmk__sched_err(history->node->priv->scheduler, "No further recovery can be attempted for %s " "because %s on %s failed (%s%s%s) at %s " QB_XS " rc=%d id=%s", history->rsc->id, history->task, pcmk__node_name(history->node), crm_exit_str(history->exit_status), (pcmk__str_empty(history->exit_reason)? "" : ": "), pcmk__s(history->exit_reason, ""), last_change_s, history->exit_status, history->id); free(last_change_s); pcmk__clear_rsc_flags(history->rsc, pcmk__rsc_managed); pcmk__set_rsc_flags(history->rsc, pcmk__rsc_blocked); } /*! * \internal * \brief Update action history's execution status and why * * \param[in,out] history Parsed action history entry * \param[out] why Where to store reason for update * \param[in] value New value * \param[in] reason Description of why value was changed */ static inline void remap_because(struct action_history *history, const char **why, int value, const char *reason) { if (history->execution_status != value) { history->execution_status = value; *why = reason; } } /*! * \internal * \brief Remap informational monitor results and operation status * * For the monitor results, certain OCF codes are for providing extended information * to the user about services that aren't yet failed but not entirely healthy either. * These must be treated as the "normal" result by Pacemaker. * * For operation status, the action result can be used to determine an appropriate * status for the purposes of responding to the action. The status provided by the * executor is not directly usable since the executor does not know what was expected. * * \param[in,out] history Parsed action history entry * \param[in,out] on_fail What should be done about the result * \param[in] expired Whether result is expired * * \note If the result is remapped and the node is not shutting down or failed, * the operation will be recorded in the scheduler data's list of failed * operations to highlight it for the user. * * \note This may update the resource's current and next role. */ static void remap_operation(struct action_history *history, enum pcmk__on_fail *on_fail, bool expired) { /* @TODO It would probably also be a good idea to map an exit status of * CRM_EX_PROMOTED or CRM_EX_DEGRADED_PROMOTED to CRM_EX_OK for promote * actions */ bool is_probe = false; int orig_exit_status = history->exit_status; int orig_exec_status = history->execution_status; const char *why = NULL; const char *task = history->task; // Remap degraded results to their successful counterparts history->exit_status = pcmk__effective_rc(history->exit_status); if (history->exit_status != orig_exit_status) { why = "degraded result"; if (!expired && (!history->node->details->shutdown || history->node->details->online)) { record_failed_op(history); } } if (!pcmk__is_bundled(history->rsc) && pcmk_xe_mask_probe_failure(history->xml) && ((history->execution_status != PCMK_EXEC_DONE) || (history->exit_status != PCMK_OCF_NOT_RUNNING))) { history->execution_status = PCMK_EXEC_DONE; history->exit_status = PCMK_OCF_NOT_RUNNING; why = "equivalent probe result"; } /* If the executor reported an execution status of anything but done or * error, consider that final. But for done or error, we know better whether * it should be treated as a failure or not, because we know the expected * result. */ switch (history->execution_status) { case PCMK_EXEC_DONE: case PCMK_EXEC_ERROR: break; // These should be treated as node-fatal case PCMK_EXEC_NO_FENCE_DEVICE: case PCMK_EXEC_NO_SECRETS: remap_because(history, &why, PCMK_EXEC_ERROR_HARD, "node-fatal error"); goto remap_done; default: goto remap_done; } is_probe = pcmk_xe_is_probe(history->xml); if (is_probe) { task = "probe"; } if (history->expected_exit_status < 0) { /* Pre-1.0 Pacemaker versions, and Pacemaker 1.1.6 or earlier with * Heartbeat 2.0.7 or earlier as the cluster layer, did not include the * expected exit status in the transition key, which (along with the * similar case of a corrupted transition key in the CIB) will be * reported to this function as -1. Pacemaker 2.0+ does not support * rolling upgrades from those versions or processing of saved CIB files * from those versions, so we do not need to care much about this case. */ remap_because(history, &why, PCMK_EXEC_ERROR, "obsolete history format"); pcmk__config_warn("Expected result not found for %s on %s " "(corrupt or obsolete CIB?)", history->key, pcmk__node_name(history->node)); } else if (history->exit_status == history->expected_exit_status) { remap_because(history, &why, PCMK_EXEC_DONE, "expected result"); } else { remap_because(history, &why, PCMK_EXEC_ERROR, "unexpected result"); pcmk__rsc_debug(history->rsc, "%s on %s: expected %d (%s), got %d (%s%s%s)", history->key, pcmk__node_name(history->node), history->expected_exit_status, crm_exit_str(history->expected_exit_status), history->exit_status, crm_exit_str(history->exit_status), (pcmk__str_empty(history->exit_reason)? "" : ": "), pcmk__s(history->exit_reason, "")); } switch (history->exit_status) { case PCMK_OCF_OK: if (is_probe && (history->expected_exit_status == PCMK_OCF_NOT_RUNNING)) { char *last_change_s = last_change_str(history->xml); remap_because(history, &why, PCMK_EXEC_DONE, "probe"); pcmk__rsc_info(history->rsc, "Probe found %s active on %s at %s", history->rsc->id, pcmk__node_name(history->node), last_change_s); free(last_change_s); } break; case PCMK_OCF_NOT_RUNNING: if (is_probe || (history->expected_exit_status == history->exit_status) || !pcmk__is_set(history->rsc->flags, pcmk__rsc_managed)) { /* For probes, recurring monitors for the Stopped role, and * unmanaged resources, "not running" is not considered a * failure. */ remap_because(history, &why, PCMK_EXEC_DONE, "exit status"); history->rsc->priv->orig_role = pcmk_role_stopped; *on_fail = pcmk__on_fail_ignore; pe__set_next_role(history->rsc, pcmk_role_unknown, "not running"); } break; case PCMK_OCF_RUNNING_PROMOTED: if (is_probe && (history->exit_status != history->expected_exit_status)) { char *last_change_s = last_change_str(history->xml); remap_because(history, &why, PCMK_EXEC_DONE, "probe"); pcmk__rsc_info(history->rsc, "Probe found %s active and promoted on %s at %s", history->rsc->id, pcmk__node_name(history->node), last_change_s); free(last_change_s); } if (!expired || (history->exit_status == history->expected_exit_status)) { history->rsc->priv->orig_role = pcmk_role_promoted; } break; case PCMK_OCF_FAILED_PROMOTED: if (!expired) { history->rsc->priv->orig_role = pcmk_role_promoted; } remap_because(history, &why, PCMK_EXEC_ERROR, "exit status"); break; case PCMK_OCF_NOT_CONFIGURED: remap_because(history, &why, PCMK_EXEC_ERROR_FATAL, "exit status"); break; case PCMK_OCF_UNIMPLEMENT_FEATURE: { guint interval_ms = 0; pcmk__xe_get_guint(history->xml, PCMK_META_INTERVAL, &interval_ms); if (interval_ms == 0) { if (!expired) { block_if_unrecoverable(history); } remap_because(history, &why, PCMK_EXEC_ERROR_HARD, "exit status"); } else { remap_because(history, &why, PCMK_EXEC_NOT_SUPPORTED, "exit status"); } } break; case PCMK_OCF_NOT_INSTALLED: case PCMK_OCF_INVALID_PARAM: case PCMK_OCF_INSUFFICIENT_PRIV: if (!expired) { block_if_unrecoverable(history); } remap_because(history, &why, PCMK_EXEC_ERROR_HARD, "exit status"); break; default: if (history->execution_status == PCMK_EXEC_DONE) { char *last_change_s = last_change_str(history->xml); pcmk__info("Treating unknown exit status %d from %s of %s on " "%s at %s as failure", history->exit_status, task, history->rsc->id, pcmk__node_name(history->node), last_change_s); remap_because(history, &why, PCMK_EXEC_ERROR, "unknown exit status"); free(last_change_s); } break; } remap_done: if (why != NULL) { pcmk__rsc_trace(history->rsc, "Remapped %s result from [%s: %s] to [%s: %s] " "because of %s", history->key, pcmk_exec_status_str(orig_exec_status), crm_exit_str(orig_exit_status), pcmk_exec_status_str(history->execution_status), crm_exit_str(history->exit_status), why); } } // return TRUE if start or monitor last failure but parameters changed static bool should_clear_for_param_change(const xmlNode *xml_op, const char *task, pcmk_resource_t *rsc, pcmk_node_t *node) { if (pcmk__str_any_of(task, PCMK_ACTION_START, PCMK_ACTION_MONITOR, NULL)) { if (pe__bundle_needs_remote_name(rsc)) { /* We haven't allocated resources yet, so we can't reliably * substitute addr parameters for the REMOTE_CONTAINER_HACK. * When that's needed, defer the check until later. */ pcmk__add_param_check(xml_op, rsc, node, pcmk__check_last_failure); } else { pcmk__op_digest_t *digest_data = NULL; digest_data = rsc_action_digest_cmp(rsc, xml_op, node, rsc->priv->scheduler); switch (digest_data->rc) { case pcmk__digest_unknown: pcmk__trace("Resource %s history entry %s on %s" " has no digest to compare", rsc->id, pcmk__xe_history_key(xml_op), node->priv->id); break; case pcmk__digest_match: break; default: return TRUE; } } } return FALSE; } // Order action after fencing of remote node, given connection rsc static void order_after_remote_fencing(pcmk_action_t *action, pcmk_resource_t *remote_conn, pcmk_scheduler_t *scheduler) { pcmk_node_t *remote_node = pcmk_find_node(scheduler, remote_conn->id); if (remote_node) { pcmk_action_t *fence = pe_fence_op(remote_node, NULL, TRUE, NULL, FALSE, scheduler); order_actions(fence, action, pcmk__ar_first_implies_then); } } static bool should_ignore_failure_timeout(const pcmk_resource_t *rsc, const char *task, guint interval_ms, bool is_last_failure) { /* Clearing failures of recurring monitors has special concerns. The * executor reports only changes in the monitor result, so if the * monitor is still active and still getting the same failure result, * that will go undetected after the failure is cleared. * * Also, the operation history will have the time when the recurring * monitor result changed to the given code, not the time when the * result last happened. * * @TODO We probably should clear such failures only when the failure * timeout has passed since the last occurrence of the failed result. * However we don't record that information. We could maybe approximate * that by clearing only if there is a more recent successful monitor or * stop result, but we don't even have that information at this point * since we are still unpacking the resource's operation history. * * This is especially important for remote connection resources with a * reconnect interval, so in that case, we skip clearing failures * if the remote node hasn't been fenced. */ if ((rsc->priv->remote_reconnect_ms > 0U) && pcmk__is_set(rsc->priv->scheduler->flags, pcmk__sched_fencing_enabled) && (interval_ms != 0) && pcmk__str_eq(task, PCMK_ACTION_MONITOR, pcmk__str_casei)) { pcmk_node_t *remote_node = pcmk_find_node(rsc->priv->scheduler, rsc->id); if (remote_node && !pcmk__is_set(remote_node->priv->flags, pcmk__node_remote_fenced)) { if (is_last_failure) { pcmk__info("Waiting to clear monitor failure for remote node %s" " until fencing has occurred", rsc->id); } return TRUE; } } return FALSE; } /*! * \internal * \brief Check operation age and schedule failure clearing when appropriate * * This function has two distinct purposes. The first is to check whether an * operation history entry is expired (i.e. the resource has a failure timeout, * the entry is older than the timeout, and the resource either has no fail * count or its fail count is entirely older than the timeout). The second is to * schedule fail count clearing when appropriate (i.e. the operation is expired * and either the resource has an expired fail count or the operation is a * last_failure for a remote connection resource with a reconnect interval, * or the operation is a last_failure for a start or monitor operation and the * resource's parameters have changed since the operation). * * \param[in,out] history Parsed action result history * * \return true if operation history entry is expired, otherwise false */ static bool check_operation_expiry(struct action_history *history) { bool expired = false; bool is_last_failure = pcmk__ends_with(history->id, "_last_failure_0"); time_t last_run = 0; int unexpired_fail_count = 0; const char *clear_reason = NULL; const guint expiration_sec = pcmk__timeout_ms2s(history->rsc->priv->failure_expiration_ms); pcmk_scheduler_t *scheduler = history->rsc->priv->scheduler; if (history->execution_status == PCMK_EXEC_NOT_INSTALLED) { pcmk__rsc_trace(history->rsc, "Resource history entry %s on %s is not expired: " "Not Installed does not expire", history->id, pcmk__node_name(history->node)); return false; // "Not installed" must always be cleared manually } if ((expiration_sec > 0) && (pcmk__xe_get_time(history->xml, PCMK_XA_LAST_RC_CHANGE, &last_run) == pcmk_rc_ok)) { /* Resource has a PCMK_META_FAILURE_TIMEOUT and history entry has a * timestamp */ time_t now = pcmk__scheduler_epoch_time(scheduler); time_t last_failure = 0; // Is this particular operation history older than the failure timeout? if ((now >= (last_run + expiration_sec)) && !should_ignore_failure_timeout(history->rsc, history->task, history->interval_ms, is_last_failure)) { expired = true; } // Does the resource as a whole have an unexpired fail count? unexpired_fail_count = pe_get_failcount(history->node, history->rsc, &last_failure, pcmk__fc_effective, history->xml); // Update scheduler recheck time according to *last* failure pcmk__trace("%s@%lld is %sexpired @%lld with unexpired_failures=%d " "expiration=%s last-failure@%lld", history->id, (long long) last_run, (expired? "" : "not "), (long long) now, unexpired_fail_count, pcmk__readable_interval(expiration_sec * 1000), (long long) last_failure); last_failure += expiration_sec + 1; if (unexpired_fail_count && (now < last_failure)) { pcmk__update_recheck_time(last_failure, scheduler, "fail count expiration"); } } if (expired) { if (pe_get_failcount(history->node, history->rsc, NULL, pcmk__fc_default, history->xml)) { // There is a fail count ignoring timeout if (unexpired_fail_count == 0) { // There is no fail count considering timeout clear_reason = "it expired"; } else { /* This operation is old, but there is an unexpired fail count. * In a properly functioning cluster, this should only be * possible if this operation is not a failure (otherwise the * fail count should be expired too), so this is really just a * failsafe. */ pcmk__rsc_trace(history->rsc, "Resource history entry %s on %s is not " "expired: Unexpired fail count", history->id, pcmk__node_name(history->node)); expired = false; } } else if (is_last_failure && (history->rsc->priv->remote_reconnect_ms > 0U)) { /* Clear any expired last failure when reconnect interval is set, * even if there is no fail count. */ clear_reason = "reconnect interval is set"; } } if (!expired && is_last_failure && should_clear_for_param_change(history->xml, history->task, history->rsc, history->node)) { clear_reason = "resource parameters have changed"; } if (clear_reason != NULL) { pcmk_action_t *clear_op = NULL; // Schedule clearing of the fail count clear_op = pe__clear_failcount(history->rsc, history->node, clear_reason, scheduler); if (pcmk__is_set(scheduler->flags, pcmk__sched_fencing_enabled) && (history->rsc->priv->remote_reconnect_ms > 0)) { /* If we're clearing a remote connection due to a reconnect * interval, we want to wait until any scheduled fencing * completes. * * We could limit this to remote_node->details->unclean, but at * this point, that's always true (it won't be reliable until * after unpack_node_history() is done). */ pcmk__info("Clearing %s failure will wait until any scheduled " "fencing of %s completes", history->task, history->rsc->id); order_after_remote_fencing(clear_op, history->rsc, scheduler); } } if (expired && (history->interval_ms == 0) && pcmk__str_eq(history->task, PCMK_ACTION_MONITOR, pcmk__str_none)) { switch (history->exit_status) { case PCMK_OCF_OK: case PCMK_OCF_NOT_RUNNING: case PCMK_OCF_RUNNING_PROMOTED: case PCMK_OCF_DEGRADED: case PCMK_OCF_DEGRADED_PROMOTED: // Don't expire probes that return these values pcmk__rsc_trace(history->rsc, "Resource history entry %s on %s is not " "expired: Probe result", history->id, pcmk__node_name(history->node)); expired = false; break; } } return expired; } int pe__target_rc_from_xml(const xmlNode *xml_op) { int target_rc = 0; const char *key = pcmk__xe_get(xml_op, PCMK__XA_TRANSITION_KEY); if (key == NULL) { return -1; } decode_transition_key(key, NULL, NULL, NULL, &target_rc); return target_rc; } /*! * \internal * \brief Update a resource's state for an action result * * \param[in,out] history Parsed action history entry * \param[in] exit_status Exit status to base new state on * \param[in] last_failure Resource's last_failure entry, if known * \param[in,out] on_fail Resource's current failure handling */ static void update_resource_state(struct action_history *history, int exit_status, const xmlNode *last_failure, enum pcmk__on_fail *on_fail) { bool clear_past_failure = false; if ((exit_status == PCMK_OCF_NOT_INSTALLED) || (!pcmk__is_bundled(history->rsc) && pcmk_xe_mask_probe_failure(history->xml))) { history->rsc->priv->orig_role = pcmk_role_stopped; } else if (exit_status == PCMK_OCF_NOT_RUNNING) { clear_past_failure = true; } else if (pcmk__str_eq(history->task, PCMK_ACTION_MONITOR, pcmk__str_none)) { if ((last_failure != NULL) && pcmk__str_eq(history->key, pcmk__xe_history_key(last_failure), pcmk__str_none)) { clear_past_failure = true; } if (history->rsc->priv->orig_role < pcmk_role_started) { set_active(history->rsc); } } else if (pcmk__str_eq(history->task, PCMK_ACTION_START, pcmk__str_none)) { history->rsc->priv->orig_role = pcmk_role_started; clear_past_failure = true; } else if (pcmk__str_eq(history->task, PCMK_ACTION_STOP, pcmk__str_none)) { history->rsc->priv->orig_role = pcmk_role_stopped; clear_past_failure = true; } else if (pcmk__str_eq(history->task, PCMK_ACTION_PROMOTE, pcmk__str_none)) { history->rsc->priv->orig_role = pcmk_role_promoted; clear_past_failure = true; } else if (pcmk__str_eq(history->task, PCMK_ACTION_DEMOTE, pcmk__str_none)) { if (*on_fail == pcmk__on_fail_demote) { /* Demote clears an error only if * PCMK_META_ON_FAIL=PCMK_VALUE_DEMOTE */ clear_past_failure = true; } history->rsc->priv->orig_role = pcmk_role_unpromoted; } else if (pcmk__str_eq(history->task, PCMK_ACTION_MIGRATE_FROM, pcmk__str_none)) { history->rsc->priv->orig_role = pcmk_role_started; clear_past_failure = true; } else if (pcmk__str_eq(history->task, PCMK_ACTION_MIGRATE_TO, pcmk__str_none)) { unpack_migrate_to_success(history); } else if (history->rsc->priv->orig_role < pcmk_role_started) { pcmk__rsc_trace(history->rsc, "%s active on %s", history->rsc->id, pcmk__node_name(history->node)); set_active(history->rsc); } if (!clear_past_failure) { return; } switch (*on_fail) { case pcmk__on_fail_stop: case pcmk__on_fail_ban: case pcmk__on_fail_standby_node: case pcmk__on_fail_fence_node: pcmk__rsc_trace(history->rsc, "%s (%s) is not cleared by a completed %s", history->rsc->id, pcmk__on_fail_text(*on_fail), history->task); break; case pcmk__on_fail_block: case pcmk__on_fail_ignore: case pcmk__on_fail_demote: case pcmk__on_fail_restart: case pcmk__on_fail_restart_container: *on_fail = pcmk__on_fail_ignore; pe__set_next_role(history->rsc, pcmk_role_unknown, "clear past failures"); break; case pcmk__on_fail_reset_remote: if (history->rsc->priv->remote_reconnect_ms == 0U) { /* With no reconnect interval, the connection is allowed to * start again after the remote node is fenced and * completely stopped. (With a reconnect interval, we wait * for the failure to be cleared entirely before attempting * to reconnect.) */ *on_fail = pcmk__on_fail_ignore; pe__set_next_role(history->rsc, pcmk_role_unknown, "clear past failures and reset remote"); } break; } } /*! * \internal * \brief Check whether a given history entry matters for resource state * * \param[in] history Parsed action history entry * * \return true if action can affect resource state, otherwise false */ static inline bool can_affect_state(struct action_history *history) { return pcmk__str_any_of(history->task, PCMK_ACTION_MONITOR, PCMK_ACTION_START, PCMK_ACTION_STOP, PCMK_ACTION_PROMOTE, PCMK_ACTION_DEMOTE, PCMK_ACTION_MIGRATE_TO, PCMK_ACTION_MIGRATE_FROM, "asyncmon", NULL); } /*! * \internal * \brief Unpack execution/exit status and exit reason from a history entry * * \param[in,out] history Action history entry to unpack * * \return Standard Pacemaker return code */ static int unpack_action_result(struct action_history *history) { if ((pcmk__xe_get_int(history->xml, PCMK__XA_OP_STATUS, &(history->execution_status)) != pcmk_rc_ok) || (history->execution_status < PCMK_EXEC_PENDING) || (history->execution_status > PCMK_EXEC_MAX) || (history->execution_status == PCMK_EXEC_CANCELLED)) { pcmk__config_err("Ignoring resource history entry %s for %s on %s " "with invalid " PCMK__XA_OP_STATUS " '%s'", history->id, history->rsc->id, pcmk__node_name(history->node), pcmk__s(pcmk__xe_get(history->xml, PCMK__XA_OP_STATUS), "")); return pcmk_rc_unpack_error; } if ((pcmk__xe_get_int(history->xml, PCMK__XA_RC_CODE, &(history->exit_status)) != pcmk_rc_ok) || (history->exit_status < 0) || (history->exit_status > CRM_EX_MAX)) { pcmk__config_err("Ignoring resource history entry %s for %s on %s " "with invalid " PCMK__XA_RC_CODE " '%s'", history->id, history->rsc->id, pcmk__node_name(history->node), pcmk__s(pcmk__xe_get(history->xml, PCMK__XA_RC_CODE), "")); return pcmk_rc_unpack_error; } history->exit_reason = pcmk__xe_get(history->xml, PCMK_XA_EXIT_REASON); return pcmk_rc_ok; } /*! * \internal * \brief Process an action history entry whose result expired * * \param[in,out] history Parsed action history entry * \param[in] orig_exit_status Action exit status before remapping * * \return Standard Pacemaker return code (in particular, pcmk_rc_ok means the * entry needs no further processing) */ static int process_expired_result(struct action_history *history, int orig_exit_status) { if (!pcmk__is_bundled(history->rsc) && pcmk_xe_mask_probe_failure(history->xml) && (orig_exit_status != history->expected_exit_status)) { if (history->rsc->priv->orig_role <= pcmk_role_stopped) { history->rsc->priv->orig_role = pcmk_role_unknown; } pcmk__trace("Ignoring resource history entry %s for probe of %s on %s: " "Masked failure expired", history->id, history->rsc->id, pcmk__node_name(history->node)); return pcmk_rc_ok; } if (history->exit_status == history->expected_exit_status) { return pcmk_rc_undetermined; // Only failures expire } if (history->interval_ms == 0) { pcmk__notice("Ignoring resource history entry %s for %s of %s on %s: " "Expired failure", history->id, history->task, history->rsc->id, pcmk__node_name(history->node)); return pcmk_rc_ok; } if (history->node->details->online && !history->node->details->unclean) { /* Reschedule the recurring action. schedule_cancel() won't work at * this stage, so as a hacky workaround, forcibly change the restart * digest so pcmk__check_action_config() does what we want later. * * @TODO We should skip this if there is a newer successful monitor. * Also, this causes rescheduling only if the history entry * has a PCMK__XA_OP_DIGEST (which the expire-non-blocked-failure * scheduler regression test doesn't, but that may not be a * realistic scenario in production). */ pcmk__notice("Rescheduling %s-interval %s of %s on %s after failure " "expired", pcmk__readable_interval(history->interval_ms), history->task, history->rsc->id, pcmk__node_name(history->node)); pcmk__xe_set(history->xml, PCMK__XA_OP_RESTART_DIGEST, "calculated-failure-timeout"); return pcmk_rc_ok; } return pcmk_rc_undetermined; } /*! * \internal * \brief Process a masked probe failure * * \param[in,out] history Parsed action history entry * \param[in] orig_exit_status Action exit status before remapping * \param[in] last_failure Resource's last_failure entry, if known * \param[in,out] on_fail Resource's current failure handling */ static void mask_probe_failure(struct action_history *history, int orig_exit_status, const xmlNode *last_failure, enum pcmk__on_fail *on_fail) { pcmk_resource_t *ban_rsc = history->rsc; if (!pcmk__is_set(history->rsc->flags, pcmk__rsc_unique)) { ban_rsc = uber_parent(history->rsc); } pcmk__notice("Treating probe result '%s' for %s on %s as 'not running'", crm_exit_str(orig_exit_status), history->rsc->id, pcmk__node_name(history->node)); update_resource_state(history, history->expected_exit_status, last_failure, on_fail); pcmk__xe_set(history->xml, PCMK_XA_UNAME, history->node->priv->name); record_failed_op(history); resource_location(ban_rsc, history->node, -PCMK_SCORE_INFINITY, "masked-probe-failure", ban_rsc->priv->scheduler); } /*! * \internal Check whether a given failure is for a given pending action * * \param[in] history Parsed history entry for pending action * \param[in] last_failure Resource's last_failure entry, if known * * \return true if \p last_failure is failure of pending action in \p history, * otherwise false * \note Both \p history and \p last_failure must come from the same * \c PCMK__XE_LRM_RESOURCE block, as node and resource are assumed to be * the same. */ static bool failure_is_newer(const struct action_history *history, const xmlNode *last_failure) { guint failure_interval_ms = 0U; long long failure_change = 0LL; long long this_change = 0LL; if (last_failure == NULL) { return false; // Resource has no last_failure entry } if (!pcmk__str_eq(history->task, pcmk__xe_get(last_failure, PCMK_XA_OPERATION), pcmk__str_none)) { return false; // last_failure is for different action } if ((pcmk__xe_get_guint(last_failure, PCMK_META_INTERVAL, &failure_interval_ms) != pcmk_rc_ok) || (history->interval_ms != failure_interval_ms)) { return false; // last_failure is for action with different interval } if ((pcmk__scan_ll(pcmk__xe_get(history->xml, PCMK_XA_LAST_RC_CHANGE), &this_change, 0LL) != pcmk_rc_ok) || (pcmk__scan_ll(pcmk__xe_get(last_failure, PCMK_XA_LAST_RC_CHANGE), &failure_change, 0LL) != pcmk_rc_ok) || (failure_change < this_change)) { return false; // Failure is not known to be newer } return true; } /*! * \internal * \brief Update a resource's role etc. for a pending action * * \param[in,out] history Parsed history entry for pending action * \param[in] last_failure Resource's last_failure entry, if known */ static void process_pending_action(struct action_history *history, const xmlNode *last_failure) { /* For recurring monitors, a failure is recorded only in RSC_last_failure_0, * and there might be a RSC_monitor_INTERVAL entry with the last successful * or pending result. * * If last_failure contains the failure of the pending recurring monitor * we're processing here, and is newer, the action is no longer pending. * (Pending results have call ID -1, which sorts last, so the last failure * if any should be known.) */ if (failure_is_newer(history, last_failure)) { return; } if (strcmp(history->task, PCMK_ACTION_START) == 0) { pcmk__set_rsc_flags(history->rsc, pcmk__rsc_start_pending); set_active(history->rsc); } else if (strcmp(history->task, PCMK_ACTION_PROMOTE) == 0) { history->rsc->priv->orig_role = pcmk_role_promoted; } else if ((strcmp(history->task, PCMK_ACTION_MIGRATE_TO) == 0) && history->node->details->unclean) { /* A migrate_to action is pending on a unclean source, so force a stop * on the target. */ const char *migrate_target = NULL; pcmk_node_t *target = NULL; migrate_target = pcmk__xe_get(history->xml, PCMK__META_MIGRATE_TARGET); target = pcmk_find_node(history->rsc->priv->scheduler, migrate_target); if (target != NULL) { stop_action(history->rsc, target, FALSE); } } if (history->rsc->priv->pending_action != NULL) { /* There should never be multiple pending actions, but as a failsafe, * just remember the first one processed for display purposes. */ return; } if (pcmk_is_probe(history->task, history->interval_ms)) { /* Pending probes are currently never displayed, even if pending * operations are requested. If we ever want to change that, * enable the below and the corresponding part of * native.c:native_pending_action(). */ #if 0 history->rsc->private->pending_action = strdup("probe"); history->rsc->private->pending_node = history->node; #endif } else { history->rsc->priv->pending_action = strdup(history->task); history->rsc->priv->pending_node = history->node; } } static void unpack_rsc_op(pcmk_resource_t *rsc, pcmk_node_t *node, xmlNode *xml_op, xmlNode **last_failure, enum pcmk__on_fail *on_fail) { int old_rc = 0; bool expired = false; pcmk_resource_t *parent = rsc; enum rsc_role_e fail_role = pcmk_role_unknown; enum pcmk__on_fail failure_strategy = pcmk__on_fail_restart; struct action_history history = { .rsc = rsc, .node = node, .xml = xml_op, .execution_status = PCMK_EXEC_UNKNOWN, }; CRM_CHECK(rsc && node && xml_op, return); history.id = pcmk__xe_id(xml_op); if (history.id == NULL) { pcmk__config_err("Ignoring resource history entry for %s on %s " "without ID", rsc->id, pcmk__node_name(node)); return; } // Task and interval history.task = pcmk__xe_get(xml_op, PCMK_XA_OPERATION); if (history.task == NULL) { pcmk__config_err("Ignoring resource history entry %s for %s on %s " "without " PCMK_XA_OPERATION, history.id, rsc->id, pcmk__node_name(node)); return; } pcmk__xe_get_guint(xml_op, PCMK_META_INTERVAL, &(history.interval_ms)); if (!can_affect_state(&history)) { pcmk__rsc_trace(rsc, "Ignoring resource history entry %s for %s on %s " "with irrelevant action '%s'", history.id, rsc->id, pcmk__node_name(node), history.task); return; } if (unpack_action_result(&history) != pcmk_rc_ok) { return; // Error already logged } history.expected_exit_status = pe__target_rc_from_xml(xml_op); history.key = pcmk__xe_history_key(xml_op); pcmk__xe_get_int(xml_op, PCMK__XA_CALL_ID, &(history.call_id)); pcmk__rsc_trace(rsc, "Unpacking %s (%s call %d on %s): %s (%s)", history.id, history.task, history.call_id, pcmk__node_name(node), pcmk_exec_status_str(history.execution_status), crm_exit_str(history.exit_status)); if (node->details->unclean) { pcmk__rsc_trace(rsc, "%s is running on %s, which is unclean (further action " "depends on value of stop's on-fail attribute)", rsc->id, pcmk__node_name(node)); } expired = check_operation_expiry(&history); old_rc = history.exit_status; remap_operation(&history, on_fail, expired); if (expired && (process_expired_result(&history, old_rc) == pcmk_rc_ok)) { goto done; } if (!pcmk__is_bundled(rsc) && pcmk_xe_mask_probe_failure(xml_op)) { mask_probe_failure(&history, old_rc, *last_failure, on_fail); goto done; } if (!pcmk__is_set(rsc->flags, pcmk__rsc_unique)) { parent = uber_parent(rsc); } switch (history.execution_status) { case PCMK_EXEC_PENDING: process_pending_action(&history, *last_failure); goto done; case PCMK_EXEC_DONE: update_resource_state(&history, history.exit_status, *last_failure, on_fail); goto done; case PCMK_EXEC_NOT_INSTALLED: unpack_failure_handling(&history, &failure_strategy, &fail_role); if (failure_strategy == pcmk__on_fail_ignore) { pcmk__warn("Cannot ignore failed %s of %s on %s: Resource " "agent doesn't exist " QB_XS " status=%d rc=%d id=%s", history.task, rsc->id, pcmk__node_name(node), history.execution_status, history.exit_status, history.id); /* Also for printing it as "FAILED" by marking it as * pcmk__rsc_failed later */ *on_fail = pcmk__on_fail_ban; } resource_location(parent, node, -PCMK_SCORE_INFINITY, "hard-error", rsc->priv->scheduler); unpack_rsc_op_failure(&history, failure_strategy, fail_role, last_failure, on_fail); goto done; case PCMK_EXEC_NOT_CONNECTED: if (pcmk__is_pacemaker_remote_node(node) && pcmk__is_set(node->priv->remote->flags, pcmk__rsc_managed)) { /* We should never get into a situation where a managed remote * connection resource is considered OK but a resource action * behind the connection gets a "not connected" status. But as a * fail-safe in case a bug or unusual circumstances do lead to * that, ensure the remote connection is considered failed. */ pcmk__set_rsc_flags(node->priv->remote, pcmk__rsc_failed|pcmk__rsc_stop_if_failed); } break; // Not done, do error handling case PCMK_EXEC_ERROR: case PCMK_EXEC_ERROR_HARD: case PCMK_EXEC_ERROR_FATAL: case PCMK_EXEC_TIMEOUT: case PCMK_EXEC_NOT_SUPPORTED: case PCMK_EXEC_INVALID: break; // Not done, do error handling default: // No other value should be possible at this point break; } unpack_failure_handling(&history, &failure_strategy, &fail_role); if ((failure_strategy == pcmk__on_fail_ignore) || ((failure_strategy == pcmk__on_fail_restart_container) && (strcmp(history.task, PCMK_ACTION_STOP) == 0))) { char *last_change_s = last_change_str(xml_op); pcmk__warn("Pretending failed %s (%s%s%s) of %s on %s at %s succeeded " QB_XS " %s", history.task, crm_exit_str(history.exit_status), (pcmk__str_empty(history.exit_reason)? "" : ": "), pcmk__s(history.exit_reason, ""), rsc->id, pcmk__node_name(node), last_change_s, history.id); free(last_change_s); update_resource_state(&history, history.expected_exit_status, *last_failure, on_fail); pcmk__xe_set(xml_op, PCMK_XA_UNAME, node->priv->name); pcmk__set_rsc_flags(rsc, pcmk__rsc_ignore_failure); record_failed_op(&history); if ((failure_strategy == pcmk__on_fail_restart_container) && (*on_fail <= pcmk__on_fail_restart)) { *on_fail = failure_strategy; } } else { unpack_rsc_op_failure(&history, failure_strategy, fail_role, last_failure, on_fail); if (history.execution_status == PCMK_EXEC_ERROR_HARD) { uint8_t log_level = LOG_ERR; if (history.exit_status == PCMK_OCF_NOT_INSTALLED) { log_level = LOG_NOTICE; } do_crm_log(log_level, "Preventing %s from restarting on %s because " "of hard failure (%s%s%s) " QB_XS " %s", parent->id, pcmk__node_name(node), crm_exit_str(history.exit_status), (pcmk__str_empty(history.exit_reason)? "" : ": "), pcmk__s(history.exit_reason, ""), history.id); resource_location(parent, node, -PCMK_SCORE_INFINITY, "hard-error", rsc->priv->scheduler); } else if (history.execution_status == PCMK_EXEC_ERROR_FATAL) { pcmk__sched_err(rsc->priv->scheduler, "Preventing %s from restarting anywhere because " "of fatal failure (%s%s%s) " QB_XS " %s", parent->id, crm_exit_str(history.exit_status), (pcmk__str_empty(history.exit_reason)? "" : ": "), pcmk__s(history.exit_reason, ""), history.id); resource_location(parent, NULL, -PCMK_SCORE_INFINITY, "fatal-error", rsc->priv->scheduler); } } done: pcmk__rsc_trace(rsc, "%s role on %s after %s is %s (next %s)", rsc->id, pcmk__node_name(node), history.id, pcmk_role_text(rsc->priv->orig_role), pcmk_role_text(rsc->priv->next_role)); } /*! * \internal * \brief Insert a node attribute with value into a \c GHashTable * * \param[in,out] key Key to insert (either freed or owned by * \p user_data upon return) * \param[in] value Value to insert (owned by \p user_data upon return) * \param[in] user_data \c GHashTable to insert into */ static gboolean insert_attr(gpointer key, gpointer value, gpointer user_data) { GHashTable *table = user_data; g_hash_table_insert(table, key, value); return TRUE; } static void add_node_attrs(const xmlNode *xml_obj, pcmk_node_t *node, bool overwrite, pcmk_scheduler_t *scheduler) { const char *cluster_name = NULL; const char *dc_id = pcmk__xe_get(scheduler->input, PCMK_XA_DC_UUID); const pcmk_rule_input_t rule_input = { .now = scheduler->priv->now, }; pcmk__insert_dup(node->priv->attrs, CRM_ATTR_UNAME, node->priv->name); pcmk__insert_dup(node->priv->attrs, CRM_ATTR_ID, node->priv->id); if ((scheduler->dc_node == NULL) && pcmk__str_eq(node->priv->id, dc_id, pcmk__str_casei)) { scheduler->dc_node = node; pcmk__insert_dup(node->priv->attrs, CRM_ATTR_IS_DC, PCMK_VALUE_TRUE); } else if (!pcmk__same_node(node, scheduler->dc_node)) { pcmk__insert_dup(node->priv->attrs, CRM_ATTR_IS_DC, PCMK_VALUE_FALSE); } cluster_name = g_hash_table_lookup(scheduler->priv->options, PCMK_OPT_CLUSTER_NAME); if (cluster_name) { pcmk__insert_dup(node->priv->attrs, CRM_ATTR_CLUSTER_NAME, cluster_name); } if (overwrite) { /* @TODO Try to reorder some unpacking so that we don't need the * overwrite argument or to unpack into a temporary table */ GHashTable *unpacked = pcmk__strkey_table(free, free); pe__unpack_dataset_nvpairs(xml_obj, PCMK_XE_INSTANCE_ATTRIBUTES, &rule_input, unpacked, NULL, scheduler); g_hash_table_foreach_steal(unpacked, insert_attr, node->priv->attrs); g_hash_table_destroy(unpacked); } else { pe__unpack_dataset_nvpairs(xml_obj, PCMK_XE_INSTANCE_ATTRIBUTES, &rule_input, node->priv->attrs, NULL, scheduler); } pe__unpack_dataset_nvpairs(xml_obj, PCMK_XE_UTILIZATION, &rule_input, node->priv->utilization, NULL, scheduler); if (pcmk__node_attr(node, CRM_ATTR_SITE_NAME, NULL, pcmk__rsc_node_current) == NULL) { const char *site_name = pcmk__node_attr(node, "site-name", NULL, pcmk__rsc_node_current); if (site_name) { pcmk__insert_dup(node->priv->attrs, CRM_ATTR_SITE_NAME, site_name); } else if (cluster_name) { /* Default to cluster-name if unset */ pcmk__insert_dup(node->priv->attrs, CRM_ATTR_SITE_NAME, cluster_name); } } } static GList * extract_operations(const char *node, const char *rsc, xmlNode * rsc_entry, gboolean active_filter) { int counter = -1; int stop_index = -1; int start_index = -1; xmlNode *rsc_op = NULL; GList *gIter = NULL; GList *op_list = NULL; GList *sorted_op_list = NULL; /* extract operations */ op_list = NULL; sorted_op_list = NULL; for (rsc_op = pcmk__xe_first_child(rsc_entry, PCMK__XE_LRM_RSC_OP, NULL, NULL); rsc_op != NULL; rsc_op = pcmk__xe_next(rsc_op, PCMK__XE_LRM_RSC_OP)) { pcmk__xe_set(rsc_op, PCMK_XA_RESOURCE, rsc); pcmk__xe_set(rsc_op, PCMK_XA_UNAME, node); op_list = g_list_prepend(op_list, rsc_op); } if (op_list == NULL) { /* if there are no operations, there is nothing to do */ return NULL; } sorted_op_list = g_list_sort(op_list, sort_op_by_callid); /* create active recurring operations as optional */ if (active_filter == FALSE) { return sorted_op_list; } op_list = NULL; calculate_active_ops(sorted_op_list, &start_index, &stop_index); for (gIter = sorted_op_list; gIter != NULL; gIter = gIter->next) { xmlNode *rsc_op = (xmlNode *) gIter->data; counter++; if (start_index < stop_index) { pcmk__trace("Skipping %s: not active", pcmk__xe_id(rsc_entry)); break; } else if (counter < start_index) { pcmk__trace("Skipping %s: old", pcmk__xe_id(rsc_op)); continue; } op_list = g_list_append(op_list, rsc_op); } g_list_free(sorted_op_list); return op_list; } GList * find_operations(const char *rsc, const char *node, gboolean active_filter, pcmk_scheduler_t *scheduler) { GList *output = NULL; GList *intermediate = NULL; xmlNode *tmp = NULL; xmlNode *status = pcmk__xe_first_child(scheduler->input, PCMK_XE_STATUS, NULL, NULL); pcmk_node_t *this_node = NULL; xmlNode *node_state = NULL; CRM_CHECK(status != NULL, return NULL); for (node_state = pcmk__xe_first_child(status, PCMK__XE_NODE_STATE, NULL, NULL); node_state != NULL; node_state = pcmk__xe_next(node_state, PCMK__XE_NODE_STATE)) { const char *uname = pcmk__xe_get(node_state, PCMK_XA_UNAME); if (node != NULL && !pcmk__str_eq(uname, node, pcmk__str_casei)) { continue; } this_node = pcmk_find_node(scheduler, uname); if(this_node == NULL) { CRM_LOG_ASSERT(this_node != NULL); continue; } else if (pcmk__is_pacemaker_remote_node(this_node)) { determine_remote_online_status(scheduler, this_node); } else { determine_online_status(node_state, this_node, scheduler); } if (this_node->details->online || pcmk__is_set(scheduler->flags, pcmk__sched_fencing_enabled)) { /* offline nodes run no resources... * unless stonith is enabled in which case we need to * make sure rsc start events happen after the stonith */ xmlNode *lrm_rsc = NULL; tmp = pcmk__xe_first_child(node_state, PCMK__XE_LRM, NULL, NULL); tmp = pcmk__xe_first_child(tmp, PCMK__XE_LRM_RESOURCES, NULL, NULL); for (lrm_rsc = pcmk__xe_first_child(tmp, PCMK__XE_LRM_RESOURCE, NULL, NULL); lrm_rsc != NULL; lrm_rsc = pcmk__xe_next(lrm_rsc, PCMK__XE_LRM_RESOURCE)) { const char *rsc_id = pcmk__xe_get(lrm_rsc, PCMK_XA_ID); if ((rsc != NULL) && !pcmk__str_eq(rsc_id, rsc, pcmk__str_none)) { continue; } intermediate = extract_operations(uname, rsc_id, lrm_rsc, active_filter); output = g_list_concat(output, intermediate); } } } return output; } diff --git a/tools/cibadmin.c b/tools/cibadmin.c index 449b329a54..ee5562f344 100644 --- a/tools/cibadmin.c +++ b/tools/cibadmin.c @@ -1,900 +1,900 @@ /* * 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 // pcmk_acl_required() #include #include #include #include #include #define SUMMARY "query and edit the Pacemaker configuration" #define INDENT " " enum cibadmin_section_type { cibadmin_section_all = 0, cibadmin_section_scope, cibadmin_section_xpath, }; static cib_t *the_cib = NULL; static crm_exit_t exit_code = CRM_EX_OK; static struct { const char *cib_action; int cmd_options; enum cibadmin_section_type section_type; char *cib_section; char *validate_with; gint message_timeout_sec; enum pcmk__acl_render_how acl_render_mode; gchar *cib_user; gchar *dest_node; gchar *input_file; gchar *input_xml; gboolean input_stdin; bool delete_all; gboolean allow_create; gboolean force; gboolean get_node_path; gboolean no_children; gboolean score_update; /* @COMPAT: For "-!" version option. Not advertised nor marked as * deprecated, but accepted. */ gboolean extended_version; // @COMPAT Deprecated since 3.0.0 gboolean local; // @COMPAT Deprecated since 3.0.1 gboolean sync_call; } options = { .cmd_options = cib_sync_call, }; int do_init(void); static int do_work(xmlNode *input, xmlNode **output); static void print_xml_output(xmlNode * xml) { if (!xml) { return; } else if (xml->type != XML_ELEMENT_NODE) { return; } if (pcmk__is_set(options.cmd_options, cib_xpath_address)) { const char *id = pcmk__xe_get(xml, PCMK_XA_ID); if (pcmk__xe_is(xml, PCMK__XE_XPATH_QUERY)) { xmlNode *child = NULL; for (child = xml->children; child; child = child->next) { print_xml_output(child); } } else if (id) { printf("%s\n", id); } } else { GString *buf = g_string_sized_new(1024); pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buf, 0); fprintf(stdout, "%s", buf->str); g_string_free(buf, TRUE); } } // Upgrade requested but already at latest schema static void report_schema_unchanged(void) { const char *err = pcmk_rc_str(pcmk_rc_schema_unchanged); pcmk__info("Upgrade unnecessary: %s\n", err); printf("Upgrade unnecessary: %s\n", err); exit_code = CRM_EX_OK; } /*! * \internal * \brief Check whether the current CIB action is dangerous * \return true if \p options.cib_action is dangerous, or false otherwise */ static inline bool cib_action_is_dangerous(void) { /* @TODO Ideally, --upgrade wouldn't be considered dangerous if the CIB * already uses the latest schema. */ return options.delete_all || pcmk__str_any_of(options.cib_action, PCMK__CIB_REQUEST_UPGRADE, PCMK__CIB_REQUEST_ERASE, NULL); } /*! * \internal * \brief Determine whether the given CIB scope is valid for \p cibadmin * * \param[in] scope Scope to validate * * \return true if \p scope is valid, or false otherwise * \note An invalid scope applies the operation to the entire CIB. */ static inline bool scope_is_valid(const char *scope) { return pcmk__str_any_of(scope, PCMK_XE_CONFIGURATION, PCMK_XE_NODES, PCMK_XE_RESOURCES, PCMK_XE_CONSTRAINTS, PCMK_XE_CRM_CONFIG, PCMK_XE_RSC_DEFAULTS, PCMK_XE_OP_DEFAULTS, PCMK_XE_ACLS, PCMK_XE_FENCING_TOPOLOGY, PCMK_XE_TAGS, PCMK_XE_ALERTS, PCMK_XE_STATUS, NULL); } static gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.delete_all = false; if (pcmk__str_any_of(option_name, "-u", "--upgrade", NULL)) { options.cib_action = PCMK__CIB_REQUEST_UPGRADE; } else if (pcmk__str_any_of(option_name, "-Q", "--query", NULL)) { options.cib_action = PCMK__CIB_REQUEST_QUERY; } else if (pcmk__str_any_of(option_name, "-E", "--erase", NULL)) { options.cib_action = PCMK__CIB_REQUEST_ERASE; } else if (pcmk__str_any_of(option_name, "-B", "--bump", NULL)) { options.cib_action = PCMK__CIB_REQUEST_BUMP; } else if (pcmk__str_any_of(option_name, "-C", "--create", NULL)) { options.cib_action = PCMK__CIB_REQUEST_CREATE; } else if (pcmk__str_any_of(option_name, "-M", "--modify", NULL)) { options.cib_action = PCMK__CIB_REQUEST_MODIFY; } else if (pcmk__str_any_of(option_name, "-P", "--patch", NULL)) { options.cib_action = PCMK__CIB_REQUEST_APPLY_PATCH; } else if (pcmk__str_any_of(option_name, "-R", "--replace", NULL)) { options.cib_action = PCMK__CIB_REQUEST_REPLACE; } else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) { options.cib_action = PCMK__CIB_REQUEST_DELETE; } else if (pcmk__str_any_of(option_name, "-d", "--delete-all", NULL)) { options.cib_action = PCMK__CIB_REQUEST_DELETE; options.delete_all = true; } else if (pcmk__str_any_of(option_name, "-a", "--empty", NULL)) { options.cib_action = "empty"; pcmk__str_update(&options.validate_with, optarg); } else if (pcmk__str_any_of(option_name, "-5", "--md5-sum", NULL)) { options.cib_action = "md5-sum"; } else if (pcmk__str_any_of(option_name, "-6", "--md5-sum-versioned", NULL)) { options.cib_action = "md5-sum-versioned"; } else { // Should be impossible return FALSE; } return TRUE; } static gboolean show_access_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_eq(optarg, "auto", pcmk__str_null_matches)) { options.acl_render_mode = pcmk__acl_render_default; } else if (g_strcmp0(optarg, "namespace") == 0) { options.acl_render_mode = pcmk__acl_render_namespace; } else if (g_strcmp0(optarg, "text") == 0) { options.acl_render_mode = pcmk__acl_render_text; } else if (g_strcmp0(optarg, "color") == 0) { options.acl_render_mode = pcmk__acl_render_color; } else { g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "Invalid value '%s' for option '%s'", optarg, option_name); return FALSE; } return TRUE; } static gboolean section_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "-o", "--scope", NULL)) { options.section_type = cibadmin_section_scope; } else if (pcmk__str_any_of(option_name, "-A", "--xpath", NULL)) { options.section_type = cibadmin_section_xpath; } else { // Should be impossible return FALSE; } pcmk__str_update(&options.cib_section, optarg); return TRUE; } static GOptionEntry command_entries[] = { { "upgrade", 'u', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Upgrade the configuration to the latest syntax", NULL }, { "query", 'Q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Query the contents of the CIB", NULL }, { "erase", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Erase the contents of the whole CIB", NULL }, { "bump", 'B', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Increase the CIB's epoch value by 1", NULL }, { "create", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Create an object in the CIB (will fail if object already exists)", NULL }, { "modify", 'M', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Find object somewhere in CIB's XML tree and update it (fails if object " "does not exist unless -c is also specified)", NULL }, { "patch", 'P', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Supply an update in the form of an XML diff (see crm_diff(8))", NULL }, { "replace", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Recursively replace an object in the CIB", NULL }, { "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Delete first object matching supplied criteria (for example, " "<" PCMK_XE_OP " " PCMK_XA_ID "=\"rsc1_op1\" " PCMK_XA_NAME "=\"monitor\"/>).\n" INDENT "The XML element name and all attributes must match in order for " "the element to be deleted.", NULL }, { "delete-all", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "When used with --xpath, remove all matching objects in the " "configuration instead of just the first one", NULL }, { "empty", 'a', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Output an empty CIB. Accepts an optional schema name argument to use as " "the " PCMK_XA_VALIDATE_WITH " value.\n" INDENT "If no schema is given, the latest will be used.", "[schema]" }, { "md5-sum", '5', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Calculate the on-disk CIB digest", NULL }, { "md5-sum-versioned", '6', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Calculate an on-the-wire versioned CIB digest", NULL }, { NULL } }; static GOptionEntry data_entries[] = { /* @COMPAT: These arguments should be last-wins. We can have an enum option * that stores the input type, along with a single string option that stores * the XML string for --xml-text, filename for --xml-file, or NULL for * --xml-pipe. */ { "xml-text", 'X', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.input_xml, "Retrieve XML from the supplied string", "value" }, { "xml-file", 'x', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &options.input_file, "Retrieve XML from the named file", "value" }, { "xml-pipe", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.input_stdin, "Retrieve XML from stdin", NULL }, { NULL } }; static GOptionEntry addl_entries[] = { { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force, "Force the action to be performed", NULL }, { "timeout", 't', G_OPTION_FLAG_NONE, G_OPTION_ARG_INT, &options.message_timeout_sec, "Time (in seconds) to wait before declaring the operation failed", "value" }, { "user", 'U', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.cib_user, "Run the command with permissions of the named user (valid only for the " "root and " CRM_DAEMON_USER " accounts)", "value" }, { "scope", 'o', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb, "Limit scope of operation to specific section of CIB\n" INDENT "Valid values: " PCMK_XE_CONFIGURATION ", " PCMK_XE_NODES ", " PCMK_XE_RESOURCES ", " PCMK_XE_CONSTRAINTS ", " PCMK_XE_CRM_CONFIG ", " PCMK_XE_RSC_DEFAULTS ",\n" INDENT " " PCMK_XE_OP_DEFAULTS ", " PCMK_XE_ACLS ", " PCMK_XE_FENCING_TOPOLOGY ", " PCMK_XE_TAGS ", " PCMK_XE_ALERTS ", " PCMK_XE_STATUS "\n" INDENT "If both --scope/-o and --xpath/-a are specified, the last one to " "appear takes effect", "value" }, { "xpath", 'A', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb, "A valid XPath to use instead of --scope/-o\n" INDENT "If both --scope/-o and --xpath/-a are specified, the last one to " "appear takes effect", "value" }, { "node-path", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.get_node_path, "When performing XPath queries, return paths of any matches found\n" INDENT "(for example, " "\"/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RESOURCES "/" PCMK_XE_CLONE "[@" PCMK_XA_ID "='dummy-clone']" "/" PCMK_XE_PRIMITIVE "[@" PCMK_XA_ID "='dummy']\")", NULL }, { "show-access", 'S', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, show_access_cb, "Whether to use syntax highlighting for ACLs (with -Q/--query and " "-U/--user)\n" INDENT "Allowed values: 'color' (default for terminal), 'text' (plain text, " "default for non-terminal),\n" INDENT " 'namespace', or 'auto' (use default value)\n" INDENT "Default value: 'auto'", "[value]" }, { "score", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.score_update, "Treat new attribute values as atomic score updates where possible " "(with --modify/-M).\n" INDENT "This currently happens by default and cannot be disabled, but\n" INDENT "this default behavior is deprecated and will be removed in a\n" INDENT "future release. Set this flag if this behavior is desired.\n" INDENT "This option takes effect when updating XML attributes. For an\n" INDENT "attribute named \"name\", if the new value is \"name++\" or\n" INDENT "\"name+=X\" for some score X, the new value is set as follows:\n" INDENT "If attribute \"name\" is not already set to some value in\n" INDENT "the element being updated, the new value is set as a literal\n" INDENT "string.\n" INDENT "If the new value is \"name++\", then the attribute is set to \n" INDENT "its existing value (parsed as a score) plus 1.\n" INDENT "If the new value is \"name+=X\" for some score X, then the\n" INDENT "attribute is set to its existing value plus X, where the\n" INDENT "existing value and X are parsed and added as scores.\n" INDENT "Scores are integer values capped at INFINITY and -INFINITY.\n" INDENT "Refer to Pacemaker Explained for more details on scores,\n" INDENT "including how they are parsed and added.", NULL }, { "allow-create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.allow_create, "(Advanced) Allow target of --modify/-M to be created if it does not " "exist", NULL }, { "no-children", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.no_children, "(Advanced) When querying an object, do not include its children in the " "result", NULL }, { "node", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.dest_node, "(Advanced) Send command to the specified host", "value" }, // @COMPAT Deprecated since 3.0.0 { "local", 'l', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.local, "(deprecated)", NULL }, // @COMPAT Deprecated since 3.0.1 { "sync-call", 's', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.sync_call, "(deprecated)", NULL }, { NULL } }; static GOptionContext * build_arg_context(pcmk__common_args_t *args) { const char *desc = NULL; GOptionContext *context = NULL; GOptionEntry extra_prog_entries[] = { // @COMPAT: Deprecated { "extended-version", '!', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.extended_version, "deprecated", NULL }, { NULL } }; desc = "Examples:\n\n" "Query the configuration:\n\n" "\t# cibadmin --query\n\n" "Query just the cluster options configuration:\n\n" "\t# cibadmin --query --scope " PCMK_XE_CRM_CONFIG "\n\n" "Query all '" PCMK_META_TARGET_ROLE "' settings:\n\n" "\t# cibadmin --query --xpath " "\"//" PCMK_XE_NVPAIR "[@" PCMK_XA_NAME "='" PCMK_META_TARGET_ROLE"']\"\n\n" "Remove all '" PCMK_META_IS_MANAGED "' settings:\n\n" "\t# cibadmin --delete-all --xpath " "\"//" PCMK_XE_NVPAIR "[@" PCMK_XA_NAME "='" PCMK_META_IS_MANAGED "']\"\n\n" "Remove the resource named 'old':\n\n" "\t# cibadmin --delete --xml-text " "'<" PCMK_XE_PRIMITIVE " " PCMK_XA_ID "=\"old\"/>'\n\n" "Remove all resources from the configuration:\n\n" "\t# cibadmin --replace --scope " PCMK_XE_RESOURCES " --xml-text '<" PCMK_XE_RESOURCES "/>'\n\n" "Replace complete configuration with contents of " "$HOME/pacemaker.xml:\n\n" "\t# cibadmin --replace --xml-file $HOME/pacemaker.xml\n\n" "Replace " PCMK_XE_CONSTRAINTS " section of configuration with " "contents of $HOME/constraints.xml:\n\n" "\t# cibadmin --replace --scope " PCMK_XE_CONSTRAINTS " --xml-file $HOME/constraints.xml\n\n" "Increase configuration version to prevent old configurations from " "being loaded accidentally:\n\n" "\t# cibadmin --modify --score --xml-text " "'<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH "=\"" PCMK_XA_ADMIN_EPOCH "++\"/>'\n\n" "Edit the configuration with your favorite $EDITOR:\n\n" "\t# cibadmin --query > $HOME/local.xml\n\n" "\t# $EDITOR $HOME/local.xml\n\n" "\t# cibadmin --replace --xml-file $HOME/local.xml\n\n" "Assuming terminal, render configuration in color (green for " "writable, blue for readable, red for\n" "denied) to visualize permissions for user tony:\n\n" "\t# cibadmin --show-access=color --query --user tony | less -r\n\n" "SEE ALSO:\n" " crm(8), pcs(8), crm_shadow(8), crm_diff(8)\n"; context = pcmk__build_arg_context(args, NULL, NULL, ""); g_option_context_set_description(context, desc); pcmk__add_main_args(context, extra_prog_entries); pcmk__add_arg_group(context, "commands", "Commands:", "Show command help", command_entries); pcmk__add_arg_group(context, "data", "Data:", "Show data help", data_entries); pcmk__add_arg_group(context, "additional", "Additional Options:", "Show additional options", addl_entries); return context; } int main(int argc, char **argv) { int rc = pcmk_rc_ok; const char *source = NULL; xmlNode *output = NULL; xmlNode *input = NULL; gchar *acl_cred = NULL; GError *error = NULL; pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); gchar **processed_args = pcmk__cmdline_preproc(argv, "ANSUXhotx"); GOptionContext *context = build_arg_context(args); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } if (g_strv_length(processed_args) > 1) { gchar *help = g_option_context_get_help(context, TRUE, NULL); GString *extra = g_string_sized_new(128); for (int lpc = 1; processed_args[lpc] != NULL; lpc++) { if (extra->len > 0) { g_string_append_c(extra, ' '); } g_string_append(extra, processed_args[lpc]); } exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "non-option ARGV-elements: %s\n\n%s", extra->str, help); g_free(help); g_string_free(extra, TRUE); goto done; } if (args->version || options.extended_version) { g_strfreev(processed_args); pcmk__free_arg_context(context); /* FIXME: When cibadmin is converted to use formatted output, this can * be replaced by out->version with the appropriate boolean flag. * * options.extended_version is deprecated and will be removed in a * future release. */ pcmk__cli_help(options.extended_version? '!' : 'v'); } /* At LOG_ERR, stderr for CIB calls is rather verbose. Several lines like * * (func@file:line) error: CIB failures * * In cibadmin we explicitly output the XML portion without the prefixes. So * we default to LOG_CRIT. */ pcmk__cli_init_logging("cibadmin", 0); set_crm_log_level(LOG_CRIT); if (args->verbosity > 0) { cib__set_call_options(options.cmd_options, crm_system_name, cib_verbose); for (int i = 0; i < args->verbosity; i++) { crm_bump_log_level(argc, argv); } } if (options.cib_action == NULL) { // @COMPAT: Create a default command if other tools have one gchar *help = g_option_context_get_help(context, TRUE, NULL); exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must specify a command option\n\n%s", help); g_free(help); goto done; } if (strcmp(options.cib_action, "empty") == 0) { // Output an empty CIB GString *buf = g_string_sized_new(1024); output = createEmptyCib(1); pcmk__xe_set(output, PCMK_XA_VALIDATE_WITH, options.validate_with); pcmk__xml_string(output, pcmk__xml_fmt_pretty, buf, 0); fprintf(stdout, "%s", buf->str); g_string_free(buf, TRUE); goto done; } if (cib_action_is_dangerous() && !options.force) { exit_code = CRM_EX_UNSAFE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "The supplied command is considered dangerous. To prevent " "accidental destruction of the cluster, the --force flag " "is required in order to proceed."); goto done; } if (options.message_timeout_sec < 1) { // Set default timeout options.message_timeout_sec = 30; } if (options.section_type == cibadmin_section_xpath) { // Enable getting section by XPath cib__set_call_options(options.cmd_options, crm_system_name, cib_xpath); } else if (options.section_type == cibadmin_section_scope) { if (!scope_is_valid(options.cib_section)) { // @COMPAT: Consider requiring --force to proceed fprintf(stderr, "Invalid value '%s' for '--scope'. Operation will apply " "to the entire CIB.\n", options.cib_section); } } if (options.allow_create) { // Allow target of --modify/-M to be created if it does not exist cib__set_call_options(options.cmd_options, crm_system_name, cib_can_create); } if (options.delete_all) { // With cibadmin_section_xpath, remove all matching objects cib__set_call_options(options.cmd_options, crm_system_name, cib_multiple); } if (options.get_node_path) { /* Enable getting node path of XPath query matches. * Meaningful only if options.section_type == cibadmin_section_xpath. */ cib__set_call_options(options.cmd_options, crm_system_name, cib_xpath_address); } if (options.no_children) { // When querying an object, don't include its children in the result cib__set_call_options(options.cmd_options, crm_system_name, cib_no_children); } if (options.input_file != NULL) { input = pcmk__xml_read(options.input_file); source = options.input_file; } else if (options.input_xml != NULL) { input = pcmk__xml_parse(options.input_xml); source = "input string"; } else if (options.input_stdin) { input = pcmk__xml_read(NULL); source = "STDIN"; } else if (options.acl_render_mode != pcmk__acl_render_none) { char *username = pcmk__uid2username(geteuid()); bool required = pcmk_acl_required(username); free(username); if (required) { if (options.force) { fprintf(stderr, "The supplied command can provide skewed" " result since it is run under user that also" " gets guarded per ACLs on their own right." " Continuing since --force flag was" " provided.\n"); } else { exit_code = CRM_EX_UNSAFE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "The supplied command can provide skewed result " "since it is run under user that also gets guarded " "per ACLs in their own right. To accept the risk " "of such a possible distortion (without even " "knowing it at this time), use the --force flag."); goto done; } } if (options.cib_user == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "The supplied command requires -U user specified."); goto done; } /* We already stopped/warned ACL-controlled users about consequences. * * Note: acl_cred takes ownership of options.cib_user here. * options.cib_user is set to NULL so that the CIB is obtained as the * user running the cibadmin command. The CIB must be obtained as a user * with full permissions in order to show the CIB correctly annotated * for the options.cib_user's permissions. */ acl_cred = options.cib_user; options.cib_user = NULL; } if (input != NULL) { - crm_log_xml_debug(input, "[admin input]"); + pcmk__log_xml_debug(input, "[admin input]"); } else if (source != NULL) { exit_code = CRM_EX_CONFIG; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Couldn't parse input from %s.", source); goto done; } if (pcmk__str_eq(options.cib_action, "md5-sum", pcmk__str_casei)) { char *digest = NULL; if (input == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Please supply XML to process with -X, -x, or -p"); goto done; } digest = pcmk__digest_on_disk_cib(input); fprintf(stderr, "Digest: "); fprintf(stdout, "%s\n", pcmk__s(digest, "")); free(digest); goto done; } else if (strcmp(options.cib_action, "md5-sum-versioned") == 0) { char *digest = NULL; if (input == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Please supply XML to process with -X, -x, or -p"); goto done; } digest = pcmk__digest_xml(input, true); fprintf(stdout, "%s\n", pcmk__s(digest, "")); free(digest); goto done; } else if (pcmk__str_eq(options.cib_action, PCMK__CIB_REQUEST_MODIFY, pcmk__str_none)) { /* @COMPAT When we drop default support for expansion in cibadmin, guard * with `if (options.score_update)` */ cib__set_call_options(options.cmd_options, crm_system_name, cib_score_update); } rc = do_init(); if (rc != pcmk_ok) { rc = pcmk_legacy2rc(rc); exit_code = pcmk_rc2exitc(rc); pcmk__err("Init failed, could not perform requested operations: %s", pcmk_rc_str(rc)); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Init failed, could not perform requested operations: %s", pcmk_rc_str(rc)); goto done; } rc = do_work(input, &output); rc = pcmk_legacy2rc(rc); if ((rc == pcmk_rc_schema_unchanged) && (strcmp(options.cib_action, PCMK__CIB_REQUEST_UPGRADE) == 0)) { report_schema_unchanged(); } else if (rc != pcmk_rc_ok) { pcmk__err("Call failed: %s", pcmk_rc_str(rc)); fprintf(stderr, "Call failed: %s\n", pcmk_rc_str(rc)); exit_code = pcmk_rc2exitc(rc); if (rc == pcmk_rc_schema_validation) { if (strcmp(options.cib_action, PCMK__CIB_REQUEST_UPGRADE) == 0) { xmlNode *obj = NULL; if (the_cib->cmds->query(the_cib, NULL, &obj, options.cmd_options) == pcmk_ok) { pcmk__update_schema(&obj, NULL, true, false); } pcmk__xml_free(obj); } else if (output != NULL) { // Show validation errors to stderr pcmk__validate_xml(output, NULL, NULL, NULL); } } } if ((output != NULL) && (options.acl_render_mode != pcmk__acl_render_none)) { xmlDoc *acl_evaled_doc; rc = pcmk__acl_annotate_permissions(acl_cred, output->doc, &acl_evaled_doc); if (rc == pcmk_rc_ok) { xmlChar *rendered = NULL; rc = pcmk__acl_evaled_render(acl_evaled_doc, options.acl_render_mode, &rendered); if (rc != pcmk_rc_ok) { exit_code = CRM_EX_CONFIG; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not render evaluated access: %s", pcmk_rc_str(rc)); goto done; } printf("%s\n", (char *) rendered); free(rendered); } else { exit_code = CRM_EX_CONFIG; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not evaluate access per request (%s, error: %s)", acl_cred, pcmk_rc_str(rc)); goto done; } } else if (output != NULL) { print_xml_output(output); } pcmk__trace("%s exiting normally", crm_system_name); done: g_strfreev(processed_args); pcmk__free_arg_context(context); g_free(options.cib_user); g_free(options.dest_node); g_free(options.input_file); g_free(options.input_xml); free(options.cib_section); free(options.validate_with); g_free(acl_cred); pcmk__xml_free(input); pcmk__xml_free(output); rc = cib__clean_up_connection(&the_cib); if (exit_code == CRM_EX_OK) { exit_code = pcmk_rc2exitc(rc); } pcmk__output_and_clear_error(&error, NULL); crm_exit(exit_code); } static int do_work(xmlNode *input, xmlNode **output) { /* construct the request */ the_cib->call_timeout = options.message_timeout_sec; if ((strcmp(options.cib_action, PCMK__CIB_REQUEST_REPLACE) == 0) && pcmk__xe_is(input, PCMK_XE_CIB)) { xmlNode *status = pcmk_find_cib_element(input, PCMK_XE_STATUS); if (status == NULL) { pcmk__xe_create(input, PCMK_XE_STATUS); } } pcmk__trace("Passing \"%s\" to variant_op...", options.cib_action); return cib_internal_op(the_cib, options.cib_action, options.dest_node, options.cib_section, input, output, options.cmd_options, options.cib_user); } int do_init(void) { int rc = pcmk_ok; the_cib = cib_new(); rc = cib__signon_attempts(the_cib, cib_command, 5); if (rc != pcmk_ok) { pcmk__err("Could not connect to the CIB: %s", pcmk_strerror(rc)); fprintf(stderr, "Could not connect to the CIB: %s\n", pcmk_strerror(rc)); } return rc; } diff --git a/tools/crm_diff.c b/tools/crm_diff.c index 9c2ec9d679..6f084651e8 100644 --- a/tools/crm_diff.c +++ b/tools/crm_diff.c @@ -1,368 +1,368 @@ /* * 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 General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include // bool #include // NULL, printf(), etc. #include // free() #include // GOption, etc. #include // xmlNode #include // xml_{create,apply}_patchset() #define SUMMARY "Compare two Pacemaker configurations (in XML format) to " \ "produce a custom diff-like output, or apply such an output " \ "as a patch" #define INDENT " " struct { gchar *source_file; gchar *target_file; gchar *source_string; gchar *target_string; bool patch; gboolean as_cib; gboolean no_version; gboolean use_stdin; //!< \deprecated } options; static gboolean patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.patch = true; g_free(options.target_file); options.target_file = g_strdup(optarg); return TRUE; } /* @COMPAT Use last-one-wins for original/new/patch input sources * * @COMPAT Precedence is --original-string > --stdin > --original. --stdin is * now deprecated and hidden, so we don't mention it in the help text. */ static GOptionEntry original_xml_entries[] = { { "original", 'o', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.source_file, "XML is contained in the named file. Currently --original-string\n" INDENT "overrides this. In a future release, the last one specified\n" INDENT "will be used.", "FILE" }, { "original-string", 'O', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, &options.source_string, "XML is contained in the supplied string. Currently this takes\n" INDENT "precedence over --original. In a future release, the last one\n" INDENT "release, the last one specified will be used.", "STRING" }, { NULL } }; /* @COMPAT Precedence is --original-string > --stdin > --original. --stdin is * now deprecated and hidden, so we don't mention it in the help text. */ static GOptionEntry operation_entries[] = { { "new", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.target_file, "Compare the original XML to the contents of the named file. Currently\n" INDENT "--new-string overrides this. In a future release, the last one\n" INDENT "specified will be used.", "FILE" }, { "new-string", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, &options.target_string, "Compare the original XML with the contents of the supplied string.\n" INDENT "Currently this takes precedence over --patch and --new. In a \n" INDENT "future release, the last one specified will be used.", "STRING" }, { "patch", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, patch_cb, "Patch the original XML with the contents of the named file. Currently\n" INDENT "--new-string and (if specified later) --new override the input\n" INDENT "source specified here. In a future release, the last one\n" INDENT "specified will be used. Note: even if this input source is\n" INDENT "overridden, the input source will be applied as a patch to the\n" INDENT "original XML.", "FILE" }, { NULL } }; static GOptionEntry addl_entries[] = { { "cib", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.as_cib, "Compare/patch the inputs as a CIB (includes version details)", NULL }, { "no-version", 'u', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.no_version, "Generate the difference without version details", NULL }, // @COMPAT Deprecated { "stdin", 's', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.use_stdin, "Get the original XML and new (or patch) XML from stdin. Currently\n" INDENT "--original-string and --new-string override this for original\n" INDENT "and new/patch XML, respectively. In a future release, the last\n" INDENT "one specified will be used.", NULL }, { NULL } }; /*! * \internal * \brief Create an XML patchset from the given source and target XML trees * * \param[in,out] out Output object * \param[in,out] source Source XML * \param[in,out] target Target XML * \param[in] as_cib If \c true, treat the XML trees as CIBs. In * particular, ignore attribute position changes, * include the target digest in the patchset, and log * the source and target CIB versions. * \param[in] no_version If \c true, ignore changes to the CIB version * (must be \c false if \p as_cib is \c true) * * \return Standard Pacemaker return code */ static int generate_patch(pcmk__output_t *out, xmlNode *source, xmlNode *target, bool as_cib, bool no_version) { static const char *const vfields[] = { PCMK_XA_ADMIN_EPOCH, PCMK_XA_EPOCH, PCMK_XA_NUM_UPDATES, }; xmlNode *patchset = NULL; GString *buffer = NULL; // Currently impossibly; just a reminder for when we move to libpacemaker pcmk__assert(!as_cib || !no_version); /* If we're ignoring the version, make the version information identical, so * it isn't detected as a change. */ if (no_version) { for (int i = 0; i < PCMK__NELEM(vfields); i++) { pcmk__xe_set(target, vfields[i], pcmk__xe_get(source, vfields[i])); } } if (as_cib) { pcmk__xml_doc_set_flags(target->doc, pcmk__xf_ignore_attr_pos); } pcmk__xml_mark_changes(source, target); - crm_log_xml_debug(target, "target"); + pcmk__log_xml_debug(target, "target"); patchset = xml_create_patchset(0, source, target, NULL, false); pcmk__log_xml_changes(LOG_INFO, target); pcmk__xml_commit_changes(target->doc); if (patchset == NULL) { return pcmk_rc_ok; // No changes } if (as_cib) { pcmk__xml_patchset_add_digest(patchset, target); } else if (no_version) { pcmk__xml_free(pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL, NULL)); } pcmk__log_xml_patchset(LOG_NOTICE, patchset); buffer = g_string_sized_new(1024); pcmk__xml_string(patchset, pcmk__xml_fmt_pretty, buffer, 0); out->output_xml(out, PCMK_XE_DIFF, buffer->str); pcmk__xml_free(patchset); g_string_free(buffer, TRUE); /* pcmk_rc_error means there's a non-empty diff. * @COMPAT Choose a more descriptive return code, like one that maps to * CRM_EX_DIGEST? */ return pcmk_rc_error; } static const pcmk__supported_format_t formats[] = { PCMK__SUPPORTED_FORMAT_NONE, PCMK__SUPPORTED_FORMAT_TEXT, PCMK__SUPPORTED_FORMAT_XML, { NULL, NULL, NULL } }; static GOptionContext * build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) { GOptionContext *context = NULL; const char *description = "Examples:\n\n" "Obtain the two different configuration files by running cibadmin on the two cluster setups to compare:\n\n" "\t# cibadmin --query > cib-old.xml\n\n" "\t# cibadmin --query > cib-new.xml\n\n" "Calculate and save the difference between the two files:\n\n" "\t# crm_diff --original cib-old.xml --new cib-new.xml > patch.xml\n\n" "Apply the patch to the original file:\n\n" "\t# crm_diff --original cib-old.xml --patch patch.xml > updated.xml\n\n" "Apply the patch to the running cluster:\n\n" "\t# cibadmin --patch -x patch.xml\n"; context = pcmk__build_arg_context(args, "text (default), xml", group, NULL); g_option_context_set_description(context, description); pcmk__add_arg_group(context, "xml", "Original XML:", "Show original XML options", original_xml_entries); pcmk__add_arg_group(context, "operation", "Operation:", "Show operation options", operation_entries); pcmk__add_arg_group(context, "additional", "Additional Options:", "Show additional options", addl_entries); return context; } int main(int argc, char **argv) { crm_exit_t exit_code = CRM_EX_OK; int rc = pcmk_rc_ok; xmlNode *source = NULL; xmlNode *target = NULL; pcmk__output_t *out = NULL; GError *error = NULL; GOptionGroup *output_group = NULL; pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); gchar **processed_args = pcmk__cmdline_preproc(argv, "nopNO"); GOptionContext *context = build_arg_context(args, &output_group); pcmk__register_formats(output_group, formats); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } pcmk__cli_init_logging("crm_diff", args->verbosity); rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv); if (rc != pcmk_rc_ok) { exit_code = CRM_EX_ERROR; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s", args->output_ty, pcmk_rc_str(rc)); goto done; } if (args->version) { out->version(out, false); goto done; } if (options.no_version) { if (options.as_cib) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "-u/--no-version incompatible with -c/--cib"); goto done; } if (options.patch) { out->err(out, "Warning: -u/--no-version ignored with -p/--patch"); } } if (options.source_string != NULL) { source = pcmk__xml_parse(options.source_string); } else if (options.use_stdin) { source = pcmk__xml_read(NULL); } else if (options.source_file != NULL) { source = pcmk__xml_read(options.source_file); } else { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Either --original or --original-string must be specified"); goto done; } if (options.target_string != NULL) { target = pcmk__xml_parse(options.target_string); } else if (options.use_stdin) { target = pcmk__xml_read(NULL); } else if (options.target_file != NULL) { target = pcmk__xml_read(options.target_file); } else { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Either --new, --new-string, or --patch must be specified"); goto done; } if (source == NULL) { exit_code = CRM_EX_DATAERR; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Failed to parse original XML"); goto done; } if (target == NULL) { exit_code = CRM_EX_DATAERR; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Failed to parse %s XML", (options.patch? "patch" : "new")); goto done; } if (options.patch) { rc = xml_apply_patchset(source, target, options.as_cib); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__RC_ERROR, rc, "Could not apply patch: %s", pcmk_rc_str(rc)); } else { GString *buffer = g_string_sized_new(1024); pcmk__xml_string(source, pcmk__xml_fmt_pretty, buffer, 0); out->output_xml(out, PCMK_XE_DIFF, buffer->str); g_string_free(buffer, TRUE); } } else { rc = generate_patch(out, source, target, options.as_cib, options.no_version); } exit_code = pcmk_rc2exitc(rc); done: g_strfreev(processed_args); pcmk__free_arg_context(context); g_free(options.source_file); g_free(options.target_file); g_free(options.source_string); g_free(options.target_string); pcmk__xml_free(source); pcmk__xml_free(target); pcmk__output_and_clear_error(&error, out); if (out != NULL) { out->finish(out, exit_code, true, NULL); pcmk__output_free(out); } crm_exit(exit_code); } diff --git a/tools/crm_resource_runtime.c b/tools/crm_resource_runtime.c index d3421ef5ed..7b02bf6891 100644 --- a/tools/crm_resource_runtime.c +++ b/tools/crm_resource_runtime.c @@ -1,2584 +1,2584 @@ /* * 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 // bool, true, false #include #include #include #include // xmlNode #include // xmlXPathObject, etc. #include #include #include #include // crm_meta_name(), etc. #include #include /*! * \internal * \brief Resource with list of node info objects for its active nodes */ struct rsc_node_info { const pcmk_resource_t *rsc; //!< Resource //! Nodes where \c rsc is active (list of node_info_t *) GList *list; }; /*! * \internal * \brief Prepend a given node to a resource node info object's list * * \param[in] data Node to prepend (const pcmk_node_t *) * \param[in,out] user_data Resource node info object whose list to prepend to * (struct rsc_node_info * * * \note This is suitable for use with \c g_list_foreach(). */ static void prepend_node_info(gpointer data, gpointer user_data) { const pcmk_node_t *node = data; struct rsc_node_info *rni = user_data; node_info_t *ni = NULL; pcmk__assert(rni->rsc != NULL); ni = pcmk__assert_alloc(1, sizeof(node_info_t)); ni->node_name = node->priv->name; ni->promoted = pcmk__is_set(rni->rsc->flags, pcmk__rsc_promotable) && (rni->rsc->priv->fns->state(rni->rsc, true) == pcmk_role_promoted); rni->list = g_list_prepend(rni->list, ni); } GList * cli_resource_search(const pcmk_resource_t *rsc, const char *requested_name) { const pcmk_resource_t *clone = NULL; struct rsc_node_info rni = { .rsc = rsc, .list = NULL, }; pcmk__assert(rsc != NULL); if (pcmk__is_clone(rsc)) { clone = rsc; } else { const pcmk_resource_t *parent = pe__const_top_resource(rsc, false); if (pcmk__is_clone(parent) && !pcmk__is_set(rsc->flags, pcmk__rsc_unique) && (rsc->priv->history_id != NULL) && pcmk__str_eq(requested_name, rsc->priv->history_id, pcmk__str_none) && !pcmk__str_eq(requested_name, rsc->id, pcmk__str_none)) { // The anonymous clone children's common ID is supplied clone = parent; } } if (clone == NULL) { g_list_foreach(rsc->priv->active_nodes, prepend_node_info, &rni); return rni.list; } for (const GList *iter = clone->priv->children; iter != NULL; iter = iter->next) { const pcmk_resource_t *child = iter->data; rni.rsc = child; g_list_foreach(child->priv->active_nodes, prepend_node_info, &rni); } return rni.list; } // \return Standard Pacemaker return code static int find_resource_attr(pcmk__output_t *out, cib_t * the_cib, const char *attr, const char *rsc, const char *attr_set_type, const char *set_name, const char *attr_id, const char *attr_name, xmlNode **result) { xmlNode *xml_search; int rc = pcmk_rc_ok; GString *xpath = NULL; const char *xpath_base = NULL; if (result) { *result = NULL; } if(the_cib == NULL) { return ENOTCONN; } xpath_base = pcmk_cib_xpath_for(PCMK_XE_RESOURCES); if (xpath_base == NULL) { pcmk__err(PCMK_XE_RESOURCES " CIB element not known (bug?)"); return ENOMSG; } xpath = g_string_sized_new(1024); pcmk__g_strcat(xpath, xpath_base, "//*[@" PCMK_XA_ID "=\"", rsc, "\"]", NULL); if (attr_set_type != NULL) { pcmk__g_strcat(xpath, "/", attr_set_type, NULL); if (set_name != NULL) { pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "=\"", set_name, "\"]", NULL); } } g_string_append(xpath, "//" PCMK_XE_NVPAIR); if (attr_id != NULL && attr_name!= NULL) { pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", attr_id, "' " "and @" PCMK_XA_NAME "='", attr_name, "']", NULL); } else if (attr_id != NULL) { pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", attr_id, "']", NULL); } else if (attr_name != NULL) { pcmk__g_strcat(xpath, "[@" PCMK_XA_NAME "='", attr_name, "']", NULL); } rc = the_cib->cmds->query(the_cib, xpath->str, &xml_search, cib_sync_call|cib_xpath); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { - crm_log_xml_debug(xml_search, "Match"); + pcmk__log_xml_debug(xml_search, "Match"); if (xml_search->children != NULL) { rc = ENOTUNIQ; pcmk__warn_multiple_name_matches(out, xml_search, attr_name); out->spacer(out); } } if (result) { *result = xml_search; } else { pcmk__xml_free(xml_search); } g_string_free(xpath, TRUE); return rc; } /* PRIVATE. Use the find_matching_attr_resources instead. */ static void find_matching_attr_resources_recursive(pcmk__output_t *out, GList /* */ **result, pcmk_resource_t *rsc, const char * attr_set, const char * attr_set_type, const char * attr_id, const char * attr_name, cib_t * cib, int depth) { int rc = pcmk_rc_ok; char *lookup_id = clone_strip(rsc->id); for (GList *gIter = rsc->priv->children; gIter != NULL; gIter = gIter->next) { find_matching_attr_resources_recursive(out, result, (pcmk_resource_t *) gIter->data, attr_set, attr_set_type, attr_id, attr_name, cib, depth+1); /* do it only once for clones */ if (pcmk__is_clone(rsc)) { break; } } rc = find_resource_attr(out, cib, PCMK_XA_ID, lookup_id, attr_set_type, attr_set, attr_id, attr_name, NULL); /* Post-order traversal. * The root is always on the list and it is the last item. */ if((0 == depth) || (pcmk_rc_ok == rc)) { /* push the head */ *result = g_list_append(*result, rsc); } free(lookup_id); } /* The result is a linearized pre-ordered tree of resources. */ static GList/**/ * find_matching_attr_resources(pcmk__output_t *out, pcmk_resource_t *rsc, const char * rsc_id, const char * attr_set, const char * attr_set_type, const char * attr_id, const char * attr_name, cib_t * cib, const char * cmd, gboolean force) { int rc = pcmk_rc_ok; char *lookup_id = NULL; GList * result = NULL; /* If --force is used, update only the requested resource (clone or primitive). * Otherwise, if the primitive has the attribute, use that. * Otherwise use the clone. */ if(force == TRUE) { return g_list_append(result, rsc); } if (pcmk__is_clone(rsc->priv->parent)) { int rc = find_resource_attr(out, cib, PCMK_XA_ID, rsc_id, attr_set_type, attr_set, attr_id, attr_name, NULL); if(rc != pcmk_rc_ok) { rsc = rsc->priv->parent; out->info(out, "Performing %s of '%s' on '%s', the parent of '%s'", cmd, attr_name, rsc->id, rsc_id); } return g_list_append(result, rsc); } else if ((rsc->priv->parent == NULL) && (rsc->priv->children != NULL) && pcmk__is_clone(rsc)) { pcmk_resource_t *child = rsc->priv->children->data; if (pcmk__is_primitive(child)) { lookup_id = clone_strip(child->id); /* Could be a cloned group! */ rc = find_resource_attr(out, cib, PCMK_XA_ID, lookup_id, attr_set_type, attr_set, attr_id, attr_name, NULL); if(rc == pcmk_rc_ok) { rsc = child; out->info(out, "A value for '%s' already exists in child '%s', performing %s on that instead of '%s'", attr_name, lookup_id, cmd, rsc_id); } free(lookup_id); } return g_list_append(result, rsc); } /* If the resource is a group ==> children inherit the attribute if defined. */ find_matching_attr_resources_recursive(out, &result, rsc, attr_set, attr_set_type, attr_id, attr_name, cib, 0); return result; } /*! * \internal * \brief Get a resource's XML by resource ID from a given CIB XML tree * * \param[in] cib_xml CIB XML to search * \param[in] rsc Resource whose XML to get * * \return Subtree of \p cib_xml belonging to \p rsc, or \c NULL if not found */ static xmlNode * get_cib_rsc(xmlNode *cib_xml, const pcmk_resource_t *rsc) { char *xpath = pcmk__assert_asprintf("%s//*[@" PCMK_XA_ID "='%s']", pcmk_cib_xpath_for(PCMK_XE_RESOURCES), pcmk__xe_id(rsc->priv->xml)); xmlNode *rsc_xml = pcmk__xpath_find_one(cib_xml->doc, xpath, LOG_ERR); free(xpath); return rsc_xml; } static int update_element_attribute(pcmk__output_t *out, pcmk_resource_t *rsc, cib_t *cib, xmlNode *cib_xml_orig, const char *attr_name, const char *attr_value) { int rc = pcmk_rc_ok; xmlNode *rsc_xml = rsc->priv->xml; rsc_xml = get_cib_rsc(cib_xml_orig, rsc); if (rsc_xml == NULL) { return ENXIO; } pcmk__xe_set(rsc_xml, attr_name, attr_value); rc = cib->cmds->replace(cib, PCMK_XE_RESOURCES, rsc_xml, cib_sync_call); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { out->info(out, "Set attribute: " PCMK_XA_NAME "=%s value=%s", attr_name, attr_value); } return rc; } static int resources_with_attr(pcmk__output_t *out, cib_t *cib, pcmk_resource_t *rsc, const char *requested_name, const char *attr_set, const char *attr_set_type, const char *attr_id, const char *attr_name, const char *top_id, gboolean force, GList **resources) { if (pcmk__str_eq(attr_set_type, PCMK_XE_INSTANCE_ATTRIBUTES, pcmk__str_casei)) { if (!force) { xmlNode *xml_search = NULL; int rc = pcmk_rc_ok; rc = find_resource_attr(out, cib, PCMK_XA_ID, top_id, PCMK_XE_META_ATTRIBUTES, attr_set, attr_id, attr_name, &xml_search); if (rc == pcmk_rc_ok || rc == ENOTUNIQ) { char *found_attr_id = NULL; found_attr_id = pcmk__xe_get_copy(xml_search, PCMK_XA_ID); if (!out->is_quiet(out)) { out->err(out, "WARNING: There is already a meta attribute " "for '%s' called '%s' (id=%s)", top_id, attr_name, found_attr_id); out->err(out, " Delete '%s' first or use the force option " "to override", found_attr_id); } free(found_attr_id); pcmk__xml_free(xml_search); return ENOTUNIQ; } pcmk__xml_free(xml_search); } *resources = g_list_append(*resources, rsc); } else { *resources = find_matching_attr_resources(out, rsc, requested_name, attr_set, attr_set_type, attr_id, attr_name, cib, "update", force); } /* If the user specified attr_set or attr_id, the intent is to modify a * single resource, which will be the last item in the list. */ if ((attr_set != NULL) || (attr_id != NULL)) { GList *last = g_list_last(*resources); *resources = g_list_remove_link(*resources, last); g_list_free(*resources); *resources = last; } return pcmk_rc_ok; } static void free_attr_update_data(gpointer data) { attr_update_data_t *ud = data; if (ud == NULL) { return; } free(ud->attr_set_type); free(ud->attr_set_id); free(ud->attr_name); free(ud->attr_value); free(ud->given_rsc_id); free(ud->found_attr_id); free(ud); } static int update_attribute(pcmk_resource_t *rsc, const char *requested_name, const char *attr_set, const char *attr_set_type, const char *attr_id, const char *attr_name, const char *attr_value, gboolean recursive, cib_t *cib, xmlNode *cib_xml_orig, gboolean force, GList **results) { pcmk__output_t *out = rsc->priv->scheduler->priv->out; int rc = pcmk_rc_ok; GList/**/ *resources = NULL; const char *top_id = pe__const_top_resource(rsc, false)->id; if ((attr_id == NULL) && !force) { find_resource_attr(out, cib, PCMK_XA_ID, top_id, NULL, NULL, NULL, attr_name, NULL); } rc = resources_with_attr(out, cib, rsc, requested_name, attr_set, attr_set_type, attr_id, attr_name, top_id, force, &resources); if (rc != pcmk_rc_ok) { return rc; } for (GList *iter = resources; iter != NULL; iter = iter->next) { // @TODO Functionize loop body to simplify freeing allocated memory char *lookup_id = NULL; char *local_attr_set = NULL; char *found_attr_id = NULL; const char *rsc_attr_id = attr_id; const char *rsc_attr_set = attr_set; xmlNode *rsc_xml = rsc->priv->xml; xmlNode *xml_top = NULL; xmlNode *xml_obj = NULL; xmlNode *xml_search = NULL; rsc = (pcmk_resource_t *) iter->data; lookup_id = clone_strip(rsc->id); /* Could be a cloned group! */ rc = find_resource_attr(out, cib, PCMK_XA_ID, lookup_id, attr_set_type, attr_set, attr_id, attr_name, &xml_search); switch (rc) { case pcmk_rc_ok: found_attr_id = pcmk__xe_get_copy(xml_search, PCMK_XA_ID); pcmk__debug("Found a match for " PCMK_XA_NAME "='%s': " PCMK_XA_ID "='%s'", attr_name, found_attr_id); rsc_attr_id = found_attr_id; break; case ENXIO: if (rsc_attr_set == NULL) { local_attr_set = pcmk__assert_asprintf("%s-%s", lookup_id, attr_set_type); rsc_attr_set = local_attr_set; } if (rsc_attr_id == NULL) { found_attr_id = pcmk__assert_asprintf("%s-%s", rsc_attr_set, attr_name); rsc_attr_id = found_attr_id; } rsc_xml = get_cib_rsc(cib_xml_orig, rsc); if (rsc_xml == NULL) { /* @TODO Warn and continue through the rest of the resources * and return the error at the end? This should never * happen, but if it does, then we could have a partial * update. */ free(lookup_id); free(found_attr_id); pcmk__xml_free(xml_search); g_list_free(resources); return ENXIO; } xml_top = pcmk__xe_create(NULL, (const char *) rsc_xml->name); pcmk__xe_set(xml_top, PCMK_XA_ID, lookup_id); xml_obj = pcmk__xe_create(xml_top, attr_set_type); pcmk__xe_set(xml_obj, PCMK_XA_ID, rsc_attr_set); break; default: free(lookup_id); free(found_attr_id); pcmk__xml_free(xml_search); g_list_free(resources); return rc; } xml_obj = crm_create_nvpair_xml(xml_obj, rsc_attr_id, attr_name, attr_value); if (xml_top == NULL) { xml_top = xml_obj; } - crm_log_xml_debug(xml_top, "Update"); + pcmk__log_xml_debug(xml_top, "Update"); rc = cib->cmds->modify(cib, PCMK_XE_RESOURCES, xml_top, cib_sync_call); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { attr_update_data_t *ud = pcmk__assert_alloc(1, sizeof(attr_update_data_t)); if (attr_set_type == NULL) { attr_set_type = (const char *) xml_search->parent->name; } if (rsc_attr_set == NULL) { rsc_attr_set = pcmk__xe_get(xml_search->parent, PCMK_XA_ID); } ud->attr_set_type = pcmk__str_copy(attr_set_type); ud->attr_set_id = pcmk__str_copy(rsc_attr_set); ud->attr_name = pcmk__str_copy(attr_name); ud->attr_value = pcmk__str_copy(attr_value); ud->given_rsc_id = pcmk__str_copy(lookup_id); ud->found_attr_id = pcmk__str_copy(found_attr_id); ud->rsc = rsc; *results = g_list_append(*results, ud); } pcmk__xml_free(xml_top); pcmk__xml_free(xml_search); free(lookup_id); free(found_attr_id); free(local_attr_set); if (recursive && pcmk__str_eq(attr_set_type, PCMK_XE_META_ATTRIBUTES, pcmk__str_casei)) { /* We want to set the attribute only on resources explicitly * colocated with this one, so we use * rsc->priv->with_this_colocations directly rather than the * with_this_colocations() method. */ pcmk__set_rsc_flags(rsc, pcmk__rsc_detect_loop); for (GList *lpc = rsc->priv->with_this_colocations; lpc != NULL; lpc = lpc->next) { pcmk__colocation_t *cons = (pcmk__colocation_t *) lpc->data; pcmk__debug("Checking %s %d", cons->id, cons->score); if (pcmk__is_set(cons->dependent->flags, pcmk__rsc_detect_loop) || (cons->score <= 0)) { continue; } pcmk__debug("Setting %s=%s for dependent resource %s", attr_name, attr_value, cons->dependent->id); update_attribute(cons->dependent, cons->dependent->id, NULL, attr_set_type, NULL, attr_name, attr_value, recursive, cib, cib_xml_orig, force, results); } } } g_list_free(resources); return rc; } // \return Standard Pacemaker return code int cli_resource_update_attribute(pcmk_resource_t *rsc, const char *requested_name, const char *attr_set, const char *attr_set_type, const char *attr_id, const char *attr_name, const char *attr_value, gboolean recursive, cib_t *cib, xmlNode *cib_xml_orig, gboolean force) { static bool need_init = true; int rc = pcmk_rc_ok; GList *results = NULL; pcmk__output_t *out = rsc->priv->scheduler->priv->out; pcmk__assert(cib_xml_orig != NULL); /* If we were asked to update the attribute in a resource element (for * instance, ) there's really not much we need to do. */ if (pcmk__str_eq(attr_set_type, ATTR_SET_ELEMENT, pcmk__str_none)) { return update_element_attribute(out, rsc, cib, cib_xml_orig, attr_name, attr_value); } /* One time initialization - clear flags so we can detect loops */ if (need_init) { need_init = false; pcmk__unpack_constraints(rsc->priv->scheduler); pe__clear_resource_flags_on_all(rsc->priv->scheduler, pcmk__rsc_detect_loop); } rc = update_attribute(rsc, requested_name, attr_set, attr_set_type, attr_id, attr_name, attr_value, recursive, cib, cib_xml_orig, force, &results); if (rc == pcmk_rc_ok) { if (results == NULL) { return rc; } out->message(out, "attribute-changed-list", results); g_list_free_full(results, free_attr_update_data); } return rc; } // \return Standard Pacemaker return code int cli_resource_delete_attribute(pcmk_resource_t *rsc, const char *requested_name, const char *attr_set, const char *attr_set_type, const char *attr_id, const char *attr_name, cib_t *cib, xmlNode *cib_xml_orig, gboolean force) { pcmk__output_t *out = rsc->priv->scheduler->priv->out; int rc = pcmk_rc_ok; GList/**/ *resources = NULL; pcmk__assert((cib != NULL) && (cib_xml_orig != NULL)); if ((attr_id == NULL) && !force) { find_resource_attr(out, cib, PCMK_XA_ID, pe__const_top_resource(rsc, false)->id, NULL, NULL, NULL, attr_name, NULL); } if (pcmk__str_eq(attr_set_type, ATTR_SET_ELEMENT, pcmk__str_none)) { xmlNode *rsc_xml = rsc->priv->xml; rsc_xml = get_cib_rsc(cib_xml_orig, rsc); if (rsc_xml == NULL) { return ENXIO; } pcmk__xe_remove_attr(rsc_xml, attr_name); rc = cib->cmds->replace(cib, PCMK_XE_RESOURCES, rsc_xml, cib_sync_call); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { out->info(out, "Deleted attribute: %s", attr_name); } return rc; } if (pcmk__str_eq(attr_set_type, PCMK_XE_META_ATTRIBUTES, pcmk__str_none)) { resources = find_matching_attr_resources(out, rsc, requested_name, attr_set, attr_set_type, attr_id, attr_name, cib, "delete", force); } else { resources = g_list_append(resources, rsc); } for (GList *iter = resources; iter != NULL; iter = iter->next) { char *lookup_id = NULL; xmlNode *xml_obj = NULL; xmlNode *xml_search = NULL; char *found_attr_id = NULL; const char *rsc_attr_id = attr_id; rsc = (pcmk_resource_t *) iter->data; /* @TODO Search the original CIB in find_resource_attr() for * future-proofing, to ensure that we're getting IDs of nvpairs that * exist in the CIB. */ lookup_id = clone_strip(rsc->id); rc = find_resource_attr(out, cib, PCMK_XA_ID, lookup_id, attr_set_type, attr_set, attr_id, attr_name, &xml_search); switch (rc) { case pcmk_rc_ok: found_attr_id = pcmk__xe_get_copy(xml_search, PCMK_XA_ID); pcmk__xml_free(xml_search); break; case ENXIO: free(lookup_id); pcmk__xml_free(xml_search); continue; default: free(lookup_id); pcmk__xml_free(xml_search); g_list_free(resources); return rc; } if (rsc_attr_id == NULL) { rsc_attr_id = found_attr_id; } xml_obj = crm_create_nvpair_xml(NULL, rsc_attr_id, attr_name, NULL); - crm_log_xml_debug(xml_obj, "Delete"); + pcmk__log_xml_debug(xml_obj, "Delete"); rc = cib->cmds->remove(cib, PCMK_XE_RESOURCES, xml_obj, cib_sync_call); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { out->info(out, "Deleted '%s' option: " PCMK_XA_ID "=%s%s%s%s%s", lookup_id, found_attr_id, ((attr_set == NULL)? "" : " set="), pcmk__s(attr_set, ""), ((attr_name == NULL)? "" : " " PCMK_XA_NAME "="), pcmk__s(attr_name, "")); } free(lookup_id); pcmk__xml_free(xml_obj); free(found_attr_id); } g_list_free(resources); return rc; } // \return Standard Pacemaker return code static int send_lrm_rsc_op(pcmk_ipc_api_t *controld_api, bool do_fail_resource, pcmk_resource_t *rsc, const char *rsc_id, const pcmk_node_t *node) { pcmk__output_t *out = NULL; const char *rsc_api_id = NULL; const char *rsc_long_id = NULL; const char *rsc_class = NULL; const char *rsc_provider = NULL; const char *rsc_type = NULL; const char *router_node = NULL; bool cib_only = false; pcmk__assert((rsc != NULL) && (rsc_id != NULL) && (node != NULL)); out = rsc->priv->scheduler->priv->out; if (!pcmk__is_primitive(rsc)) { out->err(out, "We can only process primitive resources, not %s", rsc_id); return EINVAL; } rsc_class = pcmk__xe_get(rsc->priv->xml, PCMK_XA_CLASS); rsc_provider = pcmk__xe_get(rsc->priv->xml, PCMK_XA_PROVIDER); rsc_type = pcmk__xe_get(rsc->priv->xml, PCMK_XA_TYPE); if ((rsc_class == NULL) || (rsc_type == NULL)) { out->err(out, "Resource %s does not have a class and type", rsc_id); return EINVAL; } router_node = node->priv->name; if (!node->details->online) { if (do_fail_resource) { out->err(out, "Node %s is not online", pcmk__node_name(node)); return ENOTCONN; } cib_only = true; } else if (pcmk__is_pacemaker_remote_node(node)) { const pcmk_node_t *conn_host = pcmk__current_node(node->priv->remote); if (conn_host == NULL) { out->err(out, "No cluster connection to Pacemaker Remote node %s " "detected", pcmk__node_name(node)); return ENOTCONN; } router_node = conn_host->priv->name; } if (rsc->priv->history_id != NULL) { rsc_api_id = rsc->priv->history_id; rsc_long_id = rsc->id; } else { rsc_api_id = rsc->id; } if (do_fail_resource) { return pcmk_controld_api_fail(controld_api, node->priv->name, router_node, rsc_api_id, rsc_long_id, rsc_class, rsc_provider, rsc_type); } return pcmk_controld_api_refresh(controld_api, node->priv->name, router_node, rsc_api_id, rsc_long_id, rsc_class, rsc_provider, rsc_type, cib_only); } /*! * \internal * \brief Get resource name as used in failure-related node attributes * * \param[in] rsc Resource to check * * \return Newly allocated string containing resource's fail name * \note The caller is responsible for freeing the result. */ static inline char * rsc_fail_name(const pcmk_resource_t *rsc) { const char *name = pcmk__s(rsc->priv->history_id, rsc->id); if (pcmk__is_set(rsc->flags, pcmk__rsc_unique)) { return strdup(name); } return clone_strip(name); } // \return Standard Pacemaker return code static int clear_rsc_history(pcmk_ipc_api_t *controld_api, pcmk_resource_t *rsc, const char *rsc_id, const pcmk_node_t *node) { int rc = pcmk_rc_ok; pcmk__assert((rsc != NULL) && (node != NULL)); /* Erase the resource's entire LRM history in the CIB, even if we're only * clearing a single operation's fail count. If we erased only entries for a * single operation, we might wind up with a wrong idea of the current * resource state, and we might not re-probe the resource. */ rc = send_lrm_rsc_op(controld_api, false, rsc, rsc_id, node); if (rc != pcmk_rc_ok) { return rc; } pcmk__trace("Processing %d mainloop inputs", pcmk_controld_api_replies_expected(controld_api)); while (g_main_context_iteration(NULL, FALSE)) { pcmk__trace("Processed mainloop input, %d still remaining", pcmk_controld_api_replies_expected(controld_api)); } return rc; } // \return Standard Pacemaker return code static int clear_rsc_failures(pcmk__output_t *out, pcmk_ipc_api_t *controld_api, pcmk_node_t *node, const char *rsc_id, const char *operation, const char *interval_spec) { int rc = pcmk_rc_ok; pcmk_scheduler_t *scheduler = NULL; const char *failed_value = NULL; const char *failed_id = NULL; char *interval_ms_s = NULL; GHashTable *rscs = NULL; GHashTableIter iter; pcmk__assert(node != NULL); scheduler = node->priv->scheduler; /* Create a hash table to use as a set of resources to clean. This lets us * clean each resource only once (per node) regardless of how many failed * operations it has. */ rscs = pcmk__strkey_table(NULL, NULL); // Normalize interval to milliseconds for comparison to history entry if (operation) { guint interval_ms = 0U; pcmk_parse_interval_spec(interval_spec, &interval_ms); interval_ms_s = pcmk__assert_asprintf("%u", interval_ms); } for (xmlNode *xml_op = pcmk__xe_first_child(scheduler->priv->failed, NULL, NULL, NULL); xml_op != NULL; xml_op = pcmk__xe_next(xml_op, NULL)) { failed_id = pcmk__xe_get(xml_op, PCMK__XA_RSC_ID); if (failed_id == NULL) { // Malformed history entry, should never happen continue; } // No resource specified means all resources match if (rsc_id) { pcmk_resource_t *fail_rsc = NULL; fail_rsc = pe_find_resource_with_flags(scheduler->priv->resources, failed_id, pcmk_rsc_match_history |pcmk_rsc_match_anon_basename); if ((fail_rsc == NULL) || !pcmk__str_eq(rsc_id, fail_rsc->id, pcmk__str_none)) { continue; } } // Host name should always have been provided by this point failed_value = pcmk__xe_get(xml_op, PCMK_XA_UNAME); if (!pcmk__str_eq(node->priv->name, failed_value, pcmk__str_casei)) { continue; } // No operation specified means all operations match if (operation) { failed_value = pcmk__xe_get(xml_op, PCMK_XA_OPERATION); if (!pcmk__str_eq(operation, failed_value, pcmk__str_casei)) { continue; } // Interval (if operation was specified) defaults to 0 (not all) failed_value = pcmk__xe_get(xml_op, PCMK_META_INTERVAL); if (!pcmk__str_eq(interval_ms_s, failed_value, pcmk__str_casei)) { continue; } } g_hash_table_add(rscs, (gpointer) failed_id); } free(interval_ms_s); g_hash_table_iter_init(&iter, rscs); while (g_hash_table_iter_next(&iter, (gpointer *) &failed_id, NULL)) { pcmk_resource_t *rsc = NULL; pcmk__debug("Erasing failures of %s on %s", failed_id, pcmk__node_name(node)); rsc = pe_find_resource(scheduler->priv->resources, failed_id); if (rsc == NULL) { out->err(out, "Resource %s not found", failed_id); return ENXIO; } rc = clear_rsc_history(controld_api, rsc, failed_id, node); if (rc != pcmk_rc_ok) { return rc; } } g_hash_table_destroy(rscs); return rc; } // \return Standard Pacemaker return code static int clear_rsc_fail_attrs(const pcmk_resource_t *rsc, const char *operation, const char *interval_spec, const pcmk_node_t *node) { int rc = pcmk_rc_ok; int attr_options = pcmk__node_attr_none; char *rsc_name = rsc_fail_name(rsc); if (pcmk__is_pacemaker_remote_node(node)) { attr_options |= pcmk__node_attr_remote; } rc = pcmk__attrd_api_clear_failures(NULL, node->priv->name, rsc_name, operation, interval_spec, NULL, attr_options); free(rsc_name); return rc; } // \return Standard Pacemaker return code int cli_resource_delete(pcmk_ipc_api_t *controld_api, pcmk_resource_t *rsc, pcmk_node_t *node, const char *operation, const char *interval_spec, bool just_failures, bool force) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; pcmk__assert(rsc != NULL); scheduler = rsc->priv->scheduler; out = scheduler->priv->out; if (rsc->priv->children != NULL) { for (GList *iter = rsc->priv->children; iter != NULL; iter = iter->next) { pcmk_resource_t *child = iter->data; rc = cli_resource_delete(controld_api, child, node, operation, interval_spec, just_failures, force); if (rc != pcmk_rc_ok) { return rc; } } return pcmk_rc_ok; } if (node == NULL) { GList *nodes = g_hash_table_get_values(rsc->priv->probed_nodes); if (nodes == NULL) { if (force) { nodes = g_list_copy(scheduler->nodes); } else if (pcmk__is_set(rsc->flags, pcmk__rsc_exclusive_probes)) { GHashTableIter iter; g_hash_table_iter_init(&iter, rsc->priv->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { if ((node != NULL) && (node->assign->score >= 0)) { nodes = g_list_prepend(nodes, (gpointer *) node); } } } else { nodes = g_hash_table_get_values(rsc->priv->allowed_nodes); } } for (GList *iter = nodes; iter != NULL; iter = iter->next) { node = (pcmk_node_t *) iter->data; if (!node->details->online) { continue; } rc = cli_resource_delete(controld_api, rsc, node, operation, interval_spec, just_failures, force); if (rc != pcmk_rc_ok) { break; } } g_list_free(nodes); return rc; } if (!pcmk__is_set(node->priv->flags, pcmk__node_probes_allowed)) { out->err(out, "Unable to clean up %s because resource discovery disabled on " "%s", rsc->id, pcmk__node_name(node)); return EOPNOTSUPP; } if (controld_api == NULL) { out->err(out, "Dry run: skipping clean-up of %s on %s due to CIB_file", rsc->id, pcmk__node_name(node)); return pcmk_rc_ok; } rc = clear_rsc_fail_attrs(rsc, operation, interval_spec, node); if (rc != pcmk_rc_ok) { out->err(out, "Unable to clean up %s failures on %s: %s", rsc->id, pcmk__node_name(node), pcmk_rc_str(rc)); return rc; } if (just_failures) { rc = clear_rsc_failures(out, controld_api, node, rsc->id, operation, interval_spec); } else { rc = clear_rsc_history(controld_api, rsc, rsc->id, node); } if (rc != pcmk_rc_ok) { out->err(out, "Cleaned %s failures on %s, but unable to clean history: %s", rsc->id, pcmk__node_name(node), pcmk_rc_str(rc)); } else { out->info(out, "Cleaned up %s on %s", rsc->id, pcmk__node_name(node)); } return rc; } // \return Standard Pacemaker return code int cli_cleanup_all(pcmk_ipc_api_t *controld_api, pcmk_node_t *node, const char *operation, const char *interval_spec, pcmk_scheduler_t *scheduler) { pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; int attr_options = pcmk__node_attr_none; const char *node_name = NULL; const char *log_node_name = "all nodes"; pcmk__assert(scheduler != NULL); out = scheduler->priv->out; if (node != NULL) { node_name = node->priv->name; log_node_name = pcmk__node_name(node); } if (controld_api == NULL) { out->info(out, "Dry run: skipping clean-up of %s due to CIB_file", log_node_name); return rc; } if (pcmk__is_pacemaker_remote_node(node)) { pcmk__set_node_attr_flags(attr_options, pcmk__node_attr_remote); } rc = pcmk__attrd_api_clear_failures(NULL, node_name, NULL, operation, interval_spec, NULL, attr_options); if (rc != pcmk_rc_ok) { out->err(out, "Unable to clean up all failures on %s: %s", log_node_name, pcmk_rc_str(rc)); return rc; } if (node != NULL) { rc = clear_rsc_failures(out, controld_api, node, NULL, operation, interval_spec); } else { for (GList *iter = scheduler->nodes; iter; iter = iter->next) { pcmk_node_t *sched_node = iter->data; rc = clear_rsc_failures(out, controld_api, sched_node, NULL, operation, interval_spec); if (rc != pcmk_rc_ok) { break; } } } if (rc == pcmk_rc_ok) { out->info(out, "Cleaned up all resources on %s", log_node_name); } else { // @TODO But didn't clear_rsc_failures() fail? out->err(out, "Cleaned all resource failures on %s, but unable to clean " "history: %s", log_node_name, pcmk_rc_str(rc)); } return rc; } static void check_role(resource_checks_t *checks) { const char *role_s = g_hash_table_lookup(checks->rsc->priv->meta, PCMK_META_TARGET_ROLE); if (role_s == NULL) { return; } switch (pcmk_parse_role(role_s)) { case pcmk_role_stopped: checks->flags |= rsc_remain_stopped; break; case pcmk_role_unpromoted: if (pcmk__is_set(pe__const_top_resource(checks->rsc, false)->flags, pcmk__rsc_promotable)) { checks->flags |= rsc_unpromotable; } break; default: break; } } static void check_managed(resource_checks_t *checks) { const char *managed_s = g_hash_table_lookup(checks->rsc->priv->meta, PCMK_META_IS_MANAGED); if ((managed_s != NULL) && !pcmk__is_true(managed_s)) { checks->flags |= rsc_unmanaged; } } static void check_locked(resource_checks_t *checks) { const pcmk_node_t *lock_node = checks->rsc->priv->lock_node; if (lock_node != NULL) { checks->flags |= rsc_locked; checks->lock_node = lock_node->priv->name; } } static bool node_is_unhealthy(pcmk_node_t *node) { switch (pe__health_strategy(node->priv->scheduler)) { case pcmk__health_strategy_none: break; case pcmk__health_strategy_no_red: if (pe__node_health(node) < 0) { return true; } break; case pcmk__health_strategy_only_green: if (pe__node_health(node) <= 0) { return true; } break; case pcmk__health_strategy_progressive: case pcmk__health_strategy_custom: /* @TODO These are finite scores, possibly with rules, and possibly * combining with other scores, so attributing these as a cause is * nontrivial. */ break; } return false; } static void check_node_health(resource_checks_t *checks, pcmk_node_t *node) { if (node == NULL) { GHashTableIter iter; bool allowed = false; bool all_nodes_unhealthy = true; g_hash_table_iter_init(&iter, checks->rsc->priv->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) { allowed = true; if (!node_is_unhealthy(node)) { all_nodes_unhealthy = false; break; } } if (allowed && all_nodes_unhealthy) { checks->flags |= rsc_node_health; } } else if (node_is_unhealthy(node)) { checks->flags |= rsc_node_health; } } /* @TODO Make this check all resources if rsc is NULL, so it can be called after * cleanup of all resources */ int cli_resource_check(pcmk__output_t *out, pcmk_resource_t *rsc, pcmk_node_t *node) { resource_checks_t checks = { .rsc = rsc }; check_role(&checks); check_managed(&checks); check_locked(&checks); check_node_health(&checks, node); return out->message(out, "resource-check-list", &checks); } // \return Standard Pacemaker return code int cli_resource_fail(pcmk_ipc_api_t *controld_api, pcmk_resource_t *rsc, const char *rsc_id, const pcmk_node_t *node) { pcmk__assert((rsc != NULL) && (rsc_id != NULL) && (node != NULL)); if (controld_api == NULL) { pcmk__output_t *out = rsc->priv->scheduler->priv->out; out->err(out, "Dry run: skipping fail of %s on %s due to CIB_file", rsc_id, pcmk__node_name(node)); return pcmk_rc_ok; } pcmk__notice("Failing %s on %s", rsc_id, pcmk__node_name(node)); return send_lrm_rsc_op(controld_api, true, rsc, rsc_id, node); } static GHashTable * generate_resource_params(pcmk_resource_t *rsc) { GHashTable *params = NULL; GHashTable *meta = NULL; GHashTable *combined = NULL; GHashTableIter iter; char *key = NULL; char *value = NULL; combined = pcmk__strkey_table(free, free); params = pe_rsc_params(rsc, NULL, rsc->priv->scheduler); if (params != NULL) { g_hash_table_iter_init(&iter, params); while (g_hash_table_iter_next(&iter, (gpointer *) & key, (gpointer *) & value)) { pcmk__insert_dup(combined, key, value); } } meta = pcmk__strkey_table(free, free); get_meta_attributes(meta, rsc, NULL, rsc->priv->scheduler); if (meta != NULL) { g_hash_table_iter_init(&iter, meta); while (g_hash_table_iter_next(&iter, (gpointer *) & key, (gpointer *) & value)) { char *crm_name = crm_meta_name(key); g_hash_table_insert(combined, crm_name, strdup(value)); } g_hash_table_destroy(meta); } return combined; } bool resource_is_running_on(pcmk_resource_t *rsc, const char *host) { bool found = true; GList *hIter = NULL; GList *hosts = NULL; if (rsc == NULL) { return false; } rsc->priv->fns->location(rsc, &hosts, pcmk__rsc_node_current); for (hIter = hosts; host != NULL && hIter != NULL; hIter = hIter->next) { pcmk_node_t *node = (pcmk_node_t *) hIter->data; if (pcmk__strcase_any_of(host, node->priv->name, node->priv->id, NULL)) { pcmk__trace("Resource %s is running on %s\n", rsc->id, host); goto done; } } if (host != NULL) { pcmk__trace("Resource %s is not running on: %s\n", rsc->id, host); found = false; } else if(host == NULL && hosts == NULL) { pcmk__trace("Resource %s is not running\n", rsc->id); found = false; } done: g_list_free(hosts); return found; } /*! * \internal * \brief Create a list of all resources active on host from a given list * * \param[in] host Name of host to check whether resources are active * \param[in] rsc_list List of resources to check * * \return New list of resources from list that are active on host */ static GList * get_active_resources(const char *host, GList *rsc_list) { GList *rIter = NULL; GList *active = NULL; for (rIter = rsc_list; rIter != NULL; rIter = rIter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) rIter->data; /* Expand groups to their members, because if we're restarting a member * other than the first, we can't otherwise tell which resources are * stopping and starting. */ if (pcmk__is_group(rsc)) { GList *member_active = NULL; member_active = get_active_resources(host, rsc->priv->children); active = g_list_concat(active, member_active); } else if (resource_is_running_on(rsc, host)) { active = g_list_append(active, strdup(rsc->id)); } } return active; } static void dump_list(GList *items, const char *tag) { int lpc = 0; GList *item = NULL; for (item = items; item != NULL; item = item->next) { pcmk__trace("%s[%d]: %s", tag, lpc, (char*)item->data); lpc++; } } static void display_list(pcmk__output_t *out, GList *items, const char *tag) { GList *item = NULL; for (item = items; item != NULL; item = item->next) { out->info(out, "%s%s", tag, (const char *)item->data); } } /*! * \internal * \brief Update scheduler XML input based on a CIB query and the current time * * The CIB XML is upgraded to the latest schema version. * * \param[in,out] out Output object * \param[in,out] scheduler Scheduler data to update * \param[in] cib Connection to the CIB manager * \param[out] cib_xml_orig Where to store CIB XML before any schema * upgrades (can be \c NULL) * * \return Standard Pacemaker return code */ int update_scheduler_input(pcmk__output_t *out, pcmk_scheduler_t *scheduler, cib_t *cib, xmlNode **cib_xml_orig) { xmlNode *queried_xml = NULL; xmlNode *updated_xml = NULL; int rc = pcmk_rc_ok; pcmk__assert((out != NULL) && (scheduler != NULL) && (scheduler->input == NULL) && (scheduler->priv->now == NULL) && (cib != NULL) && ((cib_xml_orig == NULL) || (*cib_xml_orig == NULL))); rc = cib->cmds->query(cib, NULL, &queried_xml, cib_sync_call); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { out->err(out, "Could not obtain the current CIB: %s", pcmk_rc_str(rc)); goto done; } if (cib_xml_orig != NULL) { updated_xml = pcmk__xml_copy(NULL, queried_xml); } else { // No need to preserve the pre-upgrade CIB, so don't make a copy updated_xml = queried_xml; queried_xml = NULL; } rc = pcmk__update_configured_schema(&updated_xml, false); if (rc != pcmk_rc_ok) { out->err(out, "Could not upgrade the current CIB XML: %s", pcmk_rc_str(rc)); pcmk__xml_free(updated_xml); goto done; } scheduler->input = updated_xml; scheduler->priv->now = crm_time_new(NULL); done: if ((rc == pcmk_rc_ok) && (cib_xml_orig != NULL)) { *cib_xml_orig = queried_xml; } else { pcmk__xml_free(queried_xml); } return rc; } // \return Standard Pacemaker return code static int update_dataset(cib_t *cib, pcmk_scheduler_t *scheduler, xmlNode **cib_xml_orig, bool simulate) { char *pid = NULL; char *shadow_file = NULL; cib_t *shadow_cib = NULL; int rc = pcmk_rc_ok; pcmk__output_t *out = scheduler->priv->out; pcmk_reset_scheduler(scheduler); pcmk__set_scheduler_flags(scheduler, pcmk__sched_no_counts); if(simulate) { bool prev_quiet = false; rc = update_scheduler_input(out, scheduler, cib, NULL); if (rc != pcmk_rc_ok) { goto done; } pid = pcmk__getpid_s(); shadow_cib = cib_shadow_new(pid); shadow_file = get_shadow_file(pid); if (shadow_cib == NULL) { out->err(out, "Could not create shadow cib: '%s'", pid); rc = ENXIO; goto done; } rc = pcmk__xml_write_file(scheduler->input, shadow_file, false); if (rc != pcmk_rc_ok) { out->err(out, "Could not populate shadow cib: %s", pcmk_rc_str(rc)); goto done; } rc = shadow_cib->cmds->signon(shadow_cib, crm_system_name, cib_command); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { out->err(out, "Could not connect to shadow cib: %s", pcmk_rc_str(rc)); goto done; } pcmk__schedule_actions(scheduler); prev_quiet = out->is_quiet(out); out->quiet = true; pcmk__simulate_transition(scheduler, shadow_cib, NULL); out->quiet = prev_quiet; rc = update_dataset(shadow_cib, scheduler, cib_xml_orig, false); } else { xmlNode *xml = NULL; rc = update_scheduler_input(out, scheduler, cib, &xml); if (rc != pcmk_rc_ok) { goto done; } pcmk__xml_free(*cib_xml_orig); *cib_xml_orig = xml; cluster_status(scheduler); } done: // Do not free scheduler->input because rsc->priv->xml must remain valid cib_delete(shadow_cib); free(pid); if(shadow_file) { unlink(shadow_file); free(shadow_file); } return rc; } /*! * \internal * \brief Find the maximum stop timeout of a resource and its children (if any) * * \param[in,out] rsc Resource to get timeout for * * \return Maximum stop timeout for \p rsc (in milliseconds) */ static guint max_rsc_stop_timeout(pcmk_resource_t *rsc) { long long result_ll; guint max_delay = 0; xmlNode *config = NULL; GHashTable *meta = NULL; if (rsc == NULL) { return 0; } // If resource is collective, use maximum of its children's stop timeouts if (rsc->priv->children != NULL) { for (GList *iter = rsc->priv->children; iter != NULL; iter = iter->next) { pcmk_resource_t *child = iter->data; guint delay = max_rsc_stop_timeout(child); if (delay > max_delay) { pcmk__rsc_trace(rsc, "Maximum stop timeout for %s is now %s " "due to %s", rsc->id, pcmk__readable_interval(delay), child->id); max_delay = delay; } } return max_delay; } // Get resource's stop action configuration from CIB config = pcmk__find_action_config(rsc, PCMK_ACTION_STOP, 0, true); /* Get configured timeout for stop action (fully evaluated for rules, * defaults, etc.). * * @TODO This currently ignores node (which might matter for rules) */ meta = pcmk__unpack_action_meta(rsc, NULL, PCMK_ACTION_STOP, 0, config); if ((pcmk__scan_ll(g_hash_table_lookup(meta, PCMK_META_TIMEOUT), &result_ll, -1LL) == pcmk_rc_ok) && (result_ll >= 0)) { max_delay = (guint) QB_MIN(result_ll, UINT_MAX); } g_hash_table_destroy(meta); return max_delay; } /*! * \internal * \brief Find a reasonable waiting time for stopping any one resource in a list * * \param[in,out] scheduler Scheduler data * \param[in] resources List of names of resources that will be stopped * * \return Rough estimate of a reasonable time to wait (in seconds) to stop any * one resource in \p resources * \note This estimate is very rough, simply the maximum stop timeout of all * given resources and their children, plus a small fudge factor. It does * not account for children that must be stopped in sequence, action * throttling, or any demotions needed. It checks the stop timeout, even * if the resources in question are actually being started. */ static guint wait_time_estimate(pcmk_scheduler_t *scheduler, const GList *resources) { guint max_delay = 0U; // Find maximum stop timeout in milliseconds for (const GList *item = resources; item != NULL; item = item->next) { pcmk_resource_t *rsc = pe_find_resource(scheduler->priv->resources, (const char *) item->data); guint delay = max_rsc_stop_timeout(rsc); if (delay > max_delay) { pcmk__rsc_trace(rsc, "Wait time is now %s due to %s", pcmk__readable_interval(delay), rsc->id); max_delay = delay; } } return pcmk__timeout_ms2s(max_delay) + 5; } #define waiting_for_starts(d, r, h) ((d != NULL) || \ (!resource_is_running_on((r), (h)))) /*! * \internal * \brief Restart a resource (on a particular host if requested). * * \param[in,out] out Output object * \param[in,out] rsc The resource to restart * \param[in] node Node to restart resource on (NULL for all) * \param[in] move_lifetime If not NULL, how long constraint should * remain in effect (as ISO 8601 string) * \param[in] timeout_ms Consider failed if actions do not complete * in this time (specified in milliseconds, * but a two-second granularity is actually * used; if 0, it will be calculated based on * the resource timeout) * \param[in,out] cib Connection to the CIB manager * \param[in] promoted_role_only If true, limit to promoted instances * \param[in] force If true, apply only to requested instance * if part of a collective resource * * \return Standard Pacemaker return code (exits on certain failures) */ int cli_resource_restart(pcmk__output_t *out, pcmk_resource_t *rsc, const pcmk_node_t *node, const char *move_lifetime, guint timeout_ms, cib_t *cib, gboolean promoted_role_only, gboolean force) { int rc = pcmk_rc_ok; int lpc = 0; guint before = 0; guint step_timeout_s = 0; /* @TODO Due to this sleep interval, a timeout <2s will cause problems and * should be rejected */ guint sleep_interval = 2U; guint timeout = pcmk__timeout_ms2s(timeout_ms); bool stop_via_ban = false; char *rsc_id = NULL; char *lookup_id = NULL; char *orig_target_role = NULL; xmlNode *cib_xml_orig = NULL; GList *list_delta = NULL; GList *target_active = NULL; GList *current_active = NULL; GList *restart_target_active = NULL; pcmk_scheduler_t *scheduler = NULL; pcmk_resource_t *parent = uber_parent(rsc); bool running = false; const char *id = pcmk__s(rsc->priv->history_id, rsc->id); const char *host = node ? node->priv->name : NULL; /* If the implicit resource or primitive resource of a bundle is given, operate on the * bundle itself instead. */ if (pcmk__is_bundled(rsc)) { rsc = parent->priv->parent; } running = resource_is_running_on(rsc, host); if (pcmk__is_clone(parent) && !running) { if (pcmk__is_unique_clone(parent)) { lookup_id = strdup(rsc->id); } else { lookup_id = clone_strip(rsc->id); } rsc = parent->priv->fns->find_rsc(parent, lookup_id, node, pcmk_rsc_match_basename |pcmk_rsc_match_current_node); free(lookup_id); running = resource_is_running_on(rsc, host); } if (!running) { if (host) { out->err(out, "%s is not running on %s and so cannot be restarted", id, host); } else { out->err(out, "%s is not running anywhere and so cannot be restarted", id); } return ENXIO; } if (!pcmk__is_set(rsc->flags, pcmk__rsc_managed)) { out->err(out, "Unmanaged resources cannot be restarted."); return EAGAIN; } rsc_id = strdup(rsc->id); if (pcmk__is_unique_clone(parent)) { lookup_id = strdup(rsc->id); } else { lookup_id = clone_strip(rsc->id); } if (host) { if (pcmk__is_clone(rsc) || pe_bundle_replicas(rsc)) { stop_via_ban = true; } else if (pcmk__is_clone(parent)) { stop_via_ban = true; free(lookup_id); lookup_id = strdup(parent->id); } } /* grab full cib determine originally active resources disable or ban poll cib and watch for affected resources to get stopped without --timeout, calculate the stop timeout for each step and wait for that if we hit --timeout or the service timeout, re-enable or un-ban, report failure and indicate which resources we couldn't take down if everything stopped, re-enable or un-ban poll cib and watch for affected resources to get started without --timeout, calculate the start timeout for each step and wait for that if we hit --timeout or the service timeout, report (different) failure and indicate which resources we couldn't bring back up report success Optimizations: - use constraints to determine ordered list of affected resources - Allow a --no-deps option (aka. --force-restart) */ scheduler = pcmk_new_scheduler(); if (scheduler == NULL) { rc = errno; out->err(out, "Could not allocate scheduler data: %s", pcmk_rc_str(rc)); goto done; } scheduler->priv->out = out; rc = update_dataset(cib, scheduler, &cib_xml_orig, false); if(rc != pcmk_rc_ok) { out->err(out, "Could not get new resource list: %s (%d)", pcmk_rc_str(rc), rc); goto done; } restart_target_active = get_active_resources(host, scheduler->priv->resources); current_active = get_active_resources(host, scheduler->priv->resources); dump_list(current_active, "Origin"); if (stop_via_ban) { /* Stop the clone or bundle instance by banning it from the host */ out->quiet = true; rc = cli_resource_ban(out, lookup_id, host, move_lifetime, cib, promoted_role_only, PCMK_ROLE_PROMOTED); } else { xmlNode *xml_search = NULL; /* Stop the resource by setting PCMK_META_TARGET_ROLE to Stopped. * Remember any existing PCMK_META_TARGET_ROLE so we can restore it * later (though it only makes any difference if it's Unpromoted). */ rc = find_resource_attr(out, cib, PCMK_XA_VALUE, lookup_id, NULL, NULL, NULL, PCMK_META_TARGET_ROLE, &xml_search); if (rc == pcmk_rc_ok) { orig_target_role = pcmk__xe_get_copy(xml_search, PCMK_XA_VALUE); } pcmk__xml_free(xml_search); rc = cli_resource_update_attribute(rsc, rsc_id, NULL, PCMK_XE_META_ATTRIBUTES, NULL, PCMK_META_TARGET_ROLE, PCMK_ACTION_STOPPED, FALSE, cib, cib_xml_orig, force); } if(rc != pcmk_rc_ok) { out->err(out, "Could not set " PCMK_META_TARGET_ROLE " for %s: %s (%d)", rsc_id, pcmk_rc_str(rc), rc); if (current_active != NULL) { g_list_free_full(current_active, free); current_active = NULL; } if (restart_target_active != NULL) { g_list_free_full(restart_target_active, free); restart_target_active = NULL; } goto done; } rc = update_dataset(cib, scheduler, &cib_xml_orig, true); if(rc != pcmk_rc_ok) { out->err(out, "Could not determine which resources would be stopped"); goto failure; } target_active = get_active_resources(host, scheduler->priv->resources); dump_list(target_active, "Target"); list_delta = pcmk__subtract_lists(current_active, target_active, (GCompareFunc) strcmp); out->info(out, "Waiting for %d resources to stop:", g_list_length(list_delta)); display_list(out, list_delta, " * "); step_timeout_s = timeout / sleep_interval; while (list_delta != NULL) { before = g_list_length(list_delta); if(timeout_ms == 0) { step_timeout_s = wait_time_estimate(scheduler, list_delta) / sleep_interval; } /* We probably don't need the entire step timeout */ for(lpc = 0; (lpc < step_timeout_s) && (list_delta != NULL); lpc++) { sleep(sleep_interval); if(timeout) { timeout -= sleep_interval; pcmk__trace("%us remaining", timeout); } rc = update_dataset(cib, scheduler, &cib_xml_orig, false); if(rc != pcmk_rc_ok) { out->err(out, "Could not determine which resources were stopped"); goto failure; } if (current_active != NULL) { g_list_free_full(current_active, free); } current_active = get_active_resources(host, scheduler->priv->resources); g_list_free(list_delta); list_delta = pcmk__subtract_lists(current_active, target_active, (GCompareFunc) strcmp); dump_list(current_active, "Current"); dump_list(list_delta, "Delta"); } pcmk__trace("%u (was %u) resources remaining", g_list_length(list_delta), before); if(before == g_list_length(list_delta)) { /* aborted during stop phase, print the contents of list_delta */ out->err(out, "Could not complete shutdown of %s, %d resources remaining", rsc_id, g_list_length(list_delta)); display_list(out, list_delta, " * "); rc = ETIME; goto failure; } } if (stop_via_ban) { rc = cli_resource_clear(lookup_id, host, NULL, cib, true, force); } else if (orig_target_role) { rc = cli_resource_update_attribute(rsc, rsc_id, NULL, PCMK_XE_META_ATTRIBUTES, NULL, PCMK_META_TARGET_ROLE, orig_target_role, FALSE, cib, cib_xml_orig, force); free(orig_target_role); orig_target_role = NULL; } else { rc = cli_resource_delete_attribute(rsc, rsc_id, NULL, PCMK_XE_META_ATTRIBUTES, NULL, PCMK_META_TARGET_ROLE, cib, cib_xml_orig, force); } if(rc != pcmk_rc_ok) { out->err(out, "Could not unset " PCMK_META_TARGET_ROLE " for %s: %s (%d)", rsc_id, pcmk_rc_str(rc), rc); goto done; } if (target_active != NULL) { g_list_free_full(target_active, free); } target_active = restart_target_active; list_delta = pcmk__subtract_lists(target_active, current_active, (GCompareFunc) strcmp); out->info(out, "Waiting for %d resources to start again:", g_list_length(list_delta)); display_list(out, list_delta, " * "); step_timeout_s = timeout / sleep_interval; while (waiting_for_starts(list_delta, rsc, host)) { before = g_list_length(list_delta); if(timeout_ms == 0) { step_timeout_s = wait_time_estimate(scheduler, list_delta) / sleep_interval; } /* We probably don't need the entire step timeout */ for (lpc = 0; (lpc < step_timeout_s) && waiting_for_starts(list_delta, rsc, host); lpc++) { sleep(sleep_interval); if(timeout) { timeout -= sleep_interval; pcmk__trace("%ds remaining", timeout); } rc = update_dataset(cib, scheduler, &cib_xml_orig, false); if(rc != pcmk_rc_ok) { out->err(out, "Could not determine which resources were started"); goto failure; } /* It's OK if dependent resources moved to a different node, * so we check active resources on all nodes. */ if (current_active != NULL) { g_list_free_full(current_active, free); } current_active = get_active_resources(NULL, scheduler->priv->resources); g_list_free(list_delta); list_delta = pcmk__subtract_lists(target_active, current_active, (GCompareFunc) strcmp); dump_list(current_active, "Current"); dump_list(list_delta, "Delta"); } if(before == g_list_length(list_delta)) { /* aborted during start phase, print the contents of list_delta */ out->err(out, "Could not complete restart of %s, %d resources remaining", rsc_id, g_list_length(list_delta)); display_list(out, list_delta, " * "); rc = ETIME; goto failure; } } rc = pcmk_rc_ok; goto done; failure: if (stop_via_ban) { cli_resource_clear(lookup_id, host, NULL, cib, true, force); } else if (orig_target_role) { cli_resource_update_attribute(rsc, rsc_id, NULL, PCMK_XE_META_ATTRIBUTES, NULL, PCMK_META_TARGET_ROLE, orig_target_role, FALSE, cib, cib_xml_orig, force); free(orig_target_role); } else { cli_resource_delete_attribute(rsc, rsc_id, NULL, PCMK_XE_META_ATTRIBUTES, NULL, PCMK_META_TARGET_ROLE, cib, cib_xml_orig, force); } done: if (list_delta != NULL) { g_list_free(list_delta); } if (current_active != NULL) { g_list_free_full(current_active, free); } if (target_active != NULL && (target_active != restart_target_active)) { g_list_free_full(target_active, free); } if (restart_target_active != NULL) { g_list_free_full(restart_target_active, free); } free(rsc_id); free(lookup_id); pcmk_free_scheduler(scheduler); return rc; } static inline bool action_is_pending(const pcmk_action_t *action) { if (pcmk__any_flags_set(action->flags, pcmk__action_optional|pcmk__action_pseudo) || !pcmk__is_set(action->flags, pcmk__action_runnable) || pcmk__str_eq(PCMK_ACTION_NOTIFY, action->task, pcmk__str_casei)) { return false; } return true; } /*! * \internal * \brief Check whether any actions in a list are pending * * \param[in] actions List of actions to check * * \return true if any actions in the list are pending, otherwise false */ static bool actions_are_pending(const GList *actions) { for (const GList *action = actions; action != NULL; action = action->next) { const pcmk_action_t *a = (const pcmk_action_t *) action->data; if (action_is_pending(a)) { pcmk__notice("Waiting for %s (flags=%#.8x)", a->uuid, a->flags); return true; } } return false; } static void print_pending_actions(pcmk__output_t *out, GList *actions) { GList *action; out->info(out, "Pending actions:"); for (action = actions; action != NULL; action = action->next) { pcmk_action_t *a = (pcmk_action_t *) action->data; if (!action_is_pending(a)) { continue; } if (a->node) { out->info(out, "\tAction %d: %s\ton %s", a->id, a->uuid, pcmk__node_name(a->node)); } else { out->info(out, "\tAction %d: %s", a->id, a->uuid); } } } /* For --wait, timeout (in seconds) to use if caller doesn't specify one */ #define WAIT_DEFAULT_TIMEOUT_S (60 * 60) /* For --wait, how long to sleep between cluster state checks */ #define WAIT_SLEEP_S (2) /*! * \internal * \brief Wait until all pending cluster actions are complete * * This waits until either the CIB's transition graph is idle or a timeout is * reached. * * \param[in,out] out Output object * \param[in] timeout_ms Consider failed if actions do not complete in * this time (specified in milliseconds, but * one-second granularity is actually used; if 0, a * default will be used) * \param[in,out] cib Connection to the CIB manager * * \return Standard Pacemaker return code */ int wait_till_stable(pcmk__output_t *out, guint timeout_ms, cib_t * cib) { // @FIXME This should bail out when run with CIB_file pcmk_scheduler_t *scheduler = NULL; xmlXPathObject *search = NULL; int rc = pcmk_rc_ok; bool pending_unknown_state_resources; time_t expire_time = time(NULL); time_t time_diff; bool printed_version_warning = out->is_quiet(out); // i.e. don't print if quiet char *xpath = NULL; if (timeout_ms == 0) { expire_time += WAIT_DEFAULT_TIMEOUT_S; } else { expire_time += pcmk__timeout_ms2s(timeout_ms + 999); } scheduler = pcmk_new_scheduler(); if (scheduler == NULL) { return ENOMEM; } xpath = pcmk__assert_asprintf("/" PCMK_XE_CIB "/" PCMK_XE_STATUS "/" PCMK__XE_NODE_STATE "/" PCMK__XE_LRM "/" PCMK__XE_LRM_RESOURCES "/" PCMK__XE_LRM_RESOURCE "/" PCMK__XE_LRM_RSC_OP "[@" PCMK__XA_RC_CODE "='%d']", PCMK_OCF_UNKNOWN); do { /* Abort if timeout is reached */ time_diff = expire_time - time(NULL); if (time_diff <= 0) { print_pending_actions(out, scheduler->priv->actions); rc = ETIME; break; } pcmk__info("Waiting up to %lld seconds for cluster actions to complete", (long long) time_diff); if (rc == pcmk_rc_ok) { /* this avoids sleep on first loop iteration */ sleep(WAIT_SLEEP_S); } /* Get latest transition graph */ pcmk_reset_scheduler(scheduler); rc = update_scheduler_input(out, scheduler, cib, NULL); if (rc != pcmk_rc_ok) { break; } pcmk__set_scheduler_flags(scheduler, pcmk__sched_no_counts); pcmk__schedule_actions(scheduler); if (!printed_version_warning) { /* If the DC has a different version than the local node, the two * could come to different conclusions about what actions need to be * done. Warn the user in this case. * * @TODO A possible long-term solution would be to reimplement the * wait as a new controller operation that would be forwarded to the * DC. However, that would have potential problems of its own. */ const char *dc_version = NULL; dc_version = g_hash_table_lookup(scheduler->priv->options, PCMK_OPT_DC_VERSION); if (!pcmk__str_eq(dc_version, PACEMAKER_VERSION "-" BUILD_VERSION, pcmk__str_casei)) { out->info(out, "warning: wait option may not work properly in " "mixed-version cluster"); printed_version_warning = true; } } search = pcmk__xpath_search(scheduler->input->doc, xpath); pending_unknown_state_resources = (pcmk__xpath_num_results(search) > 0); xmlXPathFreeObject(search); } while (actions_are_pending(scheduler->priv->actions) || pending_unknown_state_resources); pcmk_free_scheduler(scheduler); free(xpath); return rc; } static const char * get_action(const char *rsc_action) { const char *action = NULL; if (pcmk__str_eq(rsc_action, "validate", pcmk__str_casei)) { action = PCMK_ACTION_VALIDATE_ALL; } else if (pcmk__str_eq(rsc_action, "force-check", pcmk__str_casei)) { action = PCMK_ACTION_MONITOR; } else if (pcmk__strcase_any_of(rsc_action, "force-start", "force-stop", "force-demote", "force-promote", NULL)) { action = rsc_action+6; } else { action = rsc_action; } return action; } /*! * \brief Set up environment variables as expected by resource agents * * When the cluster executes resource agents, it adds certain environment * variables (directly or via resource meta-attributes) expected by some * resource agents. Add the essential ones that many resource agents expect, so * the behavior is the same for command-line execution. * * \param[in,out] params Resource parameters that will be passed to agent * \param[in] timeout_ms Action timeout (in milliseconds) * \param[in] check_level OCF check level * \param[in] verbosity Verbosity level */ static void set_agent_environment(GHashTable *params, guint timeout_ms, int check_level, int verbosity) { g_hash_table_insert(params, crm_meta_name(PCMK_META_TIMEOUT), pcmk__assert_asprintf("%u", timeout_ms)); pcmk__insert_dup(params, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET); if (check_level >= 0) { char *level = pcmk__assert_asprintf("%d", check_level); setenv("OCF_CHECK_LEVEL", level, 1); free(level); } pcmk__set_env_option(PCMK__ENV_DEBUG, ((verbosity > 0)? "1" : "0"), true); if (verbosity > 1) { setenv("OCF_TRACE_RA", "1", 1); } /* A resource agent using the standard ocf-shellfuncs library will not print * messages to stderr if it doesn't have a controlling terminal (e.g. if * crm_resource is called via script or ssh). This forces it to do so. */ setenv("OCF_TRACE_FILE", "/dev/stderr", 0); } /*! * \internal * \brief Apply command-line overrides to resource parameters * * \param[in,out] params Parameters to be passed to agent * \param[in] overrides Parameters to override (or NULL if none) */ static void apply_overrides(GHashTable *params, GHashTable *overrides) { if (overrides != NULL) { GHashTableIter iter; char *name = NULL; char *value = NULL; g_hash_table_iter_init(&iter, overrides); while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &value)) { pcmk__insert_dup(params, name, value); } } } /* Takes ownership of params. * Does not modify override_hash or its contents. */ crm_exit_t cli_resource_execute_from_params(pcmk__output_t *out, const char *rsc_name, const char *rsc_class, const char *rsc_prov, const char *rsc_type, const char *rsc_action, GHashTable *params, GHashTable *override_hash, guint timeout_ms, int resource_verbose, gboolean force, int check_level) { const char *class = rsc_class; const char *action = get_action(rsc_action); crm_exit_t exit_code = CRM_EX_OK; svc_action_t *op = NULL; // If no timeout was provided, use the same default as the cluster if (timeout_ms == 0U) { timeout_ms = PCMK_DEFAULT_ACTION_TIMEOUT_MS; } set_agent_environment(params, timeout_ms, check_level, resource_verbose); apply_overrides(params, override_hash); // services__create_resource_action() takes ownership of params on success op = services__create_resource_action(rsc_name? rsc_name : "test", rsc_class, rsc_prov, rsc_type, action, 0, QB_MIN(timeout_ms, INT_MAX), params, 0); if (op == NULL) { out->err(out, "Could not execute %s using %s%s%s:%s: %s", action, rsc_class, (rsc_prov? ":" : ""), (rsc_prov? rsc_prov : ""), rsc_type, strerror(ENOMEM)); g_hash_table_destroy(params); return CRM_EX_OSERR; } #if PCMK__ENABLE_SERVICE if (pcmk__str_eq(rsc_class, PCMK_RESOURCE_CLASS_SERVICE, pcmk__str_casei)) { class = resources_find_service_class(rsc_type); } #endif if (!pcmk__is_set(pcmk_get_ra_caps(class), pcmk_ra_cap_cli_exec)) { services__format_result(op, CRM_EX_UNIMPLEMENT_FEATURE, PCMK_EXEC_ERROR, "Manual execution of the %s standard is " "unsupported", pcmk__s(class, "unspecified")); } if (op->rc != PCMK_OCF_UNKNOWN) { exit_code = op->rc; goto done; } services_action_sync(op); // Map results to OCF codes for consistent reporting to user { enum ocf_exitcode ocf_code = services_result2ocf(class, action, op->rc); // Cast variable instead of function return to keep compilers happy exit_code = (crm_exit_t) ocf_code; } done: out->message(out, "resource-agent-action", resource_verbose, rsc_class, rsc_prov, rsc_type, rsc_name, rsc_action, override_hash, exit_code, op->status, services__exit_reason(op), op->stdout_data, op->stderr_data); services_action_free(op); return exit_code; } /*! * \internal * \brief Get the timeout the cluster would use for an action * * \param[in] rsc Resource that action is for * \param[in] action Name of action */ static guint get_action_timeout(pcmk_resource_t *rsc, const char *action) { long long timeout_ms = -1LL; xmlNode *op = pcmk__find_action_config(rsc, action, 0, true); GHashTable *meta = pcmk__unpack_action_meta(rsc, NULL, action, 0, op); if ((pcmk__scan_ll(g_hash_table_lookup(meta, PCMK_META_TIMEOUT), &timeout_ms, -1LL) != pcmk_rc_ok) || (timeout_ms <= 0LL)) { timeout_ms = PCMK_DEFAULT_ACTION_TIMEOUT_MS; } g_hash_table_destroy(meta); return (guint) QB_MIN(timeout_ms, UINT_MAX); } // Does not modify override_hash or its contents crm_exit_t cli_resource_execute(pcmk_resource_t *rsc, const char *requested_name, const char *rsc_action, GHashTable *override_hash, guint timeout_ms, cib_t *cib, int resource_verbose, bool force, int check_level) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; crm_exit_t exit_code = CRM_EX_OK; const char *rid = requested_name; const char *rtype = NULL; const char *rprov = NULL; const char *rclass = NULL; GHashTable *params = NULL; pcmk__assert(rsc != NULL); scheduler = rsc->priv->scheduler; out = scheduler->priv->out; if (pcmk__strcase_any_of(rsc_action, "force-start", "force-demote", "force-promote", NULL)) { if (pcmk__is_clone(rsc)) { GList *nodes = cli_resource_search(rsc, requested_name); if(nodes != NULL && force == FALSE) { out->err(out, "It is not safe to %s %s here: the cluster claims it is already active", rsc_action, rsc->id); out->err(out, "Try setting " PCMK_META_TARGET_ROLE "=" PCMK_ROLE_STOPPED " first or specifying the force option"); return CRM_EX_UNSAFE; } g_list_free_full(nodes, free); } } if (pcmk__is_clone(rsc)) { /* Grab the first child resource in the hope it's not a group */ rsc = rsc->priv->children->data; } if (pcmk__is_group(rsc)) { out->err(out, "Sorry, the %s option doesn't support group resources", rsc_action); return CRM_EX_UNIMPLEMENT_FEATURE; } else if (pcmk__is_bundled(rsc)) { out->err(out, "Sorry, the %s option doesn't support bundled resources", rsc_action); return CRM_EX_UNIMPLEMENT_FEATURE; } rclass = pcmk__xe_get(rsc->priv->xml, PCMK_XA_CLASS); rprov = pcmk__xe_get(rsc->priv->xml, PCMK_XA_PROVIDER); rtype = pcmk__xe_get(rsc->priv->xml, PCMK_XA_TYPE); params = generate_resource_params(rsc); // @TODO use local node if (timeout_ms == 0U) { timeout_ms = get_action_timeout(rsc, get_action(rsc_action)); } if (!pcmk__is_anonymous_clone(rsc->priv->parent)) { rid = rsc->id; } exit_code = cli_resource_execute_from_params(out, rid, rclass, rprov, rtype, rsc_action, params, override_hash, timeout_ms, resource_verbose, force, check_level); return exit_code; } // \return Standard Pacemaker return code int cli_resource_move(pcmk_resource_t *rsc, const char *rsc_id, const pcmk_node_t *dest, const char *move_lifetime, cib_t *cib, bool promoted_role_only, bool force) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; unsigned int count = 0; pcmk_node_t *current = NULL; bool cur_is_dest = false; const char *active_s = promoted_role_only? "promoted" : "active"; pcmk__assert((rsc != NULL) && (dest != NULL)); scheduler = rsc->priv->scheduler; out = scheduler->priv->out; if (promoted_role_only && !pcmk__is_set(rsc->flags, pcmk__rsc_promotable)) { pcmk_resource_t *p = uber_parent(rsc); if (pcmk__is_set(p->flags, pcmk__rsc_promotable)) { /* @TODO This is dead code. If rsc is part of a promotable clone, * then it has the pcmk__rsc_promotable flag set. * * This was added by 36e4b490. Prior to that commit, we were * checking whether rsc itself is the promotable clone, and if not, * trying to get a promotable clone ancestor. * * As of that commit, we check whether rsc has the promotable flag * set. But if it has a promotable clone ancestor, that flag is set. * * Question: Should we drop this block and use rsc for the move, or * should we check whether rsc is a clone instead of only checking * whether the promotable flag is set (as we did prior to 36e4b490)? * The latter seems appropriate, especially considering the block * below with promoted_count and promoted_node; but we need to trace * and test. */ out->info(out, "Using parent '%s' for move instead of '%s'", rsc->id, rsc_id); rsc_id = p->id; rsc = p; } else { out->info(out, "Ignoring --promoted option: %s is not promotable", rsc_id); promoted_role_only = false; } } current = pe__find_active_requires(rsc, &count); if (pcmk__is_set(rsc->flags, pcmk__rsc_promotable)) { unsigned int promoted_count = 0; pcmk_node_t *promoted_node = NULL; for (GList *iter = rsc->priv->children; iter != NULL; iter = iter->next) { pcmk_resource_t *child = iter->data; enum rsc_role_e child_role = child->priv->fns->state(child, true); if (child_role == pcmk_role_promoted) { rsc = child; promoted_node = pcmk__current_node(child); promoted_count++; } } if (promoted_role_only || (promoted_count != 0)) { count = promoted_count; current = promoted_node; } } if (count > 1) { if (!pcmk__is_clone(rsc)) { return pcmk_rc_multiple; } current = NULL; } if (pcmk__same_node(current, dest)) { if (!force) { return pcmk_rc_already; } cur_is_dest = true; pcmk__info("%s is already %s on %s, reinforcing placement with " "location constraint", rsc_id, active_s, pcmk__node_name(dest)); } /* @TODO The constraint changes in the following commands should done * atomically in a single CIB transaction, to avoid the possibility of * multiple moves */ /* Clear any previous prefer constraints across all nodes. */ cli_resource_clear(rsc_id, NULL, scheduler->nodes, cib, false, force); /* Clear any previous ban constraints on 'dest'. */ cli_resource_clear(rsc_id, dest->priv->name, scheduler->nodes, cib, true, force); /* Record an explicit preference for 'dest' */ rc = cli_resource_prefer(out, rsc_id, dest->priv->name, move_lifetime, cib, promoted_role_only, PCMK_ROLE_PROMOTED); pcmk__trace("%s%s now prefers %s%s", rsc->id, (promoted_role_only? " (promoted)" : ""), pcmk__node_name(dest), (force? " (forced)" : "")); /* Ban the current location if force is set and the current location is not * the destination. It is possible to use move to enforce a location without * regard for where the resource is currently located. */ if (force && !cur_is_dest) { /* Ban the original location if possible */ if (current != NULL) { cli_resource_ban(out, rsc_id, current->priv->name, move_lifetime, cib, promoted_role_only, PCMK_ROLE_PROMOTED); } else if (count > 1) { out->info(out, "Resource '%s' is currently %s in %u locations. " "One may now move to %s", rsc_id, active_s, count, pcmk__node_name(dest)); out->info(out, "To prevent '%s' from being %s at a specific location, " "specify a node", rsc_id, active_s); } else { pcmk__trace("Not banning %s from its current location: not active", rsc_id); } } return rc; }