diff --git a/daemons/based/based_remote.c b/daemons/based/based_remote.c index 0ef01292ef..ea9fe3f991 100644 --- a/daemons/based/based_remote.c +++ b/daemons/based/based_remote.c @@ -1,661 +1,663 @@ /* * 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) { crm_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(); crm_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 { crm_warn("Starting plain-text listener on port %d", port); } #ifndef HAVE_PAM crm_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) { crm_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) { crm_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) { crm_err("Cannot bind to listener socket: %s", pcmk_rc_str(errno)); close(*ssock); free(ssock); return -2; } if (listen(*ssock, 10) == -1) { crm_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); crm_debug("Started listener on port %d", port); return *ssock; } static int check_group_membership(const char *usr, const char *grp) { int index = 0; - struct passwd *pwd = NULL; + gid_t gid = 0; struct group *group = NULL; + int rc = pcmk_rc_ok; - pwd = getpwnam(usr); - if (pwd == NULL) { - crm_notice("Rejecting remote client: '%s' is not a valid user", usr); + rc = pcmk__lookup_user(usr, NULL, &gid); + if (rc != pcmk_rc_ok) { + crm_notice("Rejecting remote client: could not find user '%s': %s", + usr, pcmk_rc_str(rc)); return FALSE; } - group = getgrgid(pwd->pw_gid); + group = getgrgid(gid); if (group != NULL && pcmk__str_eq(grp, group->gr_name, pcmk__str_none)) { return TRUE; } group = getgrnam(grp); if (group == NULL) { crm_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; } } crm_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)) { crm_warn("Rejecting remote client: Unrecognizable message " "(element '%s' not '" PCMK__XE_CIB_COMMAND "')", login->name); crm_log_xml_debug(login, "bad"); return FALSE; } tmp = pcmk__xe_get(login, PCMK_XA_OP); if (!pcmk__str_eq(tmp, "authenticate", pcmk__str_casei)) { crm_warn("Rejecting remote client: Unrecognizable message " "(operation '%s' not 'authenticate')", tmp); crm_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) { crm_warn("Rejecting remote client: No %s given", ((user == NULL)? "username" : "password")); crm_log_xml_debug(login, "bad"); return FALSE; } crm_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); crm_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) { crm_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) { crm_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); crm_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; } crm_trace("Cleaning up after client %s disconnect", pcmk__client_name(client)); num_clients--; crm_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: crm_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); crm_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); crm_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; } crm_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 */ crm_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); } crm_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) { crm_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: crm_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 */ crm_err("PAM reported an error"); data = NULL; break; default: crm_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) { crm_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) { crm_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) { crm_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) { crm_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)) { crm_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) { crm_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 crm_warn("Rejecting remote user %s because this build does not have " "PAM support", user); return false; #endif } diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h index 9bae7973f1..09e5e3a091 100644 --- a/include/crm/common/internal.h +++ b/include/crm/common/internal.h @@ -1,405 +1,408 @@ /* * Copyright 2015-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_INTERNAL__H #define PCMK__CRM_COMMON_INTERNAL__H -#include // pid_t, getpid() +#include // struct passwd +#include // getpid() #include // bool #include // uint8_t, uint64_t +#include // pid_t, uid_t, gid_t #include // PRIu64 #include // guint, GList, GHashTable #include // xmlNode #include // do_crm_log_unlikely(), etc. #include // mainloop_io_t, struct ipc_client_callbacks #include #include #include #include #include #include #include #include #include #include // pcmk__assert_asprintf() #include #ifdef __cplusplus extern "C" { #endif /* This says whether the current application is a Pacemaker daemon or not, * and is used to change default logging settings such as whether to log to * stderr, etc., as well as a few other details such as whether blackbox signal * handling is enabled. * * It is set when logging is initialized, and does not need to be set directly. */ extern bool pcmk__is_daemon; // Number of elements in a statically defined array #define PCMK__NELEM(a) ((int) (sizeof(a)/sizeof(a[0])) ) #if PCMK__ENABLE_CIBSECRETS /* internal CIB utilities (from cib_secrets.c) */ int pcmk__substitute_secrets(const char *rsc_id, GHashTable *params); #endif /* internal main loop utilities (from mainloop.c) */ int pcmk__add_mainloop_ipc(crm_ipc_t *ipc, int priority, void *userdata, const struct ipc_client_callbacks *callbacks, mainloop_io_t **source); guint pcmk__mainloop_timer_get_period(const mainloop_timer_t *timer); /* internal name/value utilities (from nvpair.c) */ int pcmk__scan_nvpair(const gchar *input, gchar **name, gchar **value); char *pcmk__format_nvpair(const char *name, const char *value, const char *units); /* internal procfs utilities (from procfs.c) */ pid_t pcmk__procfs_pid_of(const char *name); unsigned int pcmk__procfs_num_cores(void); int pcmk__procfs_pid2path(pid_t pid, char path[], size_t path_size); bool pcmk__procfs_has_pids(void); DIR *pcmk__procfs_fd_dir(void); void pcmk__sysrq_trigger(char t); bool pcmk__throttle_cib_load(const char *server, float *load); bool pcmk__throttle_load_avg(float *load); /* internal functions related to process IDs (from pid.c) */ /*! * \internal * \brief Check whether process exists (by PID and optionally executable path) * * \param[in] pid PID of process to check * \param[in] daemon If not NULL, path component to match with procfs entry * * \return Standard Pacemaker return code * \note Particular return codes of interest include pcmk_rc_ok for alive, * ESRCH for process is not alive (verified by kill and/or executable path * match), EACCES for caller unable or not allowed to check. A result of * "alive" is less reliable when \p daemon is not provided or procfs is * not available, since there is no guarantee that the PID has not been * recycled for another process. * \note This function cannot be used to verify \e authenticity of the process. */ int pcmk__pid_active(pid_t pid, const char *daemon); int pcmk__read_pidfile(const char *filename, pid_t *pid); int pcmk__pidfile_matches(const char *filename, pid_t expected_pid, const char *expected_name, pid_t *pid); int pcmk__lock_pidfile(const char *filename, const char *name); // bitwise arithmetic utilities /*! * \internal * \brief Set specified flags in a flag group * * \param[in] function Function name of caller * \param[in] line Line number of caller * \param[in] log_level Log a message at this level * \param[in] flag_type Label describing this flag group (for logging) * \param[in] target Name of object whose flags these are (for logging) * \param[in] flag_group Flag group being manipulated * \param[in] flags Which flags in the group should be set * \param[in] flags_str Readable equivalent of \p flags (for logging) * * \return Possibly modified flag group */ static inline uint64_t pcmk__set_flags_as(const char *function, int line, uint8_t log_level, const char *flag_type, const char *target, uint64_t flag_group, uint64_t flags, const char *flags_str) { uint64_t result = flag_group | flags; if (result != flag_group) { do_crm_log_unlikely(log_level, "%s flags %#.8" PRIx64 " (%s) for %s set by %s:%d", pcmk__s(flag_type, "Group of"), flags, pcmk__s(flags_str, "flags"), pcmk__s(target, "target"), function, line); } return result; } /*! * \internal * \brief Clear specified flags in a flag group * * \param[in] function Function name of caller * \param[in] line Line number of caller * \param[in] log_level Log a message at this level * \param[in] flag_type Label describing this flag group (for logging) * \param[in] target Name of object whose flags these are (for logging) * \param[in] flag_group Flag group being manipulated * \param[in] flags Which flags in the group should be cleared * \param[in] flags_str Readable equivalent of \p flags (for logging) * * \return Possibly modified flag group */ static inline uint64_t pcmk__clear_flags_as(const char *function, int line, uint8_t log_level, const char *flag_type, const char *target, uint64_t flag_group, uint64_t flags, const char *flags_str) { uint64_t result = flag_group & ~flags; if (result != flag_group) { do_crm_log_unlikely(log_level, "%s flags %#.8" PRIx64 " (%s) for %s cleared by %s:%d", pcmk__s(flag_type, "Group of"), flags, pcmk__s(flags_str, "flags"), pcmk__s(target, "target"), function, line); } return result; } /*! * \internal * \brief Check whether any of specified flags are set in a flag group * * \param[in] flag_group Flag group to check whether \p flags_to_check are * set * \param[in] flags_to_check Flags to check whether set in \p flag_group * * \retval \c true if \p flags_to_check is nonzero and any of its flags are * set in \p flag_group * \retval \c false otherwise */ static inline bool pcmk__any_flags_set(uint64_t flag_group, uint64_t flags_to_check) { return (flag_group & flags_to_check) != 0; } /*! * \internal * \brief Check whether all of specified flags are set in a flag group * * \param[in] flag_group Flag group to check whether \p flags_to_check are * set * \param[in] flags_to_check Flags to check whether set in \p flag_group * * \retval \c true if all flags in \p flags_to_check are set in \p flag_group * or if \p flags_to_check is 0 * \retval \c false otherwise */ static inline bool pcmk__all_flags_set(uint64_t flag_group, uint64_t flags_to_check) { return (flag_group & flags_to_check) == flags_to_check; } /*! * \internal * \brief Convenience alias for \c pcmk__all_flags_set(), to check single flag * * This is truly identical to \c pcmk__all_flags_set() but allows a call that's * shorter and semantically clearer for checking a single flag. * * \param[in] flag_group Flag group (check whether \p flag is set in this) * \param[in] flag Flag (check whether this is set in \p flag_group) * * \retval \c true if \p flag is set in \p flag_group or if \p flag is 0 * \retval \c false otherwise */ static inline bool pcmk__is_set(uint64_t flag_group, uint64_t flag) { return pcmk__all_flags_set(flag_group, flag); } /*! * \internal * \brief Get readable string for whether specified flags are set * * \param[in] flag_group Group of flags to check * \param[in] flags Which flags in \p flag_group should be checked * * \return "true" if all \p flags are set in \p flag_group, otherwise "false" */ static inline const char * pcmk__flag_text(uint64_t flag_group, uint64_t flags) { return pcmk__btoa(pcmk__all_flags_set(flag_group, flags)); } // miscellaneous utilities (from utils.c) int pcmk__daemon_user(uid_t *uid, gid_t *gid); void pcmk__daemonize(const char *name, const char *pidfile); char *pcmk__generate_uuid(void); +int pcmk__lookup_user(const char *name, uid_t *uid, gid_t *gid); void pcmk__panic(const char *reason); pid_t pcmk__locate_sbd(void); void pcmk__sleep_ms(unsigned int ms); guint pcmk__create_timer(guint interval_ms, GSourceFunc fn, gpointer data); guint pcmk__timeout_ms2s(guint timeout_ms); extern int pcmk__score_red; extern int pcmk__score_green; extern int pcmk__score_yellow; /*! * \internal * \brief Allocate new zero-initialized memory, asserting on failure * * \param[in] file File where \p function is located * \param[in] function Calling function * \param[in] line Line within \p file * \param[in] nmemb Number of elements to allocate memory for * \param[in] size Size of each element * * \return Newly allocated memory of of size nmemb * size (guaranteed * not to be \c NULL) * * \note The caller is responsible for freeing the return value using \c free(). */ static inline void * pcmk__assert_alloc_as(const char *file, const char *function, uint32_t line, size_t nmemb, size_t size) { void *ptr = calloc(nmemb, size); if (ptr == NULL) { crm_abort(file, function, line, "Out of memory", FALSE, TRUE); crm_exit(CRM_EX_OSERR); } return ptr; } /*! * \internal * \brief Allocate new zero-initialized memory, asserting on failure * * \param[in] nmemb Number of elements to allocate memory for * \param[in] size Size of each element * * \return Newly allocated memory of of size nmemb * size (guaranteed * not to be \c NULL) * * \note The caller is responsible for freeing the return value using \c free(). */ #define pcmk__assert_alloc(nmemb, size) \ pcmk__assert_alloc_as(__FILE__, __func__, __LINE__, nmemb, size) /*! * \internal * \brief Resize a dynamically allocated memory block * * \param[in] ptr Memory block to resize (or NULL to allocate new memory) * \param[in] size New size of memory block in bytes (must be > 0) * * \return Pointer to resized memory block * * \note This asserts on error, so the result is guaranteed to be non-NULL * (which is the main advantage of this over directly using realloc()). */ static inline void * pcmk__realloc(void *ptr, size_t size) { void *new_ptr; // realloc(p, 0) can replace free(p) but this wrapper can't pcmk__assert(size > 0); new_ptr = realloc(ptr, size); if (new_ptr == NULL) { free(ptr); abort(); } return new_ptr; } static inline char * pcmk__getpid_s(void) { return pcmk__assert_asprintf("%lu", (unsigned long) getpid()); } // More efficient than g_list_length(list) == 1 static inline bool pcmk__list_of_1(GList *list) { return list && (list->next == NULL); } // More efficient than g_list_length(list) > 1 static inline bool pcmk__list_of_multiple(GList *list) { return list && (list->next != NULL); } /* convenience functions for failure-related node attributes */ #define PCMK__FAIL_COUNT_PREFIX "fail-count" #define PCMK__LAST_FAILURE_PREFIX "last-failure" /*! * \internal * \brief Generate a failure-related node attribute name for a resource * * \param[in] prefix Start of attribute name * \param[in] rsc_id Resource name * \param[in] op Operation name * \param[in] interval_ms Operation interval * * \return Newly allocated string with attribute name * * \note Failure attributes are named like PREFIX-RSC#OP_INTERVAL (for example, * "fail-count-myrsc#monitor_30000"). The '#' is used because it is not * a valid character in a resource ID, to reliably distinguish where the * operation name begins. The '_' is used simply to be more comparable to * action labels like "myrsc_monitor_30000". */ static inline char * pcmk__fail_attr_name(const char *prefix, const char *rsc_id, const char *op, guint interval_ms) { CRM_CHECK(prefix && rsc_id && op, return NULL); return pcmk__assert_asprintf("%s-%s#%s_%u", prefix, rsc_id, op, interval_ms); } static inline char * pcmk__failcount_name(const char *rsc_id, const char *op, guint interval_ms) { return pcmk__fail_attr_name(PCMK__FAIL_COUNT_PREFIX, rsc_id, op, interval_ms); } static inline char * pcmk__lastfailure_name(const char *rsc_id, const char *op, guint interval_ms) { return pcmk__fail_attr_name(PCMK__LAST_FAILURE_PREFIX, rsc_id, op, interval_ms); } // internal resource agent functions (from agents.c) int pcmk__effective_rc(int rc); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_INTERNAL__H diff --git a/lib/common/mock.c b/lib/common/mock.c index 08f025b1dd..559ec5dd3a 100644 --- a/lib/common/mock.c +++ b/lib/common/mock.c @@ -1,461 +1,450 @@ /* * Copyright 2021-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include // pid_t, size_t #include #include #include #include #include #include "mock_private.h" /* This file is only used when running "make check". It is built into * libcrmcommon_test.a, not into libcrmcommon.so. It is used to support * constructing mock versions of library functions for unit testing. * * HOW TO ADD A MOCKED FUNCTION: * * - In this file, declare a bool pcmk__mock_X variable, and define a __wrap_X * function with the same prototype as the actual function that performs the * desired behavior if pcmk__mock_X is true and calls __real_X otherwise. * You can use cmocka's mock_type() and mock_ptr_type() to pass extra * information to the mocked function (see existing examples for details). * * - In mock_private.h, add declarations for extern bool pcmk__mock_X and the * __real_X and __wrap_X function prototypes. * * - In mk/tap.mk, add the function name to the WRAPPED variable. * * HOW TO USE A MOCKED FUNCTION: * * - #include "mock_private.h" in your test file. * * - Write your test cases using pcmk__mock_X and cmocka's will_return() as * needed per the comments for the mocked function below. See existing test * cases for examples. */ // LCOV_EXCL_START /* abort() * * Always mock abort - there's no pcmk__mock_abort tuneable to control this. * Because abort calls _exit(), which doesn't run any of the things registered * with atexit(), coverage numbers do not get written out. This most noticably * affects places where we are testing that things abort when they should. * * The solution is this wrapper that is always enabled when we are running * unit tests (mock.c does not get included for the regular libcrmcommon.so). * All it does is dump coverage data and call the real abort(). */ _Noreturn void __wrap_abort(void) { #if (PCMK__WITH_COVERAGE == 1) __gcov_dump(); #endif __real_abort(); } /* calloc() * * If pcmk__mock_calloc is set to true, later calls to calloc() will return * NULL and must be preceded by: * * expect_*(__wrap_calloc, nmemb[, ...]); * expect_*(__wrap_calloc, size[, ...]); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_calloc = false; void * __wrap_calloc(size_t nmemb, size_t size) { if (!pcmk__mock_calloc) { return __real_calloc(nmemb, size); } check_expected(nmemb); check_expected(size); return NULL; } /* getenv() * * If pcmk__mock_getenv is set to true, later calls to getenv() must be preceded * by: * * expect_*(__wrap_getenv, name[, ...]); * will_return(__wrap_getenv, return_value); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_getenv = false; char * __wrap_getenv(const char *name) { if (!pcmk__mock_getenv) { return __real_getenv(name); } check_expected_ptr(name); return mock_ptr_type(char *); } /* realloc() * * If pcmk__mock_realloc is set to true, later calls to realloc() will return * NULL and must be preceded by: * * expect_*(__wrap_realloc, ptr[, ...]); * expect_*(__wrap_realloc, size[, ...]); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_realloc = false; void * __wrap_realloc(void *ptr, size_t size) { if (!pcmk__mock_realloc) { return __real_realloc(ptr, size); } check_expected_ptr(ptr); check_expected(size); return NULL; } /* setenv() * * If pcmk__mock_setenv is set to true, later calls to setenv() must be preceded * by: * * expect_*(__wrap_setenv, name[, ...]); * expect_*(__wrap_setenv, value[, ...]); * expect_*(__wrap_setenv, overwrite[, ...]); * will_return(__wrap_setenv, errno_to_set); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html * * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. */ bool pcmk__mock_setenv = false; int __wrap_setenv(const char *name, const char *value, int overwrite) { if (!pcmk__mock_setenv) { return __real_setenv(name, value, overwrite); } check_expected_ptr(name); check_expected_ptr(value); check_expected(overwrite); errno = mock_type(int); return (errno == 0)? 0 : -1; } /* unsetenv() * * If pcmk__mock_unsetenv is set to true, later calls to unsetenv() must be * preceded by: * * expect_*(__wrap_unsetenv, name[, ...]); * will_return(__wrap_setenv, errno_to_set); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html * * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. */ bool pcmk__mock_unsetenv = false; int __wrap_unsetenv(const char *name) { if (!pcmk__mock_unsetenv) { return __real_unsetenv(name); } check_expected_ptr(name); errno = mock_type(int); return (errno == 0)? 0 : -1; } /* getpid() * * If pcmk__mock_getpid is set to true, later calls to getpid() must be preceded * by: * * will_return(__wrap_getpid, return_value); */ bool pcmk__mock_getpid = false; pid_t __wrap_getpid(void) { return pcmk__mock_getpid? mock_type(pid_t) : __real_getpid(); } /* setgrent(), getgrent() and endgrent() * * If pcmk__mock_grent is set to true, getgrent() will behave as if the only * groups on the system are: * * - grp0 (user0, user1) * - grp1 (user1) * - grp2 (user2, user1) */ bool pcmk__mock_grent = false; // Index of group that will be returned next from getgrent() static int group_idx = 0; // Data used for testing static const char* grp0_members[] = { "user0", "user1", NULL }; static const char* grp1_members[] = { "user1", NULL }; static const char* grp2_members[] = { "user2", "user1", NULL }; /* An array of "groups" (a struct from grp.h) * * The members of the groups are initalized here to some testing data, casting * away the consts to make the compiler happy and simplify initialization. We * never actually change these variables during the test! * * string literal = const char* (cannot be changed b/c ? ) * vs. char* (it's getting casted to this) */ static const int NUM_GROUPS = 3; static struct group groups[] = { {(char*)"grp0", (char*)"", 0, (char**)grp0_members}, {(char*)"grp1", (char*)"", 1, (char**)grp1_members}, {(char*)"grp2", (char*)"", 2, (char**)grp2_members}, }; // This function resets the group_idx to 0. void __wrap_setgrent(void) { if (pcmk__mock_grent) { group_idx = 0; } else { __real_setgrent(); } } /* This function returns the next group entry in the list of groups, or * NULL if there aren't any left. * group_idx is a global variable which keeps track of where you are in the list */ struct group * __wrap_getgrent(void) { if (pcmk__mock_grent) { if (group_idx >= NUM_GROUPS) { return NULL; } return &groups[group_idx++]; } else { return __real_getgrent(); } } void __wrap_endgrent(void) { if (!pcmk__mock_grent) { __real_endgrent(); } } /* fopen() * * If pcmk__mock_fopen is set to true, later calls to fopen() must be * preceded by: * * expect_*(__wrap_fopen, pathname[, ...]); * expect_*(__wrap_fopen, mode[, ...]); * will_return(__wrap_fopen, errno_to_set); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html * * This has two mocked functions, since fopen() is sometimes actually fopen64(). */ bool pcmk__mock_fopen = false; FILE * __wrap_fopen(const char *pathname, const char *mode) { if (pcmk__mock_fopen) { check_expected_ptr(pathname); check_expected_ptr(mode); errno = mock_type(int); if (errno != 0) { return NULL; } else { return __real_fopen(pathname, mode); } } else { return __real_fopen(pathname, mode); } } #ifdef HAVE_FOPEN64 FILE * __wrap_fopen64(const char *pathname, const char *mode) { if (pcmk__mock_fopen) { check_expected_ptr(pathname); check_expected_ptr(mode); errno = mock_type(int); if (errno != 0) { return NULL; } else { return __real_fopen64(pathname, mode); } } else { return __real_fopen64(pathname, mode); } } #endif -/* getpwnam_r() +/* getpwnam() * - * If pcmk__mock_getpwnam_r is set to true, later calls to getpwnam_r() must be + * If pcmk__mock_getpwnam is set to true, later calls to getpwnam() must be * preceded by: * - * expect_*(__wrap_getpwnam_r, name[, ...]); - * expect_*(__wrap_getpwnam_r, pwd[, ...]); - * expect_*(__wrap_getpwnam_r, buf[, ...]); - * expect_*(__wrap_getpwnam_r, buflen[, ...]); - * expect_*(__wrap_getpwnam_r, result[, ...]); - * will_return(__wrap_getpwnam_r, return_value); - * will_return(__wrap_getpwnam_r, ptr_to_result_struct); + * expect_*(__wrap_getpwnam, name[, ...]); + * will_return(__wrap_getpwnam, errno_to_set); + * will_return(__wrap_getpwnam, ptr_to_result_struct); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ -bool pcmk__mock_getpwnam_r = false; +bool pcmk__mock_getpwnam = false; -int -__wrap_getpwnam_r(const char *name, struct passwd *pwd, char *buf, - size_t buflen, struct passwd **result) +struct passwd * +__wrap_getpwnam(const char *name) { - if (pcmk__mock_getpwnam_r) { - int retval = mock_type(int); - + if (pcmk__mock_getpwnam) { check_expected_ptr(name); - check_expected_ptr(pwd); - check_expected_ptr(buf); - check_expected(buflen); - check_expected_ptr(result); - *result = mock_ptr_type(struct passwd *); - return retval; + errno = mock_type(int); + return mock_ptr_type(struct passwd *); } else { - return __real_getpwnam_r(name, pwd, buf, buflen, result); + return __real_getpwnam(name); } } /* * If pcmk__mock_readlink is set to true, later calls to readlink() must be * preceded by: * * expect_*(__wrap_readlink, path[, ...]); * expect_*(__wrap_readlink, buf[, ...]); * expect_*(__wrap_readlink, bufsize[, ...]); * will_return(__wrap_readlink, errno_to_set); * will_return(__wrap_readlink, link_contents); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html * * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. */ bool pcmk__mock_readlink = false; ssize_t __wrap_readlink(const char *restrict path, char *restrict buf, size_t bufsize) { if (pcmk__mock_readlink) { const char *contents = NULL; check_expected_ptr(path); check_expected_ptr(buf); check_expected(bufsize); errno = mock_type(int); contents = mock_ptr_type(const char *); if (errno == 0) { strncpy(buf, contents, bufsize - 1); return strlen(contents); } return -1; } else { return __real_readlink(path, buf, bufsize); } } /* strdup() * * If pcmk__mock_strdup is set to true, later calls to strdup() will return * NULL and must be preceded by: * * expect_*(__wrap_strdup, s[, ...]); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_strdup = false; char * __wrap_strdup(const char *s) { if (!pcmk__mock_strdup) { return __real_strdup(s); } check_expected_ptr(s); return NULL; } // LCOV_EXCL_STOP diff --git a/lib/common/mock_private.h b/lib/common/mock_private.h index 272714f187..5fa2d918fa 100644 --- a/lib/common/mock_private.h +++ b/lib/common/mock_private.h @@ -1,97 +1,95 @@ /* - * Copyright 2021-2024 the Pacemaker project contributors + * Copyright 2021-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__COMMON_MOCK_PRIVATE__H #define PCMK__COMMON_MOCK_PRIVATE__H #include // struct passwd #include // bool #include // FILE #include #include #include // pid_t, size_t #include #include #include // struct group #include // _Noreturn #ifdef __cplusplus extern "C" { // C++ doesn't support the restrict keyword #define restrict #endif /* This header is for the sole use of libcrmcommon_test and unit tests */ _Noreturn void __real_abort(void); _Noreturn void __wrap_abort(void); extern bool pcmk__mock_calloc; void *__real_calloc(size_t nmemb, size_t size); void *__wrap_calloc(size_t nmemb, size_t size); extern bool pcmk__mock_fopen; FILE *__real_fopen(const char *pathname, const char *mode); FILE *__wrap_fopen(const char *pathname, const char *mode); #ifdef HAVE_FOPEN64 FILE *__real_fopen64(const char *pathname, const char *mode); FILE *__wrap_fopen64(const char *pathname, const char *mode); #endif extern bool pcmk__mock_getenv; char *__real_getenv(const char *name); char *__wrap_getenv(const char *name); extern bool pcmk__mock_realloc; void *__real_realloc(void *ptr, size_t size); void *__wrap_realloc(void *ptr, size_t size); extern bool pcmk__mock_setenv; int __real_setenv(const char *name, const char *value, int overwrite); int __wrap_setenv(const char *name, const char *value, int overwrite); extern bool pcmk__mock_unsetenv; int __real_unsetenv(const char *name); int __wrap_unsetenv(const char *name); extern bool pcmk__mock_getpid; pid_t __real_getpid(void); pid_t __wrap_getpid(void); extern bool pcmk__mock_grent; void __real_setgrent(void); void __wrap_setgrent(void); struct group * __wrap_getgrent(void); struct group * __real_getgrent(void); void __wrap_endgrent(void); void __real_endgrent(void); -extern bool pcmk__mock_getpwnam_r; -int __real_getpwnam_r(const char *name, struct passwd *pwd, - char *buf, size_t buflen, struct passwd **result); -int __wrap_getpwnam_r(const char *name, struct passwd *pwd, - char *buf, size_t buflen, struct passwd **result); +extern bool pcmk__mock_getpwnam; +struct passwd *__real_getpwnam(const char *name); +struct passwd *__wrap_getpwnam(const char *name); extern bool pcmk__mock_readlink; ssize_t __real_readlink(const char *restrict path, char *restrict buf, size_t bufsize); ssize_t __wrap_readlink(const char *restrict path, char *restrict buf, size_t bufsize); extern bool pcmk__mock_strdup; char *__real_strdup(const char *s); char *__wrap_strdup(const char *s); #ifdef __cplusplus } #endif #endif // PCMK__COMMON_MOCK_PRIVATE__H diff --git a/lib/common/tests/utils/Makefile.am b/lib/common/tests/utils/Makefile.am index 1ecd87d591..9e78afe01d 100644 --- a/lib/common/tests/utils/Makefile.am +++ b/lib/common/tests/utils/Makefile.am @@ -1,25 +1,25 @@ # # Copyright 2020-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 $(top_srcdir)/mk/common.mk include $(top_srcdir)/mk/tap.mk include $(top_srcdir)/mk/unittest.mk # Add "_test" to the end of all test program names to simplify .gitignore. check_PROGRAMS = compare_version_test \ - crm_user_lookup_test \ pcmk__daemon_user_test \ pcmk__fail_attr_name_test \ pcmk__failcount_name_test \ pcmk__getpid_s_test \ pcmk__lastfailure_name_test \ + pcmk__lookup_user_test \ pcmk__realloc_test \ pcmk__timeout_ms2s_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/utils/crm_user_lookup_test.c b/lib/common/tests/utils/crm_user_lookup_test.c deleted file mode 100644 index 5842ec5027..0000000000 --- a/lib/common/tests/utils/crm_user_lookup_test.c +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2022 the Pacemaker project contributors - * - * The version control history for this file may have further details. - * - * This source code is licensed under the GNU General Public License version 2 - * or later (GPLv2+) WITHOUT ANY WARRANTY. - */ - -#include - -#include - -#include "crmcommon_private.h" -#include "mock_private.h" - -#include -#include - -static void -calloc_fails(void **state) -{ - uid_t uid; - gid_t gid; - - pcmk__mock_calloc = true; // calloc() will return NULL - - expect_value(__wrap_calloc, nmemb, 1); - expect_value(__wrap_calloc, size, PCMK__PW_BUFFER_LEN); - assert_int_equal(crm_user_lookup("hauser", &uid, &gid), -ENOMEM); - - pcmk__mock_calloc = false; // Use real calloc() -} - -static void -getpwnam_r_fails(void **state) -{ - uid_t uid; - gid_t gid; - - // Set getpwnam_r() return value and result parameter - pcmk__mock_getpwnam_r = true; - - expect_string(__wrap_getpwnam_r, name, "hauser"); - expect_any(__wrap_getpwnam_r, pwd); - expect_any(__wrap_getpwnam_r, buf); - expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN); - expect_any(__wrap_getpwnam_r, result); - will_return(__wrap_getpwnam_r, EIO); - will_return(__wrap_getpwnam_r, NULL); - - assert_int_equal(crm_user_lookup("hauser", &uid, &gid), -EIO); - - pcmk__mock_getpwnam_r = false; -} - -static void -no_matching_pwent(void **state) -{ - uid_t uid; - gid_t gid; - - // Set getpwnam_r() return value and result parameter - pcmk__mock_getpwnam_r = true; - - expect_string(__wrap_getpwnam_r, name, "hauser"); - expect_any(__wrap_getpwnam_r, pwd); - expect_any(__wrap_getpwnam_r, buf); - expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN); - expect_any(__wrap_getpwnam_r, result); - will_return(__wrap_getpwnam_r, 0); - will_return(__wrap_getpwnam_r, NULL); - - assert_int_equal(crm_user_lookup("hauser", &uid, &gid), -EINVAL); - - pcmk__mock_getpwnam_r = false; -} - -static void -entry_found(void **state) -{ - uid_t uid; - gid_t gid; - - /* We don't care about any of the other fields of the password entry, so just - * leave them blank. - */ - struct passwd returned_ent = { .pw_uid = 1000, .pw_gid = 1000 }; - - /* Test getpwnam_r returning a valid passwd entry, but we don't pass uid or gid. */ - - // Set getpwnam_r() return value and result parameter - pcmk__mock_getpwnam_r = true; - - expect_string(__wrap_getpwnam_r, name, "hauser"); - expect_any(__wrap_getpwnam_r, pwd); - expect_any(__wrap_getpwnam_r, buf); - expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN); - expect_any(__wrap_getpwnam_r, result); - will_return(__wrap_getpwnam_r, 0); - will_return(__wrap_getpwnam_r, &returned_ent); - - assert_int_equal(crm_user_lookup("hauser", NULL, NULL), 0); - - /* Test getpwnam_r returning a valid passwd entry, and we do pass uid and gid. */ - - // Set getpwnam_r() return value and result parameter - expect_string(__wrap_getpwnam_r, name, "hauser"); - expect_any(__wrap_getpwnam_r, pwd); - expect_any(__wrap_getpwnam_r, buf); - expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN); - expect_any(__wrap_getpwnam_r, result); - will_return(__wrap_getpwnam_r, 0); - will_return(__wrap_getpwnam_r, &returned_ent); - - assert_int_equal(crm_user_lookup("hauser", &uid, &gid), 0); - assert_int_equal(uid, 1000); - assert_int_equal(gid, 1000); - - pcmk__mock_getpwnam_r = false; -} - -PCMK__UNIT_TEST(NULL, NULL, - cmocka_unit_test(calloc_fails), - cmocka_unit_test(getpwnam_r_fails), - cmocka_unit_test(no_matching_pwent), - cmocka_unit_test(entry_found)) diff --git a/lib/common/tests/utils/pcmk__daemon_user_test.c b/lib/common/tests/utils/pcmk__daemon_user_test.c index dbf063c72c..1cbd01315b 100644 --- a/lib/common/tests/utils/pcmk__daemon_user_test.c +++ b/lib/common/tests/utils/pcmk__daemon_user_test.c @@ -1,90 +1,81 @@ /* * Copyright 2022-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 "crmcommon_private.h" #include "mock_private.h" #include #include static void no_matching_pwent(void **state) { uid_t uid = 0; gid_t gid = 0; - // Set getpwnam_r() return value and result parameter - pcmk__mock_getpwnam_r = true; + pcmk__mock_getpwnam = true; - expect_string(__wrap_getpwnam_r, name, "hacluster"); - expect_any(__wrap_getpwnam_r, pwd); - expect_any(__wrap_getpwnam_r, buf); - expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN); - expect_any(__wrap_getpwnam_r, result); - will_return(__wrap_getpwnam_r, ENOENT); - will_return(__wrap_getpwnam_r, NULL); + expect_string(__wrap_getpwnam, name, "hacluster"); + will_return(__wrap_getpwnam, 0); + will_return(__wrap_getpwnam, NULL); assert_int_equal(pcmk__daemon_user(&uid, &gid), ENOENT); - pcmk__mock_getpwnam_r = false; + pcmk__mock_getpwnam = false; } static void entry_found(void **state) { uid_t uid = 0; gid_t gid = 0; // We don't care about the other fields of the passwd entry struct passwd returned_ent = { .pw_uid = 1000, .pw_gid = 1000 }; - // Test getpwnam_r() returning a valid passwd entry with null output args + // Test getpwnam() returning a valid passwd entry with null output args - pcmk__mock_getpwnam_r = true; + pcmk__mock_getpwnam = true; - expect_string(__wrap_getpwnam_r, name, "hacluster"); - expect_any(__wrap_getpwnam_r, pwd); - expect_any(__wrap_getpwnam_r, buf); - expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN); - expect_any(__wrap_getpwnam_r, result); - will_return(__wrap_getpwnam_r, 0); - will_return(__wrap_getpwnam_r, &returned_ent); + expect_string(__wrap_getpwnam, name, "hacluster"); + will_return(__wrap_getpwnam, 0); + will_return(__wrap_getpwnam, &returned_ent); assert_int_equal(pcmk__daemon_user(NULL, NULL), pcmk_rc_ok); - // Test getpwnam_r() returning a valid passwd entry with non-NULL outputs + // Test getpwnam() returning a valid passwd entry with non-NULL outputs /* We don't need to call expect_*() or will_return() again because * pcmk__daemon_user() will have cached the uid/gid from the previous call - * and won't make another call to getpwnam_r(). + * and won't make another call to getpwnam(). */ assert_int_equal(pcmk__daemon_user(&uid, NULL), pcmk_rc_ok); assert_int_equal(uid, 1000); assert_int_equal(gid, 0); uid = 0; assert_int_equal(pcmk__daemon_user(NULL, &gid), pcmk_rc_ok); assert_int_equal(uid, 0); assert_int_equal(gid, 1000); gid = 0; assert_int_equal(pcmk__daemon_user(&uid, &gid), pcmk_rc_ok); assert_int_equal(uid, 1000); assert_int_equal(gid, 1000); - pcmk__mock_getpwnam_r = false; + pcmk__mock_getpwnam = false; } PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(no_matching_pwent), cmocka_unit_test(entry_found)) diff --git a/lib/common/tests/utils/pcmk__lookup_user_test.c b/lib/common/tests/utils/pcmk__lookup_user_test.c new file mode 100644 index 0000000000..58fa6ae59d --- /dev/null +++ b/lib/common/tests/utils/pcmk__lookup_user_test.c @@ -0,0 +1,147 @@ +/* + * Copyright 2022-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 "crmcommon_private.h" +#include "mock_private.h" + +#include +#include + +/*! + * \internal + * \brief Perform one test of \c pcmk__lookup_user() + * + * \param[in] name \c name argument for \c pcmk__lookup_user() + * \param[in] uid \c uid argument for \c pcmk__lookup_user() + * (unchanged upon return) + * \param[in] gid \c gid argument for \c pcmk__lookup_user() + * (unchanged upon return) + * \param[in] expected_rc Expected return code of \c pcmk__lookup_user() + * \param[in] expected_uid Expected value at \p *uid after + * \c pcmk__lookup_user() call + * \param[in] expected_gid Expected value at \p *gid after + * \c pcmk__lookup_user() call + */ +static void +assert_lookup_user(const char *name, uid_t *uid, gid_t *gid, int expected_rc, + uid_t expected_uid, gid_t expected_gid) +{ + uid_t uid_orig = ((uid != NULL)? *uid : 0); + gid_t gid_orig = ((gid != NULL)? *gid : 0); + + assert_int_equal(pcmk__lookup_user(name, uid, gid), expected_rc); + + if (uid != NULL) { + assert_int_equal(*uid, expected_uid); + *uid = uid_orig; + } + if (gid != NULL) { + assert_int_equal(*gid, expected_gid); + *gid = gid_orig; + } +} + +static void +null_name(void **state) +{ + uid_t uid = 0; + gid_t gid = 0; + + // These dump core via CRM_CHECK() + assert_lookup_user(NULL, NULL, NULL, EINVAL, 0, 0); + assert_lookup_user(NULL, NULL, &gid, EINVAL, 0, 0); + assert_lookup_user(NULL, &uid, NULL, EINVAL, 0, 0); + assert_lookup_user(NULL, &uid, &gid, EINVAL, 0, 0); +} + +static void +getpwnam_fails(void **state) +{ + uid_t uid = 0; + gid_t gid = 0; + + pcmk__mock_getpwnam = true; + + expect_string(__wrap_getpwnam, name, "hauser"); + will_return(__wrap_getpwnam, EIO); // errno + will_return(__wrap_getpwnam, NULL); // return value + assert_lookup_user("hauser", &uid, &gid, EIO, 0, 0); + + pcmk__mock_getpwnam = false; +} + +static void +no_matching_pwent(void **state) +{ + uid_t uid = 0; + gid_t gid = 0; + + pcmk__mock_getpwnam = true; + + /* errno may or may not be set when no matching passwd entry is found. + * However, if the return value is NULL and errno == 0, then we can be sure + * no entry was found. In other words, it's sufficient but not necessary. So + * this is our test case for "no matching entry," and we should return + * ENOENT. + */ + expect_string(__wrap_getpwnam, name, "hauser"); + will_return(__wrap_getpwnam, 0); // errno + will_return(__wrap_getpwnam, NULL); // return value + assert_lookup_user("hauser", &uid, &gid, ENOENT, 0, 0); + + pcmk__mock_getpwnam = false; +} + +static void +entry_found(void **state) +{ + uid_t uid = 0; + gid_t gid = 0; + + // We don't care about the other fields of the passwd entry + struct passwd returned_ent = { .pw_uid = 1000, .pw_gid = 1000 }; + + pcmk__mock_getpwnam = true; + + // NULL uid and NULL gid + expect_string(__wrap_getpwnam, name, "hauser"); + will_return(__wrap_getpwnam, 0); + will_return(__wrap_getpwnam, &returned_ent); + assert_lookup_user("hauser", NULL, NULL, pcmk_rc_ok, 0, 0); + + // Non-NULL uid and NULL gid + expect_string(__wrap_getpwnam, name, "hauser"); + will_return(__wrap_getpwnam, 0); + will_return(__wrap_getpwnam, &returned_ent); + assert_lookup_user("hauser", &uid, NULL, pcmk_rc_ok, 1000, 0); + + // NULL uid and non-NULL gid + expect_string(__wrap_getpwnam, name, "hauser"); + will_return(__wrap_getpwnam, 0); + will_return(__wrap_getpwnam, &returned_ent); + assert_lookup_user("hauser", NULL, &gid, pcmk_rc_ok, 0, 1000); + + // Non-NULL uid and non-NULL gid + expect_string(__wrap_getpwnam, name, "hauser"); + will_return(__wrap_getpwnam, 0); + will_return(__wrap_getpwnam, &returned_ent); + assert_lookup_user("hauser", &uid, &gid, pcmk_rc_ok, 1000, 1000); + + pcmk__mock_getpwnam = false; +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_name), + cmocka_unit_test(getpwnam_fails), + cmocka_unit_test(no_matching_pwent), + cmocka_unit_test(entry_found)) diff --git a/lib/common/utils.c b/lib/common/utils.c index bb223084e3..2f9bd671ca 100644 --- a/lib/common/utils.c +++ b/lib/common/utils.c @@ -1,552 +1,592 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "crmcommon_private.h" CRM_TRACE_INIT_DATA(common); bool pcmk__config_has_error = false; bool pcmk__config_has_warning = false; char *crm_system_name = NULL; /*! * \brief Free all memory used by libcrmcommon * * Free all global memory allocated by the libcrmcommon library. This should be * called before exiting a process that uses the library, and the process should * not call any libcrmcommon or libxml2 APIs after calling this one. */ void pcmk_common_cleanup(void) { // @TODO This isn't really everything, move all cleanup here mainloop_cleanup(); pcmk__xml_cleanup(); pcmk__free_common_logger(); qb_log_fini(); // Don't log anything after this point free(crm_system_name); crm_system_name = NULL; } bool pcmk__is_user_in_group(const char *user, const char *group) { struct group *grent; char **gr_mem; if (user == NULL || group == NULL) { return false; } setgrent(); while ((grent = getgrent()) != NULL) { if (grent->gr_mem == NULL) { continue; } if(strcmp(group, grent->gr_name) != 0) { continue; } gr_mem = grent->gr_mem; while (*gr_mem != NULL) { if (!strcmp(user, *gr_mem++)) { endgrent(); return true; } } } endgrent(); return false; } +int +pcmk__lookup_user(const char *name, uid_t *uid, gid_t *gid) +{ + struct passwd *pwentry = NULL; + + CRM_CHECK(name != NULL, return EINVAL); + + // getpwnam() is not thread-safe, but Pacemaker is single-threaded + errno = 0; + pwentry = getpwnam(name); + if (pwentry == NULL) { + /* Either an error occurred or no passwd entry was found. + * + * The value of errno is implementation-dependent if no passwd entry is + * found. The POSIX specification does not consider it an error. + * POSIX.1-2008 specifies that errno shall not be changed in this case, + * while POSIX.1-2001 does not specify the value of errno in this case. + * The man page on Linux notes that a variety of values have been + * observed in practice. So an implementation may set errno to an + * arbitrary value, despite the POSIX specification. + * + * However, if pwentry == NULL and errno == 0, then we know that no + * matching entry was found and there was no error. So we default to + * ENOENT as our return code. + */ + return ((errno != 0)? errno : ENOENT); + } + + if (uid != NULL) { + *uid = pwentry->pw_uid; + } + if (gid != NULL) { + *gid = pwentry->pw_gid; + } + crm_trace("User %s has uid=%lld gid=%lld", name, + (long long) pwentry->pw_uid, (long long) pwentry->pw_gid); + + return pcmk_rc_ok; +} + int crm_user_lookup(const char *name, uid_t * uid, gid_t * gid) { int rc = pcmk_ok; char *buffer = NULL; struct passwd pwd; struct passwd *pwentry = NULL; buffer = calloc(1, PCMK__PW_BUFFER_LEN); if (buffer == NULL) { return -ENOMEM; } rc = getpwnam_r(name, &pwd, buffer, PCMK__PW_BUFFER_LEN, &pwentry); if (pwentry) { if (uid) { *uid = pwentry->pw_uid; } if (gid) { *gid = pwentry->pw_gid; } crm_trace("User %s has uid=%d gid=%d", name, pwentry->pw_uid, pwentry->pw_gid); } else { rc = rc? -rc : -EINVAL; crm_info("User %s lookup: %s", name, pcmk_strerror(rc)); } free(buffer); return rc; } /*! * \internal * \brief Get user and group IDs of Pacemaker daemon user * * \param[out] uid Where to store daemon user ID (can be \c NULL) * \param[out] gid Where to store daemon group ID (can be \c NULL) * * \return Standard Pacemaker return code */ int pcmk__daemon_user(uid_t *uid, gid_t *gid) { static uid_t daemon_uid = 0; static gid_t daemon_gid = 0; static bool found = false; if (!found) { - int rc = crm_user_lookup(CRM_DAEMON_USER, &daemon_uid, &daemon_gid); + int rc = pcmk__lookup_user(CRM_DAEMON_USER, &daemon_uid, &daemon_gid); - if (rc != pcmk_ok) { - return pcmk_legacy2rc(rc); + if (rc != pcmk_rc_ok) { + return rc; } found = true; } if (uid != NULL) { *uid = daemon_uid; } if (gid != NULL) { *gid = daemon_gid; } return pcmk_rc_ok; } /*! * \internal * \brief Return the integer equivalent of a portion of a string * * \param[in] text Pointer to beginning of string portion * \param[out] end_text This will point to next character after integer */ static int version_helper(const char *text, const char **end_text) { int atoi_result = -1; pcmk__assert(end_text != NULL); errno = 0; if (text != NULL && text[0] != 0) { /* seemingly sacrificing const-correctness -- because while strtol doesn't modify the input, it doesn't want to artificially taint the "end_text" pointer-to-pointer-to-first-char-in-string with constness in case the input wasn't actually constant -- by semantic definition not a single character will get modified so it shall be perfectly safe to make compiler happy with dropping "const" qualifier here */ atoi_result = (int) strtol(text, (char **) end_text, 10); if (errno == EINVAL) { crm_err("Conversion of '%s' %c failed", text, text[0]); atoi_result = -1; } } return atoi_result; } /* * version1 < version2 : -1 * version1 = version2 : 0 * version1 > version2 : 1 */ int compare_version(const char *version1, const char *version2) { int rc = 0; int lpc = 0; const char *ver1_iter, *ver2_iter; if (version1 == NULL && version2 == NULL) { return 0; } else if (version1 == NULL) { return -1; } else if (version2 == NULL) { return 1; } ver1_iter = version1; ver2_iter = version2; while (1) { int digit1 = 0; int digit2 = 0; lpc++; if (ver1_iter == ver2_iter) { break; } if (ver1_iter != NULL) { digit1 = version_helper(ver1_iter, &ver1_iter); } if (ver2_iter != NULL) { digit2 = version_helper(ver2_iter, &ver2_iter); } if (digit1 < digit2) { rc = -1; break; } else if (digit1 > digit2) { rc = 1; break; } if (ver1_iter != NULL && *ver1_iter == '.') { ver1_iter++; } if (ver1_iter != NULL && *ver1_iter == '\0') { ver1_iter = NULL; } if (ver2_iter != NULL && *ver2_iter == '.') { ver2_iter++; } if (ver2_iter != NULL && *ver2_iter == 0) { ver2_iter = NULL; } } if (rc == 0) { crm_trace("%s == %s (%d)", version1, version2, lpc); } else if (rc < 0) { crm_trace("%s < %s (%d)", version1, version2, lpc); } else if (rc > 0) { crm_trace("%s > %s (%d)", version1, version2, lpc); } return rc; } /*! * \internal * \brief Convert the current process to a daemon process * * Fork a child process, exit the parent, create a PID file with the current * process ID, and close the standard input/output/error file descriptors. * Exit instead if a daemon is already running and using the PID file. * * \param[in] name Daemon executable name * \param[in] pidfile File name to use as PID file */ void pcmk__daemonize(const char *name, const char *pidfile) { int rc; pid_t pid; /* Check before we even try... */ rc = pcmk__pidfile_matches(pidfile, 1, name, &pid); if ((rc != pcmk_rc_ok) && (rc != ENOENT)) { crm_err("%s: already running [pid %lld in %s]", name, (long long) pid, pidfile); printf("%s: already running [pid %lld in %s]\n", name, (long long) pid, pidfile); crm_exit(CRM_EX_ERROR); } pid = fork(); if (pid < 0) { fprintf(stderr, "%s: could not start daemon\n", name); crm_perror(LOG_ERR, "fork"); crm_exit(CRM_EX_OSERR); } else if (pid > 0) { crm_exit(CRM_EX_OK); } rc = pcmk__lock_pidfile(pidfile, name); if (rc != pcmk_rc_ok) { crm_err("Could not lock '%s' for %s: %s " QB_XS " rc=%d", pidfile, name, pcmk_rc_str(rc), rc); printf("Could not lock '%s' for %s: %s (%d)\n", pidfile, name, pcmk_rc_str(rc), rc); crm_exit(CRM_EX_ERROR); } umask(S_IWGRP | S_IWOTH | S_IROTH); close(STDIN_FILENO); pcmk__open_devnull(O_RDONLY); // stdin (fd 0) close(STDOUT_FILENO); pcmk__open_devnull(O_WRONLY); // stdout (fd 1) close(STDERR_FILENO); pcmk__open_devnull(O_WRONLY); // stderr (fd 2) } /* @FIXME uuid.h is an optional header per configure.ac, and we include it * conditionally above. But uuid_generate() and uuid_unparse() depend on it, on * many or perhaps all systems with libuuid. So it's not clear how it would ever * be optional in practice. * * Note that these functions are not POSIX, although there is probably no good * portable alternative. * * We do list libuuid as a build dependency in INSTALL.md already. */ #ifdef HAVE_UUID_UUID_H #include #endif // HAVE_UUID_UUID_H /*! * \internal * \brief Generate a 37-byte (36 bytes plus null terminator) UUID string * * \return Newly allocated UUID string * * \note The caller is responsible for freeing the return value using \c free(). */ char * pcmk__generate_uuid(void) { uuid_t uuid; // uuid_unparse() converts a UUID to a 37-byte string (including null byte) char *buffer = pcmk__assert_alloc(37, sizeof(char)); uuid_generate(uuid); uuid_unparse(uuid, buffer); return buffer; } /*! * \internal * \brief Sleep for given milliseconds * * \param[in] ms Time to sleep * * \note The full time might not be slept if a signal is received. */ void pcmk__sleep_ms(unsigned int ms) { // @TODO Impose a sane maximum sleep to avoid hanging a process for long //CRM_CHECK(ms <= MAX_SLEEP, ms = MAX_SLEEP); // Use sleep() for any whole seconds if (ms >= 1000) { sleep(ms / 1000); ms -= ms / 1000; } if (ms == 0) { return; } #if defined(HAVE_NANOSLEEP) // nanosleep() is POSIX-2008, so prefer that { struct timespec req = { .tv_sec = 0, .tv_nsec = (long) (ms * 1000000) }; nanosleep(&req, NULL); } #elif defined(HAVE_USLEEP) // usleep() is widely available, though considered obsolete usleep((useconds_t) ms); #else // Otherwise use a trick with select() timeout { struct timeval tv = { .tv_sec = 0, .tv_usec = (suseconds_t) ms }; select(0, NULL, NULL, NULL, &tv); } #endif } /*! * \internal * \brief Add a timer * * \param[in] interval_ms The interval for the function to be called, in ms * \param[in] fn The function to be called * \param[in] data Data to be passed to fn (can be NULL) * * \return The ID of the event source */ guint pcmk__create_timer(guint interval_ms, GSourceFunc fn, gpointer data) { pcmk__assert(interval_ms != 0 && fn != NULL); if (interval_ms % 1000 == 0) { /* In case interval_ms is 0, the call to pcmk__timeout_ms2s ensures * an interval of one second. */ return g_timeout_add_seconds(pcmk__timeout_ms2s(interval_ms), fn, data); } else { return g_timeout_add(interval_ms, fn, data); } } /*! * \internal * \brief Convert milliseconds to seconds * * \param[in] timeout_ms The interval, in ms * * \return If \p timeout_ms is 0, return 0. Otherwise, return the number of * seconds, rounded to the nearest integer, with a minimum of 1. */ guint pcmk__timeout_ms2s(guint timeout_ms) { guint quot, rem; if (timeout_ms == 0) { return 0; } else if (timeout_ms < 1000) { return 1; } quot = timeout_ms / 1000; rem = timeout_ms % 1000; if (rem >= 500) { quot += 1; } return quot; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include static void _gnutls_log_func(int level, const char *msg) { crm_trace("%s", msg); } void crm_gnutls_global_init(void) { signal(SIGPIPE, SIG_IGN); gnutls_global_init(); gnutls_global_set_log_level(8); gnutls_global_set_log_function(_gnutls_log_func); } /*! * \brief Check whether string represents a client name used by cluster daemons * * \param[in] name String to check * * \return true if name is standard client name used by daemons, false otherwise * * \note This is provided by the client, and so cannot be used by itself as a * secure means of authentication. */ bool crm_is_daemon_name(const char *name) { return pcmk__str_any_of(name, "attrd", CRM_SYSTEM_CIB, CRM_SYSTEM_CRMD, CRM_SYSTEM_DC, CRM_SYSTEM_LRMD, CRM_SYSTEM_MCP, CRM_SYSTEM_PENGINE, CRM_SYSTEM_TENGINE, "pacemaker-attrd", "pacemaker-based", "pacemaker-controld", "pacemaker-execd", "pacemaker-fenced", "pacemaker-remoted", "pacemaker-schedulerd", "stonith-ng", "stonithd", NULL); } char * crm_generate_uuid(void) { return pcmk__generate_uuid(); } int pcmk_daemon_user(uid_t *uid, gid_t *gid) { static uid_t daemon_uid; static gid_t daemon_gid; static bool found = false; int rc = pcmk_ok; if (!found) { rc = crm_user_lookup(CRM_DAEMON_USER, &daemon_uid, &daemon_gid); if (rc == pcmk_ok) { found = true; } } if (found) { if (uid) { *uid = daemon_uid; } if (gid) { *gid = daemon_gid; } } return rc; } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/services/services.c b/lib/services/services.c index d105595102..566fcc226b 100644 --- a/lib/services/services.c +++ b/lib/services/services.c @@ -1,1379 +1,1383 @@ /* * Copyright 2010-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU 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 "services_private.h" #include "services_ocf.h" #if PCMK__ENABLE_LSB #include "services_lsb.h" #endif #if SUPPORT_SYSTEMD # include #endif /* TODO: Develop a rollover strategy */ static int operations = 0; static GHashTable *recurring_actions = NULL; /* ops waiting to run async because of conflicting active * pending ops */ static GList *blocked_ops = NULL; /* ops currently active (in-flight) */ static GList *inflight_ops = NULL; static void handle_blocked_ops(void); /*! * \brief Find first service class that can provide a specified agent * * \param[in] agent Name of agent to search for * * \return Service class if found, NULL otherwise * * \note The priority is LSB then systemd. It would be preferable to put systemd * first, but LSB merely requires a file existence check, while systemd * requires contacting DBus. */ const char * resources_find_service_class(const char *agent) { #if PCMK__ENABLE_LSB if (services__lsb_agent_exists(agent)) { return PCMK_RESOURCE_CLASS_LSB; } #endif #if SUPPORT_SYSTEMD if (systemd_unit_exists(agent)) { return PCMK_RESOURCE_CLASS_SYSTEMD; } #endif return NULL; } static inline void init_recurring_actions(void) { if (recurring_actions == NULL) { recurring_actions = pcmk__strkey_table(NULL, NULL); } } /*! * \internal * \brief Check whether op is in-flight systemd op * * \param[in] op Operation to check * * \return TRUE if op is in-flight systemd op */ static inline gboolean inflight_systemd(const svc_action_t *op) { return pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_SYSTEMD, pcmk__str_casei) && (g_list_find(inflight_ops, op) != NULL); } /*! * \internal * \brief Expand "service" alias to an actual resource class * * \param[in] rsc Resource name (for logging only) * \param[in] standard Resource class as configured * \param[in] agent Agent name to look for * * \return Newly allocated string with actual resource class * * \note The caller is responsible for calling free() on the result. */ static char * expand_resource_class(const char *rsc, const char *standard, const char *agent) { char *expanded_class = NULL; #if PCMK__ENABLE_SERVICE if (strcasecmp(standard, PCMK_RESOURCE_CLASS_SERVICE) == 0) { const char *found_class = resources_find_service_class(agent); if (found_class != NULL) { crm_debug("Found %s agent %s for %s", found_class, agent, rsc); expanded_class = pcmk__str_copy(found_class); } else { const char *default_standard = NULL; #if PCMK__ENABLE_LSB default_standard = PCMK_RESOURCE_CLASS_LSB; #elif SUPPORT_SYSTEMD default_standard = PCMK_RESOURCE_CLASS_SYSTEMD; #else #error No standards supported for service alias (configure script bug) #endif crm_info("Assuming resource class %s for agent %s for %s", default_standard, agent, rsc); expanded_class = pcmk__str_copy(default_standard); } } #endif if (expanded_class == NULL) { expanded_class = pcmk__str_copy(standard); } return expanded_class; } /*! * \internal * \brief Create a simple svc_action_t instance * * \return Newly allocated instance (or NULL if not enough memory) */ static svc_action_t * new_action(void) { svc_action_t *op = calloc(1, sizeof(svc_action_t)); if (op == NULL) { return NULL; } op->opaque = calloc(1, sizeof(svc_action_private_t)); if (op->opaque == NULL) { free(op); return NULL; } // Initialize result services__set_result(op, PCMK_OCF_UNKNOWN, PCMK_EXEC_UNKNOWN, NULL); return op; } static bool required_argument_missing(uint32_t ra_caps, const char *name, const char *standard, const char *provider, const char *agent, const char *action) { if (pcmk__str_empty(name)) { crm_info("Cannot create operation without resource name (bug?)"); return true; } if (pcmk__str_empty(standard)) { crm_info("Cannot create operation for %s without resource class (bug?)", name); return true; } if (pcmk__is_set(ra_caps, pcmk_ra_cap_provider) && pcmk__str_empty(provider)) { crm_info("Cannot create operation for %s resource %s " "without provider (bug?)", standard, name); return true; } if (pcmk__str_empty(agent)) { crm_info("Cannot create operation for %s without agent name (bug?)", name); return true; } if (pcmk__str_empty(action)) { crm_info("Cannot create operation for %s without action name (bug?)", name); return true; } return false; } // \return Standard Pacemaker return code (pcmk_rc_ok or ENOMEM) static int copy_action_arguments(svc_action_t *op, uint32_t ra_caps, const char *name, const char *standard, const char *provider, const char *agent, const char *action) { op->rsc = strdup(name); if (op->rsc == NULL) { return ENOMEM; } op->agent = strdup(agent); if (op->agent == NULL) { return ENOMEM; } op->standard = expand_resource_class(name, standard, agent); if (op->standard == NULL) { return ENOMEM; } if (pcmk__is_set(ra_caps, pcmk_ra_cap_status) && pcmk__str_eq(action, PCMK_ACTION_MONITOR, pcmk__str_casei)) { action = PCMK_ACTION_STATUS; } op->action = strdup(action); if (op->action == NULL) { return ENOMEM; } if (pcmk__is_set(ra_caps, pcmk_ra_cap_provider)) { op->provider = strdup(provider); if (op->provider == NULL) { return ENOMEM; } } return pcmk_rc_ok; } // Takes ownership of params svc_action_t * services__create_resource_action(const char *name, const char *standard, const char *provider, const char *agent, const char *action, guint interval_ms, int timeout, GHashTable *params, enum svc_action_flags flags) { svc_action_t *op = NULL; uint32_t ra_caps = pcmk_get_ra_caps(standard); int rc = pcmk_rc_ok; op = new_action(); if (op == NULL) { crm_crit("Cannot prepare action: %s", strerror(ENOMEM)); if (params != NULL) { g_hash_table_destroy(params); } return NULL; } op->interval_ms = interval_ms; op->timeout = timeout; op->flags = flags; op->sequence = ++operations; // Take ownership of params if (pcmk__is_set(ra_caps, pcmk_ra_cap_params)) { op->params = params; } else if (params != NULL) { g_hash_table_destroy(params); params = NULL; } if (required_argument_missing(ra_caps, name, standard, provider, agent, action)) { services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR_FATAL, "Required agent or action information missing"); return op; } op->id = pcmk__op_key(name, action, interval_ms); if (copy_action_arguments(op, ra_caps, name, standard, provider, agent, action) != pcmk_rc_ok) { crm_crit("Cannot prepare %s action for %s: %s", action, name, strerror(ENOMEM)); services__handle_exec_error(op, ENOMEM); return op; } if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_OCF) == 0) { rc = services__ocf_prepare(op); #if PCMK__ENABLE_LSB } else if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_LSB) == 0) { rc = services__lsb_prepare(op); #endif #if SUPPORT_SYSTEMD } else if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_SYSTEMD) == 0) { rc = services__systemd_prepare(op); #endif } else { crm_info("Unknown resource standard: %s", op->standard); rc = ENOENT; } if (rc != pcmk_rc_ok) { crm_info("Cannot prepare %s operation for %s: %s", action, name, strerror(rc)); services__handle_exec_error(op, rc); } return op; } svc_action_t * resources_action_create(const char *name, const char *standard, const char *provider, const char *agent, const char *action, guint interval_ms, int timeout, GHashTable *params, enum svc_action_flags flags) { svc_action_t *op = services__create_resource_action(name, standard, provider, agent, action, interval_ms, timeout, params, flags); if (op == NULL || op->rc != 0) { services_action_free(op); return NULL; } else { // Preserve public API backward compatibility op->rc = PCMK_OCF_OK; op->status = PCMK_EXEC_DONE; return op; } } svc_action_t * services_action_create_generic(const char *exec, const char *args[]) { svc_action_t *op = new_action(); pcmk__mem_assert(op); op->opaque->exec = strdup(exec); op->opaque->args[0] = strdup(exec); if ((op->opaque->exec == NULL) || (op->opaque->args[0] == NULL)) { crm_crit("Cannot prepare action for '%s': %s", exec, strerror(ENOMEM)); services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, strerror(ENOMEM)); return op; } if (args == NULL) { return op; } for (int cur_arg = 1; args[cur_arg - 1] != NULL; cur_arg++) { if (cur_arg == PCMK__NELEM(op->opaque->args)) { crm_info("Cannot prepare action for '%s': Too many arguments", exec); services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR_HARD, "Too many arguments"); break; } op->opaque->args[cur_arg] = strdup(args[cur_arg - 1]); if (op->opaque->args[cur_arg] == NULL) { crm_crit("Cannot prepare action for '%s': %s", exec, strerror(ENOMEM)); services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, strerror(ENOMEM)); break; } } return op; } /*! * \brief Create an alert agent action * * \param[in] id Alert ID * \param[in] exec Path to alert agent executable * \param[in] timeout Action timeout * \param[in] params Parameters to use with action * \param[in] sequence Action sequence number * \param[in] cb_data Data to pass to callback function * * \return New action on success, NULL on error * \note It is the caller's responsibility to free cb_data. * The caller should not free params explicitly. */ svc_action_t * services_alert_create(const char *id, const char *exec, int timeout, GHashTable *params, int sequence, void *cb_data) { svc_action_t *action = services_action_create_generic(exec, NULL); action->id = pcmk__str_copy(id); action->standard = pcmk__str_copy(PCMK_RESOURCE_CLASS_ALERT); action->timeout = timeout; action->params = params; action->sequence = sequence; action->cb_data = cb_data; return action; } /*! * \brief Set the user and group that an action will execute as * * \param[in,out] op Action to modify * \param[in] user Name of user to execute action as * \param[in] group Name of group to execute action as * * \return pcmk_ok on success, -errno otherwise * * \note This will have no effect unless the process executing the action runs * as root and the action is not a systemd action. We could implement this * for systemd by adding User= and Group= to [Service] in the override * file, but that seems more likely to cause problems than be useful. */ int services_action_user(svc_action_t *op, const char *user) { + int rc = pcmk_ok; + CRM_CHECK((op != NULL) && (user != NULL), return -EINVAL); - return crm_user_lookup(user, &(op->opaque->uid), &(op->opaque->gid)); + + rc = pcmk__lookup_user(user, &(op->opaque->uid), &(op->opaque->gid)); + return pcmk_rc2legacy(rc); } /*! * \brief Execute an alert agent action * * \param[in,out] action Action to execute * \param[in] cb Function to call when action completes * * \return TRUE if the library will free action, FALSE otherwise * * \note If this function returns FALSE, it is the caller's responsibility to * free the action with services_action_free(). However, unless someone * intentionally creates a recurring alert action, this will never return * FALSE. */ gboolean services_alert_async(svc_action_t *action, void (*cb)(svc_action_t *op)) { action->synchronous = false; action->opaque->callback = cb; return services__execute_file(action) == pcmk_rc_ok; } #if HAVE_DBUS /*! * \internal * \brief Update operation's pending DBus call, unreferencing old one if needed * * \param[in,out] op Operation to modify * \param[in] pending Pending call to set */ void services_set_op_pending(svc_action_t *op, DBusPendingCall *pending) { if (op->opaque->pending && (op->opaque->pending != pending)) { if (pending) { crm_info("Lost pending %s DBus call (%p)", op->id, op->opaque->pending); } else { crm_trace("Done with pending %s DBus call (%p)", op->id, op->opaque->pending); } dbus_pending_call_unref(op->opaque->pending); } op->opaque->pending = pending; if (pending) { crm_trace("Updated pending %s DBus call (%p)", op->id, pending); } else { crm_trace("Cleared pending %s DBus call", op->id); } } #endif void services_action_cleanup(svc_action_t * op) { if ((op == NULL) || (op->opaque == NULL)) { return; } #if HAVE_DBUS if(op->opaque->timerid != 0) { crm_trace("Removing timer for call %s to %s", op->action, op->rsc); g_source_remove(op->opaque->timerid); op->opaque->timerid = 0; } if(op->opaque->pending) { if (dbus_pending_call_get_completed(op->opaque->pending)) { // This should never be the case crm_warn("Result of %s op %s was unhandled", op->standard, op->id); } else { crm_debug("Will ignore any result of canceled %s op %s", op->standard, op->id); } dbus_pending_call_cancel(op->opaque->pending); services_set_op_pending(op, NULL); } #endif if (op->opaque->stderr_gsource) { mainloop_del_fd(op->opaque->stderr_gsource); op->opaque->stderr_gsource = NULL; } if (op->opaque->stdout_gsource) { mainloop_del_fd(op->opaque->stdout_gsource); op->opaque->stdout_gsource = NULL; } } /*! * \internal * \brief Map an actual resource action result to a standard OCF result * * \param[in] standard Agent standard (must not be "service") * \param[in] action Action that result is for * \param[in] exit_status Actual agent exit status * * \return Standard OCF result */ enum ocf_exitcode services_result2ocf(const char *standard, const char *action, int exit_status) { if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_casei)) { return services__ocf2ocf(exit_status); #if SUPPORT_SYSTEMD } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_SYSTEMD, pcmk__str_casei)) { return services__systemd2ocf(exit_status); #endif #if PCMK__ENABLE_LSB } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei)) { return services__lsb2ocf(action, exit_status); #endif } else { crm_warn("Treating result from unknown standard '%s' as OCF", ((standard == NULL)? "unspecified" : standard)); return services__ocf2ocf(exit_status); } } void services_action_free(svc_action_t * op) { unsigned int i; if (op == NULL) { return; } /* The operation should be removed from all tracking lists by this point. * If it's not, we have a bug somewhere, so bail. That may lead to a * memory leak, but it's better than a use-after-free segmentation fault. */ CRM_CHECK(g_list_find(inflight_ops, op) == NULL, return); CRM_CHECK(g_list_find(blocked_ops, op) == NULL, return); CRM_CHECK((recurring_actions == NULL) || (g_hash_table_lookup(recurring_actions, op->id) == NULL), return); services_action_cleanup(op); if (op->opaque->repeat_timer) { g_source_remove(op->opaque->repeat_timer); op->opaque->repeat_timer = 0; } free(op->id); free(op->opaque->exec); for (i = 0; i < PCMK__NELEM(op->opaque->args); i++) { free(op->opaque->args[i]); } free(op->opaque->exit_reason); #if SUPPORT_SYSTEMD free(op->opaque->job_path); #endif // SUPPORT_SYSTEMD free(op->opaque); free(op->rsc); free(op->action); free(op->standard); free(op->agent); free(op->provider); free(op->stdout_data); free(op->stderr_data); if (op->params) { g_hash_table_destroy(op->params); op->params = NULL; } free(op); } gboolean cancel_recurring_action(svc_action_t * op) { crm_info("Cancelling %s operation %s", op->standard, op->id); if (recurring_actions) { g_hash_table_remove(recurring_actions, op->id); } if (op->opaque->repeat_timer) { g_source_remove(op->opaque->repeat_timer); op->opaque->repeat_timer = 0; } return TRUE; } /*! * \brief Cancel a recurring action * * \param[in] name Name of resource that operation is for * \param[in] action Name of operation to cancel * \param[in] interval_ms Interval of operation to cancel * * \return TRUE if action was successfully cancelled, FALSE otherwise */ gboolean services_action_cancel(const char *name, const char *action, guint interval_ms) { gboolean cancelled = FALSE; char *id = pcmk__op_key(name, action, interval_ms); svc_action_t *op = NULL; /* We can only cancel a recurring action */ init_recurring_actions(); op = g_hash_table_lookup(recurring_actions, id); if (op == NULL) { goto done; } // Tell services__finalize_async_op() not to reschedule the operation op->cancel = TRUE; /* Stop tracking it as a recurring operation, and stop its repeat timer */ cancel_recurring_action(op); /* If the op has a PID, it's an in-flight child process, so kill it. * * Whether the kill succeeds or fails, the main loop will send the op to * async_action_complete() (and thus services__finalize_async_op()) when the * process goes away. */ if (op->pid != 0) { crm_info("Terminating in-flight op %s[%d] early because it was cancelled", id, op->pid); cancelled = mainloop_child_kill(op->pid); if (cancelled == FALSE) { crm_err("Termination of %s[%d] failed", id, op->pid); } goto done; } #if HAVE_DBUS // In-flight systemd ops don't have a pid if (inflight_systemd(op)) { inflight_ops = g_list_remove(inflight_ops, op); /* This will cause any result that comes in later to be discarded, so we * don't call the callback and free the operation twice. */ services_action_cleanup(op); } #endif /* The rest of this is essentially equivalent to * services__finalize_async_op(), minus the handle_blocked_ops() call. */ // Report operation as cancelled services__set_cancelled(op); if (op->opaque->callback) { op->opaque->callback(op); } blocked_ops = g_list_remove(blocked_ops, op); services_action_free(op); cancelled = TRUE; // @TODO Initiate handle_blocked_ops() asynchronously done: free(id); return cancelled; } gboolean services_action_kick(const char *name, const char *action, guint interval_ms) { svc_action_t * op = NULL; char *id = pcmk__op_key(name, action, interval_ms); init_recurring_actions(); op = g_hash_table_lookup(recurring_actions, id); free(id); if (op == NULL) { return FALSE; } if (op->pid || inflight_systemd(op)) { return TRUE; } else { if (op->opaque->repeat_timer) { g_source_remove(op->opaque->repeat_timer); op->opaque->repeat_timer = 0; } recurring_action_timer(op); return TRUE; } } /*! * \internal * \brief Add a new recurring operation, checking for duplicates * * \param[in,out] op Operation to add * * \return TRUE if duplicate found (and reschedule), FALSE otherwise */ static gboolean handle_duplicate_recurring(svc_action_t *op) { svc_action_t * dup = NULL; /* check for duplicates */ dup = g_hash_table_lookup(recurring_actions, op->id); if (dup && (dup != op)) { /* update user data */ if (op->opaque->callback) { dup->opaque->callback = op->opaque->callback; dup->cb_data = op->cb_data; op->cb_data = NULL; } /* immediately execute the next interval */ if (dup->pid != 0) { if (op->opaque->repeat_timer) { g_source_remove(op->opaque->repeat_timer); op->opaque->repeat_timer = 0; } recurring_action_timer(dup); } /* free the duplicate */ services_action_free(op); return TRUE; } return FALSE; } /*! * \internal * \brief Execute an action appropriately according to its standard * * \param[in,out] op Action to execute * * \return Standard Pacemaker return code * \retval EBUSY Recurring operation could not be initiated * \retval pcmk_rc_error Synchronous action failed * \retval pcmk_rc_ok Synchronous action succeeded, or asynchronous action * should not be freed (because it's pending or because * it failed to execute and was already freed) * * \note If the return value for an asynchronous action is not pcmk_rc_ok, the * caller is responsible for freeing the action. */ static int execute_action(svc_action_t *op) { #if SUPPORT_SYSTEMD if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_SYSTEMD, pcmk__str_casei)) { return services__execute_systemd(op); } #endif return services__execute_file(op); } void services_add_inflight_op(svc_action_t * op) { if (op == NULL) { return; } pcmk__assert(op->synchronous == FALSE); /* keep track of ops that are in-flight to avoid collisions in the same namespace */ if (op->rsc) { inflight_ops = g_list_append(inflight_ops, op); } } /*! * \internal * \brief Stop tracking an operation that completed * * \param[in] op Operation to stop tracking */ void services_untrack_op(const svc_action_t *op) { /* Op is no longer in-flight or blocked */ inflight_ops = g_list_remove(inflight_ops, op); blocked_ops = g_list_remove(blocked_ops, op); /* Op is no longer blocking other ops, so check if any need to run */ handle_blocked_ops(); } gboolean services_action_async_fork_notify(svc_action_t * op, void (*action_callback) (svc_action_t *), void (*action_fork_callback) (svc_action_t *)) { CRM_CHECK(op != NULL, return TRUE); op->synchronous = false; if (action_callback != NULL) { op->opaque->callback = action_callback; } if (action_fork_callback != NULL) { op->opaque->fork_callback = action_fork_callback; } if (op->interval_ms > 0) { init_recurring_actions(); if (handle_duplicate_recurring(op)) { /* entry rescheduled, dup freed */ /* exit early */ return TRUE; } g_hash_table_replace(recurring_actions, op->id, op); } if (!pcmk__is_set(op->flags, SVC_ACTION_NON_BLOCKED) && (op->rsc != NULL) && is_op_blocked(op->rsc)) { blocked_ops = g_list_append(blocked_ops, op); return TRUE; } return execute_action(op) == pcmk_rc_ok; } gboolean services_action_async(svc_action_t * op, void (*action_callback) (svc_action_t *)) { return services_action_async_fork_notify(op, action_callback, NULL); } static gboolean processing_blocked_ops = FALSE; gboolean is_op_blocked(const char *rsc) { GList *gIter = NULL; svc_action_t *op = NULL; for (gIter = inflight_ops; gIter != NULL; gIter = gIter->next) { op = gIter->data; if (pcmk__str_eq(op->rsc, rsc, pcmk__str_none)) { return TRUE; } } return FALSE; } static void handle_blocked_ops(void) { GList *executed_ops = NULL; GList *gIter = NULL; svc_action_t *op = NULL; if (processing_blocked_ops) { /* avoid nested calling of this function */ return; } processing_blocked_ops = TRUE; /* n^2 operation here, but blocked ops are incredibly rare. this list * will be empty 99% of the time. */ for (gIter = blocked_ops; gIter != NULL; gIter = gIter->next) { op = gIter->data; if (is_op_blocked(op->rsc)) { continue; } executed_ops = g_list_append(executed_ops, op); if (execute_action(op) != pcmk_rc_ok) { /* this can cause this function to be called recursively * which is why we have processing_blocked_ops static variable */ services__finalize_async_op(op); } } for (gIter = executed_ops; gIter != NULL; gIter = gIter->next) { op = gIter->data; blocked_ops = g_list_remove(blocked_ops, op); } g_list_free(executed_ops); processing_blocked_ops = FALSE; } /*! * \internal * \brief Execute a meta-data action appropriately to standard * * \param[in,out] op Meta-data action to execute * * \return Standard Pacemaker return code */ static int execute_metadata_action(svc_action_t *op) { const char *class = op->standard; if (op->agent == NULL) { crm_info("Meta-data requested without specifying agent"); services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR_FATAL, "Agent not specified"); return EINVAL; } if (class == NULL) { crm_info("Meta-data requested for agent %s without specifying class", op->agent); services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR_FATAL, "Agent standard not specified"); return EINVAL; } #if PCMK__ENABLE_SERVICE if (!strcmp(class, PCMK_RESOURCE_CLASS_SERVICE)) { class = resources_find_service_class(op->agent); } if (class == NULL) { crm_info("Meta-data requested for %s, but could not determine class", op->agent); services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR_HARD, "Agent standard could not be determined"); return EINVAL; } #endif #if PCMK__ENABLE_LSB if (pcmk__str_eq(class, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei)) { return pcmk_legacy2rc(services__get_lsb_metadata(op->agent, &op->stdout_data)); } #endif return execute_action(op); } gboolean services_action_sync(svc_action_t * op) { gboolean rc = TRUE; if (op == NULL) { crm_trace("No operation to execute"); return FALSE; } op->synchronous = true; if (pcmk__str_eq(op->action, PCMK_ACTION_META_DATA, pcmk__str_casei)) { /* Synchronous meta-data operations are handled specially. Since most * resource classes don't provide any meta-data, it has to be * synthesized from available information about the agent. * * services_action_async() doesn't treat meta-data actions specially, so * it will result in an error for classes that don't support the action. */ rc = (execute_metadata_action(op) == pcmk_rc_ok); } else { rc = (execute_action(op) == pcmk_rc_ok); } crm_trace(" > " PCMK__OP_FMT ": %s = %d", op->rsc, op->action, op->interval_ms, op->opaque->exec, op->rc); if (op->stdout_data) { crm_trace(" > stdout: %s", op->stdout_data); } if (op->stderr_data) { crm_trace(" > stderr: %s", op->stderr_data); } return rc; } GList * get_directory_list(const char *root, gboolean files, gboolean executable) { return services_os_get_directory_list(root, files, executable); } GList * resources_list_standards(void) { GList *standards = NULL; standards = g_list_append(standards, strdup(PCMK_RESOURCE_CLASS_OCF)); #if PCMK__ENABLE_SERVICE standards = g_list_append(standards, strdup(PCMK_RESOURCE_CLASS_SERVICE)); #endif #if PCMK__ENABLE_LSB standards = g_list_append(standards, strdup(PCMK_RESOURCE_CLASS_LSB)); #endif #if SUPPORT_SYSTEMD { GList *agents = systemd_unit_listall(); if (agents != NULL) { standards = g_list_append(standards, strdup(PCMK_RESOURCE_CLASS_SYSTEMD)); g_list_free_full(agents, free); } } #endif return standards; } GList * resources_list_providers(const char *standard) { if (pcmk__is_set(pcmk_get_ra_caps(standard), pcmk_ra_cap_provider)) { return resources_os_list_ocf_providers(); } return NULL; } GList * resources_list_agents(const char *standard, const char *provider) { if ((standard == NULL) #if PCMK__ENABLE_SERVICE || (strcasecmp(standard, PCMK_RESOURCE_CLASS_SERVICE) == 0) #endif ) { GList *tmp1; GList *tmp2; GList *result = NULL; if (standard == NULL) { tmp1 = result; tmp2 = resources_os_list_ocf_agents(NULL); if (tmp2) { result = g_list_concat(tmp1, tmp2); } } #if PCMK__ENABLE_LSB result = g_list_concat(result, services__list_lsb_agents()); #endif #if SUPPORT_SYSTEMD tmp1 = result; tmp2 = systemd_unit_listall(); if (tmp2) { result = g_list_concat(tmp1, tmp2); } #endif return result; } else if (strcasecmp(standard, PCMK_RESOURCE_CLASS_OCF) == 0) { return resources_os_list_ocf_agents(provider); #if PCMK__ENABLE_LSB } else if (strcasecmp(standard, PCMK_RESOURCE_CLASS_LSB) == 0) { return services__list_lsb_agents(); #endif #if SUPPORT_SYSTEMD } else if (strcasecmp(standard, PCMK_RESOURCE_CLASS_SYSTEMD) == 0) { return systemd_unit_listall(); #endif } return NULL; } gboolean resources_agent_exists(const char *standard, const char *provider, const char *agent) { GList *standards = NULL; GList *providers = NULL; GList *iter = NULL; gboolean rc = FALSE; gboolean has_providers = FALSE; standards = resources_list_standards(); for (iter = standards; iter != NULL; iter = iter->next) { if (pcmk__str_eq(iter->data, standard, pcmk__str_none)) { rc = TRUE; break; } } if (rc == FALSE) { goto done; } rc = FALSE; has_providers = pcmk__is_set(pcmk_get_ra_caps(standard), pcmk_ra_cap_provider); if (has_providers == TRUE && provider != NULL) { providers = resources_list_providers(standard); for (iter = providers; iter != NULL; iter = iter->next) { if (pcmk__str_eq(iter->data, provider, pcmk__str_none)) { rc = TRUE; break; } } } else if (has_providers == FALSE && provider == NULL) { rc = TRUE; } if (rc == FALSE) { goto done; } #if PCMK__ENABLE_SERVICE if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_SERVICE, pcmk__str_casei)) { #if PCMK__ENABLE_LSB if (services__lsb_agent_exists(agent)) { rc = TRUE; goto done; } #endif #if SUPPORT_SYSTEMD if (systemd_unit_exists(agent)) { rc = TRUE; goto done; } #endif rc = FALSE; goto done; } #endif if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_casei)) { rc = services__ocf_agent_exists(provider, agent); #if PCMK__ENABLE_LSB } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei)) { rc = services__lsb_agent_exists(agent); #endif #if SUPPORT_SYSTEMD } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_SYSTEMD, pcmk__str_casei)) { rc = systemd_unit_exists(agent); #endif } else { rc = FALSE; } done: g_list_free(standards); g_list_free(providers); return rc; } /*! * \internal * \brief Set the result of an action * * \param[out] action Where to set action result * \param[in] agent_status Exit status to set * \param[in] exec_status Execution status to set * \param[in] reason Human-friendly description of event to set */ void services__set_result(svc_action_t *action, int agent_status, enum pcmk_exec_status exec_status, const char *reason) { if (action == NULL) { return; } action->rc = agent_status; action->status = exec_status; if (!pcmk__str_eq(action->opaque->exit_reason, reason, pcmk__str_none)) { free(action->opaque->exit_reason); action->opaque->exit_reason = (reason == NULL)? NULL : strdup(reason); } } /*! * \internal * \brief Set a \c pcmk__action_result_t based on a \c svc_action_t * * \param[in] action Service action whose result to copy * \param[in,out] result Action result object to set */ void services__copy_result(const svc_action_t *action, pcmk__action_result_t *result) { pcmk__set_result(result, action->rc, action->status, action->opaque->exit_reason); } /*! * \internal * \brief Set the result of an action, with a formatted exit reason * * \param[out] action Where to set action result * \param[in] agent_status Exit status to set * \param[in] exec_status Execution status to set * \param[in] format printf-style format for a human-friendly * description of reason for result * \param[in] ... arguments for \p format */ void services__format_result(svc_action_t *action, int agent_status, enum pcmk_exec_status exec_status, const char *format, ...) { va_list ap; int len = 0; char *reason = NULL; if (action == NULL) { return; } action->rc = agent_status; action->status = exec_status; if (format != NULL) { va_start(ap, format); len = vasprintf(&reason, format, ap); pcmk__assert(len > 0); va_end(ap); } free(action->opaque->exit_reason); action->opaque->exit_reason = reason; } /*! * \internal * \brief Set the result of an action to cancelled * * \param[out] action Where to set action result * * \note This sets execution status but leaves the exit status unchanged */ void services__set_cancelled(svc_action_t *action) { if (action != NULL) { action->status = PCMK_EXEC_CANCELLED; free(action->opaque->exit_reason); action->opaque->exit_reason = NULL; } } /*! * \internal * \brief Get a readable description of what an action is for * * \param[in] action Action to check * * \return Readable name for the kind of \p action */ const char * services__action_kind(const svc_action_t *action) { if ((action == NULL) || (action->standard == NULL)) { return "Process"; } else if (pcmk__str_eq(action->standard, PCMK_RESOURCE_CLASS_STONITH, pcmk__str_none)) { return "Fence agent"; } else if (pcmk__str_eq(action->standard, PCMK_RESOURCE_CLASS_ALERT, pcmk__str_none)) { return "Alert agent"; } else { return "Resource agent"; } } /*! * \internal * \brief Get the exit reason of an action * * \param[in] action Action to check * * \return Action's exit reason (or NULL if none) */ const char * services__exit_reason(const svc_action_t *action) { return action->opaque->exit_reason; } /*! * \internal * \brief Steal stdout from an action * * \param[in,out] action Action whose stdout is desired * * \return Action's stdout (which may be NULL) * \note Upon return, \p action will no longer track the output, so it is the * caller's responsibility to free the return value. */ char * services__grab_stdout(svc_action_t *action) { char *output = action->stdout_data; action->stdout_data = NULL; return output; } /*! * \internal * \brief Steal stderr from an action * * \param[in,out] action Action whose stderr is desired * * \return Action's stderr (which may be NULL) * \note Upon return, \p action will no longer track the output, so it is the * caller's responsibility to free the return value. */ char * services__grab_stderr(svc_action_t *action) { char *output = action->stderr_data; action->stderr_data = NULL; return output; } diff --git a/mk/tap.mk b/mk/tap.mk index e06f9a8624..bac6d0e7e0 100644 --- a/mk/tap.mk +++ b/mk/tap.mk @@ -1,39 +1,39 @@ # -# Copyright 2021-2024 the Pacemaker project contributors +# Copyright 2021-2025 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # AM_TESTS_ENVIRONMENT = G_DEBUG=gc-friendly AM_TESTS_ENVIRONMENT += MALLOC_CHECK_=2 AM_TESTS_ENVIRONMENT += MALLOC_PERTURB_=$$(($${RANDOM:-256} % 256)) AM_TESTS_ENVIRONMENT += PCMK_CTS_CLI_DIR=$(top_srcdir)/cts/cli AM_TESTS_ENVIRONMENT += PCMK_schema_directory=$(top_builddir)/xml LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tests/tap-driver.sh LOG_COMPILER = $(top_srcdir)/tests/tap-test CLEANFILES = *.log *.trs WRAPPED = abort \ calloc \ endgrent \ fopen \ getenv \ getpid \ getgrent \ - getpwnam_r \ + getpwnam \ readlink \ realloc \ setenv \ setgrent \ strdup \ unsetenv if WRAPPABLE_FOPEN64 WRAPPED += fopen64 endif LDFLAGS_WRAP = $(foreach fn,$(WRAPPED),-Wl,--wrap=$(fn))