diff --git a/daemons/controld/controld_throttle.c b/daemons/controld/controld_throttle.c index 27d75e4d13..d2ad9355e9 100644 --- a/daemons/controld/controld_throttle.c +++ b/daemons/controld/controld_throttle.c @@ -1,586 +1,407 @@ /* * Copyright 2013-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include /* These values don't need to be bits, but these particular values must be kept * for backward compatibility during rolling upgrades. */ enum throttle_state_e { throttle_none = 0x0000, throttle_low = 0x0001, throttle_med = 0x0010, throttle_high = 0x0100, throttle_extreme = 0x1000, }; struct throttle_record_s { int max; enum throttle_state_e mode; char *node; }; static int throttle_job_max = 0; static float throttle_load_target = 0.0; #define THROTTLE_FACTOR_LOW 1.2 #define THROTTLE_FACTOR_MEDIUM 1.6 #define THROTTLE_FACTOR_HIGH 2.0 static GHashTable *throttle_records = NULL; static mainloop_timer_t *throttle_timer = NULL; static const char * load2str(enum throttle_state_e mode) { switch (mode) { case throttle_extreme: return "extreme"; case throttle_high: return "high"; case throttle_med: return "medium"; case throttle_low: return "low"; case throttle_none: return "negligible"; default: return "undetermined"; } } -#if HAVE_LINUX_PROCFS -/*! - * \internal - * \brief Return name of /proc file containing the CIB daemon's load statistics - * - * \return Newly allocated memory with file name on success, NULL otherwise - * - * \note It is the caller's responsibility to free the return value. - * This will return NULL if the daemon is being run via valgrind. - * This should be called only on Linux systems. - */ -static char * -find_cib_loadfile(void) -{ - pid_t pid = pcmk__procfs_pid_of(PCMK__SERVER_BASED); - - return pid? crm_strdup_printf("/proc/%lld/stat", (long long) pid) : NULL; -} - -static bool -throttle_cib_load(float *load) -{ -/* - /proc/[pid]/stat - Status information about the process. This is used by ps(1). It is defined in /usr/src/linux/fs/proc/array.c. - - The fields, in order, with their proper scanf(3) format specifiers, are: - - pid %d (1) The process ID. - - comm %s (2) The filename of the executable, in parentheses. This is visible whether or not the executable is swapped out. - - state %c (3) One character from the string "RSDZTW" where R is running, S is sleeping in an interruptible wait, D is waiting in uninterruptible disk sleep, Z is zombie, T is traced or stopped (on a signal), and W is paging. - - ppid %d (4) The PID of the parent. - - pgrp %d (5) The process group ID of the process. - - session %d (6) The session ID of the process. - - tty_nr %d (7) The controlling terminal of the process. (The minor device number is contained in the combination of bits 31 to 20 and 7 to 0; the major device number is in bits 15 to 8.) - - tpgid %d (8) The ID of the foreground process group of the controlling terminal of the process. - - flags %u (%lu before Linux 2.6.22) - (9) The kernel flags word of the process. For bit meanings, see the PF_* defines in the Linux kernel source file include/linux/sched.h. Details depend on the kernel version. - - minflt %lu (10) The number of minor faults the process has made which have not required loading a memory page from disk. - - cminflt %lu (11) The number of minor faults that the process's waited-for children have made. - - majflt %lu (12) The number of major faults the process has made which have required loading a memory page from disk. - - cmajflt %lu (13) The number of major faults that the process's waited-for children have made. - - utime %lu (14) Amount of time that this process has been scheduled in user mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). This includes guest time, guest_time (time spent running a virtual CPU, see below), so that applications that are not aware of the guest time field do not lose that time from their calculations. - - stime %lu (15) Amount of time that this process has been scheduled in kernel mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). - */ - - static char *loadfile = NULL; - static time_t last_call = 0; - static long ticks_per_s = 0; - static unsigned long last_utime, last_stime; - - char buffer[64*1024]; - FILE *stream = NULL; - time_t now = time(NULL); - - if(load == NULL) { - return FALSE; - } else { - *load = 0.0; - } - - if(loadfile == NULL) { - last_call = 0; - last_utime = 0; - last_stime = 0; - loadfile = find_cib_loadfile(); - if (loadfile == NULL) { - crm_warn("Couldn't find CIB load file"); - return FALSE; - } - ticks_per_s = sysconf(_SC_CLK_TCK); - crm_trace("Found %s", loadfile); - } - - stream = fopen(loadfile, "r"); - if(stream == NULL) { - int rc = errno; - - crm_warn("Couldn't read %s: %s (%d)", loadfile, pcmk_rc_str(rc), rc); - free(loadfile); loadfile = NULL; - return FALSE; - } - - if(fgets(buffer, sizeof(buffer), stream)) { - char *comm = pcmk__assert_alloc(1, 256); - char state = 0; - int rc = 0, pid = 0, ppid = 0, pgrp = 0, session = 0, tty_nr = 0, tpgid = 0; - unsigned long flags = 0, minflt = 0, cminflt = 0, majflt = 0, cmajflt = 0, utime = 0, stime = 0; - - rc = sscanf(buffer, "%d %[^ ] %c %d %d %d %d %d %lu %lu %lu %lu %lu %lu %lu", - &pid, comm, &state, - &ppid, &pgrp, &session, &tty_nr, &tpgid, - &flags, &minflt, &cminflt, &majflt, &cmajflt, &utime, &stime); - free(comm); - - if(rc != 15) { - crm_err("Only %d of 15 fields found in %s", rc, loadfile); - fclose(stream); - return FALSE; - - } else if(last_call > 0 - && last_call < now - && last_utime <= utime - && last_stime <= stime) { - - time_t elapsed = now - last_call; - unsigned long delta_utime = utime - last_utime; - unsigned long delta_stime = stime - last_stime; - - *load = (delta_utime + delta_stime); /* Cast to a float before division */ - *load /= ticks_per_s; - *load /= elapsed; - crm_debug("cib load: %f (%lu ticks in %lds)", *load, delta_utime + delta_stime, (long)elapsed); - - } else { - crm_debug("Init %lu + %lu ticks at %ld (%lu tps)", utime, stime, (long)now, ticks_per_s); - } - - last_call = now; - last_utime = utime; - last_stime = stime; - - fclose(stream); - return TRUE; - } - - fclose(stream); - return FALSE; -} - -static bool -throttle_load_avg(float *load) -{ - char buffer[256]; - FILE *stream = NULL; - const char *loadfile = "/proc/loadavg"; - - if(load == NULL) { - return FALSE; - } - - stream = fopen(loadfile, "r"); - if(stream == NULL) { - int rc = errno; - crm_warn("Couldn't read %s: %s (%d)", loadfile, pcmk_rc_str(rc), rc); - return FALSE; - } - - if(fgets(buffer, sizeof(buffer), stream)) { - char *nl = strstr(buffer, "\n"); - - /* Grab the 1-minute average, ignore the rest */ - *load = strtof(buffer, NULL); - if(nl) { nl[0] = 0; } - - fclose(stream); - return TRUE; - } - - fclose(stream); - return FALSE; -} - /*! * \internal * \brief Check a load value against throttling thresholds * * \param[in] load Load value to check * \param[in] desc Description of metric (for logging) * \param[in] thresholds Low/medium/high/extreme thresholds * * \return Throttle mode corresponding to load value */ static enum throttle_state_e throttle_check_thresholds(float load, const char *desc, const float thresholds[4]) { if (load > thresholds[3]) { crm_notice("Extreme %s detected: %f", desc, load); return throttle_extreme; } else if (load > thresholds[2]) { crm_notice("High %s detected: %f", desc, load); return throttle_high; } else if (load > thresholds[1]) { crm_info("Moderate %s detected: %f", desc, load); return throttle_med; } else if (load > thresholds[0]) { crm_debug("Noticeable %s detected: %f", desc, load); return throttle_low; } crm_trace("Negligible %s detected: %f", desc, load); return throttle_none; } static enum throttle_state_e throttle_handle_load(float load, const char *desc, int cores) { float normalize; float thresholds[4]; if (cores == 1) { /* On a single core machine, a load of 1.0 is already too high */ normalize = 0.6; } else { /* Normalize the load to be per-core */ normalize = cores; } thresholds[0] = throttle_load_target * normalize * THROTTLE_FACTOR_LOW; thresholds[1] = throttle_load_target * normalize * THROTTLE_FACTOR_MEDIUM; thresholds[2] = throttle_load_target * normalize * THROTTLE_FACTOR_HIGH; thresholds[3] = load + 1.0; /* never extreme */ return throttle_check_thresholds(load, desc, thresholds); } -#endif // HAVE_LINUX_PROCFS static enum throttle_state_e throttle_mode(void) { enum throttle_state_e mode = throttle_none; -#if HAVE_LINUX_PROCFS - unsigned int cores; + unsigned int cores = pcmk__procfs_num_cores(); float load; float thresholds[4]; - cores = pcmk__procfs_num_cores(); - if(throttle_cib_load(&load)) { + if (pcmk__throttle_cib_load(PCMK__SERVER_BASED, &load)) { float cib_max_cpu = 0.95; - /* The CIB is a single-threaded task and thus cannot consume - * more than 100% of a CPU (and 1/cores of the overall system - * load). + /* The CIB is a single-threaded task and thus cannot consume more + * than 100% of a CPU (and 1/cores of the overall system load). * - * On a many-cored system, the CIB might therefore be maxed out - * (causing operations to fail or appear to fail) even though - * the overall system load is still reasonable. + * On a many-cored system, the CIB might therefore be maxed out (causing + * operations to fail or appear to fail) even though the overall system + * load is still reasonable. * - * Therefore, the 'normal' thresholds can not apply here, and we - * need a special case. + * Therefore, the 'normal' thresholds can not apply here, and we need a + * special case. */ - if(cores == 1) { + if (cores == 1) { cib_max_cpu = 0.4; } - if(throttle_load_target > 0.0 && throttle_load_target < cib_max_cpu) { + if ((throttle_load_target > 0.0) && (throttle_load_target < cib_max_cpu)) { cib_max_cpu = throttle_load_target; } thresholds[0] = cib_max_cpu * 0.8; thresholds[1] = cib_max_cpu * 0.9; thresholds[2] = cib_max_cpu; /* Can only happen on machines with a low number of cores */ thresholds[3] = cib_max_cpu * 1.5; mode = throttle_check_thresholds(load, "CIB load", thresholds); } - if(throttle_load_target <= 0) { - /* If we ever make this a valid value, the cluster will at least behave as expected */ + if (throttle_load_target <= 0) { + /* If we ever make this a valid value, the cluster will at least behave + * as expected + */ return mode; } - if(throttle_load_avg(&load)) { + if (pcmk__throttle_load_avg(&load)) { enum throttle_state_e cpu_load; cpu_load = throttle_handle_load(load, "CPU load", cores); if (cpu_load > mode) { mode = cpu_load; } crm_debug("Current load is %f across %u core(s)", load, cores); } -#endif // HAVE_LINUX_PROCFS + return mode; } static void throttle_send_command(enum throttle_state_e mode) { xmlNode *xml = NULL; static enum throttle_state_e last = -1; if(mode != last) { crm_info("New throttle mode: %s load (was %s)", load2str(mode), load2str(last)); last = mode; xml = pcmk__new_request(pcmk_ipc_controld, CRM_SYSTEM_CRMD, NULL, CRM_SYSTEM_CRMD, CRM_OP_THROTTLE, NULL); crm_xml_add_int(xml, PCMK__XA_CRM_LIMIT_MODE, mode); crm_xml_add_int(xml, PCMK__XA_CRM_LIMIT_MAX, throttle_job_max); pcmk__cluster_send_message(NULL, pcmk_ipc_controld, xml); pcmk__xml_free(xml); } } static gboolean throttle_timer_cb(gpointer data) { throttle_send_command(throttle_mode()); return TRUE; } static void throttle_record_free(gpointer p) { struct throttle_record_s *r = p; free(r->node); free(r); } static void throttle_set_load_target(float target) { throttle_load_target = target; } /*! * \internal * \brief Update the maximum number of simultaneous jobs * * \param[in] preference Cluster-wide \c PCMK_OPT_NODE_ACTION_LIMIT from the * CIB */ static void throttle_update_job_max(const char *preference) { long long max = 0LL; // Per-node override const char *env_limit = pcmk__env_option(PCMK__ENV_NODE_ACTION_LIMIT); if (env_limit != NULL) { int rc = pcmk__scan_ll(env_limit, &max, 0LL); if (rc != pcmk_rc_ok) { crm_warn("Ignoring local option PCMK_" PCMK__ENV_NODE_ACTION_LIMIT " because '%s' is not a valid value: %s", env_limit, pcmk_rc_str(rc)); env_limit = NULL; } } if (env_limit == NULL) { // Option validator should prevent invalid values CRM_LOG_ASSERT(pcmk__scan_ll(preference, &max, 0LL) == pcmk_rc_ok); } if (max > 0) { throttle_job_max = (max >= INT_MAX)? INT_MAX : (int) max; } else { // Default is based on the number of cores detected throttle_job_max = 2 * pcmk__procfs_num_cores(); } } void throttle_init(void) { if(throttle_records == NULL) { throttle_records = pcmk__strkey_table(NULL, throttle_record_free); throttle_timer = mainloop_timer_add("throttle", 30 * 1000, TRUE, throttle_timer_cb, NULL); } throttle_update_job_max(NULL); mainloop_timer_start(throttle_timer); } /*! * \internal * \brief Configure throttle options based on the CIB * * \param[in,out] options Name/value pairs for configured options */ void controld_configure_throttle(GHashTable *options) { const char *value = g_hash_table_lookup(options, PCMK_OPT_LOAD_THRESHOLD); if (value != NULL) { throttle_set_load_target(strtof(value, NULL) / 100.0); } value = g_hash_table_lookup(options, PCMK_OPT_NODE_ACTION_LIMIT); throttle_update_job_max(value); } void throttle_fini(void) { if (throttle_timer != NULL) { mainloop_timer_del(throttle_timer); throttle_timer = NULL; } if (throttle_records != NULL) { g_hash_table_destroy(throttle_records); throttle_records = NULL; } } int throttle_get_total_job_limit(int l) { /* Cluster-wide limit */ GHashTableIter iter; int limit = l; int peers = pcmk__cluster_num_active_nodes(); struct throttle_record_s *r = NULL; g_hash_table_iter_init(&iter, throttle_records); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &r)) { switch(r->mode) { case throttle_extreme: if(limit == 0 || limit > peers/4) { limit = QB_MAX(1, peers/4); } break; case throttle_high: if(limit == 0 || limit > peers/2) { limit = QB_MAX(1, peers/2); } break; default: break; } } if(limit == l) { } else if(l == 0) { crm_trace("Using " PCMK_OPT_BATCH_LIMIT "=%d", limit); } else { crm_trace("Using " PCMK_OPT_BATCH_LIMIT "=%d instead of %d", limit, l); } return limit; } int throttle_get_job_limit(const char *node) { int jobs = 1; struct throttle_record_s *r = NULL; r = g_hash_table_lookup(throttle_records, node); if(r == NULL) { r = pcmk__assert_alloc(1, sizeof(struct throttle_record_s)); r->node = pcmk__str_copy(node); r->mode = throttle_low; r->max = throttle_job_max; crm_trace("Defaulting to local values for unknown node %s", node); g_hash_table_insert(throttle_records, r->node, r); } switch(r->mode) { case throttle_extreme: case throttle_high: jobs = 1; /* At least one job must always be allowed */ break; case throttle_med: jobs = QB_MAX(1, r->max / 4); break; case throttle_low: jobs = QB_MAX(1, r->max / 2); break; case throttle_none: jobs = QB_MAX(1, r->max); break; default: crm_err("Unknown throttle mode %.4x on %s", r->mode, node); break; } return jobs; } void throttle_update(xmlNode *xml) { int max = 0; int mode = 0; struct throttle_record_s *r = NULL; const char *from = crm_element_value(xml, PCMK__XA_SRC); crm_element_value_int(xml, PCMK__XA_CRM_LIMIT_MODE, &mode); crm_element_value_int(xml, PCMK__XA_CRM_LIMIT_MAX, &max); r = g_hash_table_lookup(throttle_records, from); if(r == NULL) { r = pcmk__assert_alloc(1, sizeof(struct throttle_record_s)); r->node = pcmk__str_copy(from); g_hash_table_insert(throttle_records, r->node, r); } r->max = max; r->mode = (enum throttle_state_e) mode; crm_debug("Node %s has %s load and supports at most %d jobs; new job limit %d", from, load2str((enum throttle_state_e) mode), max, throttle_get_job_limit(from)); } diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h index a91a4bec50..82feb6476a 100644 --- a/include/crm/common/internal.h +++ b/include/crm/common/internal.h @@ -1,359 +1,363 @@ /* * Copyright 2015-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_INTERNAL__H #define PCMK__CRM_COMMON_INTERNAL__H #include // pid_t, getpid() #include // bool #include // uint8_t, uint64_t #include // guint, GList, GHashTable #include // xmlNode #include // do_crm_log_unlikely(), etc. #include // mainloop_io_t, struct ipc_client_callbacks #include // crm_strdup_printf() #include #include #include #include #include #include #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif /* This says whether the current application is a Pacemaker daemon or not, * and is used to change default logging settings such as whether to log to * stderr, etc., as well as a few other details such as whether blackbox signal * handling is enabled. * * It is set when logging is initialized, and does not need to be set directly. */ extern bool pcmk__is_daemon; // Number of elements in a statically defined array #define PCMK__NELEM(a) ((int) (sizeof(a)/sizeof(a[0])) ) #if PCMK__ENABLE_CIBSECRETS /* internal CIB utilities (from cib_secrets.c) */ int pcmk__substitute_secrets(const char *rsc_id, GHashTable *params); #endif /* internal main loop utilities (from mainloop.c) */ int pcmk__add_mainloop_ipc(crm_ipc_t *ipc, int priority, void *userdata, const struct ipc_client_callbacks *callbacks, mainloop_io_t **source); guint pcmk__mainloop_timer_get_period(const mainloop_timer_t *timer); /* internal node-related XML utilities (from nodes.c) */ /*! * \internal * \brief Add local node name and ID to an XML node * * \param[in,out] request XML node to modify * \param[in] node The local node's name * \param[in] nodeid The local node's ID (can be 0) */ void pcmk__xe_add_node(xmlNode *xml, const char *node, int nodeid); /* internal name/value utilities (from nvpair.c) */ int pcmk__scan_nvpair(const char *input, char **name, char **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 %#.8llx (%s) for %s set by %s:%d", ((flag_type == NULL)? "Group of" : flag_type), (unsigned long long) flags, ((flags_str == NULL)? "flags" : flags_str), ((target == NULL)? "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 %#.8llx (%s) for %s cleared by %s:%d", ((flag_type == NULL)? "Group of" : flag_type), (unsigned long long) flags, ((flags_str == NULL)? "flags" : flags_str), ((target == NULL)? "target" : target), function, line); } return result; } /*! * \internal * \brief Get readable string for whether specified flags are set * * \param[in] flag_group Group of flags to check * \param[in] flags Which flags in \p flag_group should be checked * * \return "true" if all \p flags are set in \p flag_group, otherwise "false" */ static inline const char * pcmk__flag_text(uint64_t flag_group, uint64_t flags) { return pcmk__btoa(pcmk_all_flags_set(flag_group, flags)); } // miscellaneous utilities (from utils.c) void pcmk__daemonize(const char *name, const char *pidfile); void pcmk__panic(const char *reason); pid_t pcmk__locate_sbd(void); void pcmk__sleep_ms(unsigned int ms); guint pcmk__create_timer(guint interval_ms, GSourceFunc fn, gpointer data); guint pcmk__timeout_ms2s(guint timeout_ms); extern int pcmk__score_red; extern int pcmk__score_green; extern int pcmk__score_yellow; /*! * \internal * \brief Allocate new zero-initialized memory, asserting on failure * * \param[in] file File where \p function is located * \param[in] function Calling function * \param[in] line Line within \p file * \param[in] nmemb Number of elements to allocate memory for * \param[in] size Size of each element * * \return Newly allocated memory of of size nmemb * size (guaranteed * not to be \c NULL) * * \note The caller is responsible for freeing the return value using \c free(). */ static inline void * pcmk__assert_alloc_as(const char *file, const char *function, uint32_t line, size_t nmemb, size_t size) { void *ptr = calloc(nmemb, size); if (ptr == NULL) { crm_abort(file, function, line, "Out of memory", FALSE, TRUE); crm_exit(CRM_EX_OSERR); } return ptr; } /*! * \internal * \brief Allocate new zero-initialized memory, asserting on failure * * \param[in] nmemb Number of elements to allocate memory for * \param[in] size Size of each element * * \return Newly allocated memory of of size nmemb * size (guaranteed * not to be \c NULL) * * \note The caller is responsible for freeing the return value using \c free(). */ #define pcmk__assert_alloc(nmemb, size) \ pcmk__assert_alloc_as(__FILE__, __func__, __LINE__, nmemb, size) /*! * \internal * \brief Resize a dynamically allocated memory block * * \param[in] ptr Memory block to resize (or NULL to allocate new memory) * \param[in] size New size of memory block in bytes (must be > 0) * * \return Pointer to resized memory block * * \note This asserts on error, so the result is guaranteed to be non-NULL * (which is the main advantage of this over directly using realloc()). */ static inline void * pcmk__realloc(void *ptr, size_t size) { void *new_ptr; // realloc(p, 0) can replace free(p) but this wrapper can't pcmk__assert(size > 0); new_ptr = realloc(ptr, size); if (new_ptr == NULL) { free(ptr); abort(); } return new_ptr; } static inline char * pcmk__getpid_s(void) { return crm_strdup_printf("%lu", (unsigned long) getpid()); } // More efficient than g_list_length(list) == 1 static inline bool pcmk__list_of_1(GList *list) { return list && (list->next == NULL); } // More efficient than g_list_length(list) > 1 static inline bool pcmk__list_of_multiple(GList *list) { return list && (list->next != NULL); } /* convenience functions for failure-related node attributes */ #define PCMK__FAIL_COUNT_PREFIX "fail-count" #define PCMK__LAST_FAILURE_PREFIX "last-failure" /*! * \internal * \brief Generate a failure-related node attribute name for a resource * * \param[in] prefix Start of attribute name * \param[in] rsc_id Resource name * \param[in] op Operation name * \param[in] interval_ms Operation interval * * \return Newly allocated string with attribute name * * \note Failure attributes are named like PREFIX-RSC#OP_INTERVAL (for example, * "fail-count-myrsc#monitor_30000"). The '#' is used because it is not * a valid character in a resource ID, to reliably distinguish where the * operation name begins. The '_' is used simply to be more comparable to * action labels like "myrsc_monitor_30000". */ static inline char * pcmk__fail_attr_name(const char *prefix, const char *rsc_id, const char *op, guint interval_ms) { CRM_CHECK(prefix && rsc_id && op, return NULL); return crm_strdup_printf("%s-%s#%s_%u", prefix, rsc_id, op, interval_ms); } static inline char * pcmk__failcount_name(const char *rsc_id, const char *op, guint interval_ms) { return pcmk__fail_attr_name(PCMK__FAIL_COUNT_PREFIX, rsc_id, op, interval_ms); } static inline char * pcmk__lastfailure_name(const char *rsc_id, const char *op, guint interval_ms) { return pcmk__fail_attr_name(PCMK__LAST_FAILURE_PREFIX, rsc_id, op, interval_ms); } // internal resource agent functions (from agents.c) int pcmk__effective_rc(int rc); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_INTERNAL__H diff --git a/lib/common/io.c b/lib/common/io.c index 102d7f1bf0..972b927888 100644 --- a/lib/common/io.c +++ b/lib/common/io.c @@ -1,639 +1,635 @@ /* * Copyright 2004-2024 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 /*! * \internal * \brief Create a directory, including any parent directories needed * * \param[in] path_c Pathname of the directory to create * \param[in] mode Permissions to be used (with current umask) when creating * * \return Standard Pacemaker return code */ int pcmk__build_path(const char *path_c, mode_t mode) { int offset = 1, len = 0; int rc = pcmk_rc_ok; char *path = strdup(path_c); // cppcheck seems not to understand the abort logic in CRM_CHECK // cppcheck-suppress memleak CRM_CHECK(path != NULL, return -ENOMEM); for (len = strlen(path); offset < len; offset++) { if (path[offset] == '/') { path[offset] = 0; if ((mkdir(path, mode) < 0) && (errno != EEXIST)) { rc = errno; goto done; } path[offset] = '/'; } } if ((mkdir(path, mode) < 0) && (errno != EEXIST)) { rc = errno; } done: free(path); return rc; } /*! * \internal * \brief Return canonicalized form of a path name * * \param[in] path Pathname to canonicalize * \param[out] resolved_path Where to store canonicalized pathname * * \return Standard Pacemaker return code * \note The caller is responsible for freeing \p resolved_path on success. * \note This function exists because not all C library versions of * realpath(path, resolved_path) support a NULL resolved_path. */ int pcmk__real_path(const char *path, char **resolved_path) { CRM_CHECK((path != NULL) && (resolved_path != NULL), return EINVAL); #if _POSIX_VERSION >= 200809L /* Recent C libraries can dynamically allocate memory as needed */ *resolved_path = realpath(path, NULL); return (*resolved_path == NULL)? errno : pcmk_rc_ok; #elif defined(PATH_MAX) /* Older implementations require pre-allocated memory */ /* (this is less desirable because PATH_MAX may be huge or not defined) */ *resolved_path = malloc(PATH_MAX); if ((*resolved_path == NULL) || (realpath(path, *resolved_path) == NULL)) { return errno; } return pcmk_rc_ok; #else *resolved_path = NULL; return ENOTSUP; #endif } /*! * \internal * \brief Create a file name using a sequence number * * \param[in] directory Directory that contains the file series * \param[in] series Start of file name * \param[in] sequence Sequence number * \param[in] bzip Whether to use ".bz2" instead of ".raw" as extension * * \return Newly allocated file path (asserts on error, so always non-NULL) * \note The caller is responsible for freeing the return value. */ char * pcmk__series_filename(const char *directory, const char *series, unsigned int sequence, bool bzip) { pcmk__assert((directory != NULL) && (series != NULL)); return crm_strdup_printf("%s/%s-%u.%s", directory, series, sequence, (bzip? "bz2" : "raw")); } /*! * \internal * \brief Read sequence number stored in a file series' .last file * * \param[in] directory Directory that contains the file series * \param[in] series Start of file name * \param[out] seq Where to store the sequence number * * \return Standard Pacemaker return code */ int pcmk__read_series_sequence(const char *directory, const char *series, unsigned int *seq) { int rc; FILE *fp = NULL; char *series_file = NULL; if ((directory == NULL) || (series == NULL) || (seq == NULL)) { return EINVAL; } series_file = crm_strdup_printf("%s/%s.last", directory, series); fp = fopen(series_file, "r"); if (fp == NULL) { rc = errno; crm_debug("Could not open series file %s: %s", series_file, strerror(rc)); free(series_file); return rc; } errno = 0; if (fscanf(fp, "%u", seq) != 1) { rc = (errno == 0)? ENODATA : errno; crm_debug("Could not read sequence number from series file %s: %s", series_file, pcmk_rc_str(rc)); fclose(fp); return rc; } fclose(fp); crm_trace("Found last sequence number %u in series file %s", *seq, series_file); free(series_file); return pcmk_rc_ok; } /*! * \internal * \brief Write sequence number to a file series' .last file * * \param[in] directory Directory that contains the file series * \param[in] series Start of file name * \param[in] sequence Sequence number to write * \param[in] max Maximum sequence value, after which it is reset to 0 * * \note This function logs some errors but does not return any to the caller */ void pcmk__write_series_sequence(const char *directory, const char *series, unsigned int sequence, int max) { int rc = 0; FILE *file_strm = NULL; char *series_file = NULL; CRM_CHECK(directory != NULL, return); CRM_CHECK(series != NULL, return); if (max == 0) { return; } if (max > 0 && sequence >= max) { sequence = 0; } series_file = crm_strdup_printf("%s/%s.last", directory, series); file_strm = fopen(series_file, "w"); if (file_strm != NULL) { rc = fprintf(file_strm, "%u", sequence); if (rc < 0) { crm_perror(LOG_ERR, "Cannot write to series file %s", series_file); } } else { crm_err("Cannot open series file %s for writing", series_file); } if (file_strm != NULL) { fflush(file_strm); fclose(file_strm); } crm_trace("Wrote %d to %s", sequence, series_file); free(series_file); } /*! * \internal * \brief Change the owner and group of a file series' .last file * * \param[in] directory Directory that contains series * \param[in] series Series to change * \param[in] uid User ID of desired file owner * \param[in] gid Group ID of desired file group * * \return Standard Pacemaker return code * \note The caller must have the appropriate privileges. */ int pcmk__chown_series_sequence(const char *directory, const char *series, uid_t uid, gid_t gid) { char *series_file = NULL; int rc = pcmk_rc_ok; if ((directory == NULL) || (series == NULL)) { return EINVAL; } series_file = crm_strdup_printf("%s/%s.last", directory, series); if (chown(series_file, uid, gid) < 0) { rc = errno; } free(series_file); return rc; } static bool pcmk__daemon_user_can_write(const char *target_name, struct stat *target_stat) { struct passwd *sys_user = NULL; errno = 0; sys_user = getpwnam(CRM_DAEMON_USER); if (sys_user == NULL) { crm_notice("Could not find user %s: %s", CRM_DAEMON_USER, pcmk_rc_str(errno)); return FALSE; } if (target_stat->st_uid != sys_user->pw_uid) { crm_notice("%s is not owned by user %s " QB_XS " uid %d != %d", target_name, CRM_DAEMON_USER, sys_user->pw_uid, target_stat->st_uid); return FALSE; } if ((target_stat->st_mode & (S_IRUSR | S_IWUSR)) == 0) { crm_notice("%s is not readable and writable by user %s " QB_XS " st_mode=0%lo", target_name, CRM_DAEMON_USER, (unsigned long) target_stat->st_mode); return FALSE; } return TRUE; } static bool pcmk__daemon_group_can_write(const char *target_name, struct stat *target_stat) { struct group *sys_grp = NULL; errno = 0; sys_grp = getgrnam(CRM_DAEMON_GROUP); if (sys_grp == NULL) { crm_notice("Could not find group %s: %s", CRM_DAEMON_GROUP, pcmk_rc_str(errno)); return FALSE; } if (target_stat->st_gid != sys_grp->gr_gid) { crm_notice("%s is not owned by group %s " QB_XS " uid %d != %d", target_name, CRM_DAEMON_GROUP, sys_grp->gr_gid, target_stat->st_gid); return FALSE; } if ((target_stat->st_mode & (S_IRGRP | S_IWGRP)) == 0) { crm_notice("%s is not readable and writable by group %s " QB_XS " st_mode=0%lo", target_name, CRM_DAEMON_GROUP, (unsigned long) target_stat->st_mode); return FALSE; } return TRUE; } /*! * \internal * \brief Check whether a directory or file is writable by the cluster daemon * * Return true if either the cluster daemon user or cluster daemon group has * write permission on a specified file or directory. * * \param[in] dir Directory to check (this argument must be specified, and * the directory must exist) * \param[in] file File to check (only the directory will be checked if this * argument is not specified or the file does not exist) * * \return true if target is writable by cluster daemon, false otherwise */ bool pcmk__daemon_can_write(const char *dir, const char *file) { int s_res = 0; struct stat buf; char *full_file = NULL; const char *target = NULL; // Caller must supply directory pcmk__assert(dir != NULL); // If file is given, check whether it exists as a regular file if (file != NULL) { full_file = crm_strdup_printf("%s/%s", dir, file); target = full_file; s_res = stat(full_file, &buf); if (s_res < 0) { crm_notice("%s not found: %s", target, pcmk_rc_str(errno)); free(full_file); full_file = NULL; target = NULL; } else if (S_ISREG(buf.st_mode) == FALSE) { crm_err("%s must be a regular file " QB_XS " st_mode=0%lo", target, (unsigned long) buf.st_mode); free(full_file); return false; } } // If file is not given, ensure dir exists as directory if (target == NULL) { target = dir; s_res = stat(dir, &buf); if (s_res < 0) { crm_err("%s not found: %s", dir, pcmk_rc_str(errno)); return false; } else if (S_ISDIR(buf.st_mode) == FALSE) { crm_err("%s must be a directory " QB_XS " st_mode=0%lo", dir, (unsigned long) buf.st_mode); return false; } } if (!pcmk__daemon_user_can_write(target, &buf) && !pcmk__daemon_group_can_write(target, &buf)) { crm_err("%s must be owned and writable by either user %s or group %s " QB_XS " st_mode=0%lo", target, CRM_DAEMON_USER, CRM_DAEMON_GROUP, (unsigned long) buf.st_mode); free(full_file); return false; } free(full_file); return true; } /*! * \internal * \brief Flush and sync a directory to disk * * \param[in] name Directory to flush and sync * \note This function logs errors but does not return them to the caller */ void pcmk__sync_directory(const char *name) { int fd; DIR *directory; directory = opendir(name); if (directory == NULL) { crm_perror(LOG_ERR, "Could not open %s for syncing", name); return; } fd = dirfd(directory); if (fd < 0) { crm_perror(LOG_ERR, "Could not obtain file descriptor for %s", name); return; } if (fsync(fd) < 0) { crm_perror(LOG_ERR, "Could not sync %s", name); } if (closedir(directory) < 0) { crm_perror(LOG_ERR, "Could not close %s after fsync", name); } } /*! * \internal * \brief Read the contents of a file * * \param[in] filename Name of file to read * \param[out] contents Where to store file contents * * \return Standard Pacemaker return code * \note On success, the caller is responsible for freeing contents. */ int pcmk__file_contents(const char *filename, char **contents) { FILE *fp; int length, read_len; int rc = pcmk_rc_ok; if ((filename == NULL) || (contents == NULL)) { return EINVAL; } fp = fopen(filename, "r"); if ((fp == NULL) || (fseek(fp, 0L, SEEK_END) < 0)) { rc = errno; goto bail; } length = ftell(fp); if (length < 0) { rc = errno; goto bail; } if (length == 0) { *contents = NULL; } else { *contents = calloc(length + 1, sizeof(char)); if (*contents == NULL) { rc = errno; goto bail; } rewind(fp); read_len = fread(*contents, 1, length, fp); if (read_len != length) { free(*contents); *contents = NULL; rc = EIO; } else { /* Coverity thinks *contents isn't null-terminated. It doesn't * understand calloc(). */ (*contents)[length] = '\0'; } } bail: if (fp != NULL) { fclose(fp); } return rc; } /*! * \internal * \brief Write text to a file, flush and sync it to disk, then close the file * * \param[in] fd File descriptor opened for writing * \param[in] contents String to write to file * * \return Standard Pacemaker return code */ int pcmk__write_sync(int fd, const char *contents) { int rc = 0; FILE *fp = fdopen(fd, "w"); if (fp == NULL) { return errno; } if ((contents != NULL) && (fprintf(fp, "%s", contents) < 0)) { rc = EIO; } if (fflush(fp) != 0) { rc = errno; } if (fsync(fileno(fp)) < 0) { rc = errno; } fclose(fp); return rc; } /*! * \internal * \brief Set a file descriptor to non-blocking * * \param[in] fd File descriptor to use * * \return Standard Pacemaker return code */ int pcmk__set_nonblocking(int fd) { int flag = fcntl(fd, F_GETFL); if (flag < 0) { return errno; } if (fcntl(fd, F_SETFL, flag | O_NONBLOCK) < 0) { return errno; } return pcmk_rc_ok; } /*! * \internal * \brief Get directory name for temporary files * * Return the value of the TMPDIR environment variable if it is set to a * full path, otherwise return "/tmp". * * \return Name of directory to be used for temporary files */ const char * pcmk__get_tmpdir(void) { const char *dir = getenv("TMPDIR"); return (dir && (*dir == '/'))? dir : "/tmp"; } /*! * \internal * \brief Close open file descriptors * * Close all file descriptors (except optionally stdin, stdout, and stderr), * which is a best practice for a new child process forked for the purpose of * executing an external program. * * \param[in] bool If true, close stdin, stdout, and stderr as well */ void pcmk__close_fds_in_child(bool all) { DIR *dir; struct rlimit rlim; rlim_t max_fd; int min_fd = (all? 0 : (STDERR_FILENO + 1)); /* Find the current process's (soft) limit for open files. getrlimit() * should always work, but have a fallback just in case. */ if (getrlimit(RLIMIT_NOFILE, &rlim) == 0) { max_fd = rlim.rlim_cur - 1; } else { long conf_max = sysconf(_SC_OPEN_MAX); max_fd = (conf_max > 0)? conf_max : 1024; } - /* /proc/self/fd (on Linux) or /dev/fd (on most OSes) contains symlinks to - * all open files for the current process, named as the file descriptor. - * Use this if available, because it's more efficient than a shotgun - * approach to closing descriptors. + /* First try /proc. If that returns NULL (either because opening the + * directory failed, or because procfs isn't supported on this platform), + * fall back to /dev/fd. */ -#if HAVE_LINUX_PROCFS - dir = opendir("/proc/self/fd"); + dir = pcmk__procfs_fd_dir(); if (dir == NULL) { dir = opendir("/dev/fd"); } -#else - dir = opendir("/dev/fd"); -#endif // HAVE_LINUX_PROCFS + if (dir != NULL) { struct dirent *entry; int dir_fd = dirfd(dir); while ((entry = readdir(dir)) != NULL) { int lpc = atoi(entry->d_name); /* How could one of these entries be higher than max_fd, you ask? * It isn't possible in normal operation, but when run under * valgrind, valgrind can open high-numbered file descriptors for * its own use that are higher than the process's soft limit. * These will show up in the fd directory but aren't closable. */ if ((lpc >= min_fd) && (lpc <= max_fd) && (lpc != dir_fd)) { close(lpc); } } closedir(dir); return; } /* If no fd directory is available, iterate over all possible descriptors. * This is less efficient due to the overhead of many system calls. */ for (int lpc = max_fd; lpc >= min_fd; lpc--) { close(lpc); } } /*! * \brief Duplicate a file path, inserting a prefix if not absolute * * \param[in] filename File path to duplicate * \param[in] dirname If filename is not absolute, prefix to add * * \return Newly allocated memory with full path (guaranteed non-NULL) */ char * pcmk__full_path(const char *filename, const char *dirname) { pcmk__assert(filename != NULL); if (filename[0] == '/') { return pcmk__str_copy(filename); } pcmk__assert(dirname != NULL); return crm_strdup_printf("%s/%s", dirname, filename); } diff --git a/lib/common/procfs.c b/lib/common/procfs.c index bc610ecbd4..a6b0d8e7ac 100644 --- a/lib/common/procfs.c +++ b/lib/common/procfs.c @@ -1,223 +1,465 @@ /* * Copyright 2015-2024 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 +#if HAVE_LINUX_PROCFS +/*! + * \internal + * \brief Return name of /proc file containing the CIB daemon's load statistics + * + * \return Newly allocated memory with file name on success, NULL otherwise + * + * \note It is the caller's responsibility to free the return value. + * This will return NULL if the daemon is being run via valgrind. + * This should be called only on Linux systems. + */ +static char * +find_cib_loadfile(const char *server) +{ + pid_t pid = pcmk__procfs_pid_of(server); + + return pid? crm_strdup_printf("/proc/%lld/stat", (long long) pid) : NULL; +} + /*! * \internal * \brief Get process ID and name associated with a /proc directory entry * * \param[in] entry Directory entry (must be result of readdir() on /proc) * \param[out] name If not NULL, a char[16] to hold the process name * \param[out] pid If not NULL, will be set to process ID of entry * * \return Standard Pacemaker return code * \note This should be called only on Linux systems, as not all systems that * support /proc store process names and IDs in the same way. The kernel * limits the process name to the first 15 characters (plus terminator). * It would be nice if there were a public kernel API constant for that * limit, but there isn't. */ static int pcmk__procfs_process_info(const struct dirent *entry, char *name, pid_t *pid) { int fd, local_pid; FILE *file; struct stat statbuf; char procpath[128] = { 0 }; /* We're only interested in entries whose name is a PID, * so skip anything non-numeric or that is too long. * * 114 = 128 - strlen("/proc/") - strlen("/status") - 1 */ local_pid = atoi(entry->d_name); if ((local_pid <= 0) || (strlen(entry->d_name) > 114)) { return -1; } if (pid) { *pid = (pid_t) local_pid; } /* Get this entry's file information */ strcpy(procpath, "/proc/"); strcat(procpath, entry->d_name); fd = open(procpath, O_RDONLY); if (fd < 0 ) { return -1; } if (fstat(fd, &statbuf) < 0) { close(fd); return -1; } close(fd); /* We're only interested in subdirectories */ if (!S_ISDIR(statbuf.st_mode)) { return -1; } /* Read the first entry ("Name:") from the process's status file. * We could handle the valgrind case if we parsed the cmdline file * instead, but that's more of a pain than it's worth. */ if (name != NULL) { strcat(procpath, "/status"); file = fopen(procpath, "r"); if (!file) { return -1; } if (fscanf(file, "Name:\t%15[^\n]", name) != 1) { fclose(file); return -1; } name[15] = 0; fclose(file); } return 0; } +#endif // HAVE_LINUX_PROCFS /*! * \internal * \brief Return process ID of a named process * * \param[in] name Process name (as used in /proc/.../status) * * \return Process ID of named process if running, 0 otherwise * * \note This will return 0 if the process is being run via valgrind. * This should be called only on Linux systems. */ pid_t pcmk__procfs_pid_of(const char *name) { +#if HAVE_LINUX_PROCFS DIR *dp; struct dirent *entry; pid_t pid = 0; char entry_name[64] = { 0 }; dp = opendir("/proc"); if (dp == NULL) { crm_notice("Can not read /proc directory to track existing components"); return 0; } while ((entry = readdir(dp)) != NULL) { if ((pcmk__procfs_process_info(entry, entry_name, &pid) == pcmk_rc_ok) && pcmk__str_eq(entry_name, name, pcmk__str_casei) && (pcmk__pid_active(pid, NULL) == pcmk_rc_ok)) { crm_info("Found %s active as process %lld", name, (long long) pid); break; } pid = 0; } closedir(dp); return pid; +#else + return 0; +#endif // HAVE_LINUX_PROCFS } /*! * \internal * \brief Calculate number of logical CPU cores from procfs * * \return Number of cores (or 1 if unable to determine) */ unsigned int pcmk__procfs_num_cores(void) { +#if HAVE_LINUX_PROCFS int cores = 0; FILE *stream = NULL; /* Parse /proc/stat instead of /proc/cpuinfo because it's smaller */ stream = fopen("/proc/stat", "r"); if (stream == NULL) { crm_perror(LOG_INFO, "Could not open /proc/stat"); } else { char buffer[2048]; while (fgets(buffer, sizeof(buffer), stream)) { if (pcmk__starts_with(buffer, "cpu") && isdigit(buffer[3])) { ++cores; } } fclose(stream); } return cores? cores : 1; +#else + return 1; +#endif // HAVE_LINUX_PROCFS } /*! * \internal * \brief Get the executable path corresponding to a process ID * * \param[in] pid Process ID to check * \param[out] path Where to store executable path * \param[in] path_size Size of \p path in characters (ideally PATH_MAX) * * \return Standard Pacemaker error code (as possible errno values from * readlink()) */ int pcmk__procfs_pid2path(pid_t pid, char path[], size_t path_size) { #if HAVE_LINUX_PROCFS char procfs_exe_path[PATH_MAX]; ssize_t link_rc; if (snprintf(procfs_exe_path, PATH_MAX, "/proc/%lld/exe", (long long) pid) >= PATH_MAX) { return ENAMETOOLONG; // Truncated (shouldn't be possible in practice) } link_rc = readlink(procfs_exe_path, path, path_size - 1); if (link_rc < 0) { return errno; } else if (link_rc >= (path_size - 1)) { return ENAMETOOLONG; } path[link_rc] = '\0'; return pcmk_rc_ok; #else return EOPNOTSUPP; #endif // HAVE_LINUX_PROCFS } /*! * \internal * \brief Check whether process ID information is available from procfs * * \return true if process ID information is available, otherwise false */ bool pcmk__procfs_has_pids(void) { #if HAVE_LINUX_PROCFS static bool have_pids = false; static bool checked = false; if (!checked) { char path[PATH_MAX]; have_pids = pcmk__procfs_pid2path(getpid(), path, sizeof(path)) == pcmk_rc_ok; checked = true; } return have_pids; #else return false; #endif // HAVE_LINUX_PROCFS } + +/*! + * \internal + * \brief Return an open handle on the directory containing links to open file + * descriptors, or NULL on error + */ +DIR * +pcmk__procfs_fd_dir(void) +{ + DIR *dir = NULL; + + /* /proc/self/fd (on Linux) or /dev/fd (on most OSes) contains symlinks to + * all open files for the current process, named as the file descriptor. + * Use this if available, because it's more efficient than a shotgun + * approach to closing descriptors. + */ +#if HAVE_LINUX_PROCFS + dir = opendir("/proc/self/fd"); +#endif // HAVE_LINUX_PROCFS + + return dir; +} + +/*! + * \internal + * \brief Trigger a sysrq command if supported on current platform + * + * \param[in] t Sysrq command to trigger + */ +void +pcmk__sysrq_trigger(char t) +{ +#if HAVE_LINUX_PROCFS + // Root can always write here, regardless of kernel.sysrq value + FILE *procf = fopen("/proc/sysrq-trigger", "a"); + + if (procf == NULL) { + crm_warn("Could not open sysrq-trigger: %s", strerror(errno)); + } else { + fprintf(procf, "%c\n", t); + fclose(procf); + } +#endif // HAVE_LINUX_PROCFS +} + +bool +pcmk__throttle_cib_load(const char *server, float *load) +{ +/* /proc/[pid]/stat + * + * Status information about the process. This is used by ps(1). It is defined + * in /usr/src/linux/fs/proc/array.c. + * + * The fields, in order, with their proper scanf(3) format specifiers, are: + * + * pid %d (1) The process ID. + * comm %s (2) The filename of the executable, in parentheses. This is + * visible whether or not the executable is swapped out. + * state %c (3) One character from the string "RSDZTW" where R is running, + * S is sleeping in an interruptible wait, D is waiting in + * uninterruptible disk sleep, Z is zombie, T is traced or + * stopped (on a signal), and W is paging. + * ppid %d (4) The PID of the parent. + * pgrp %d (5) The process group ID of the process. + * session %d (6) The session ID of the process. + * tty_nr %d (7) The controlling terminal of the process. (The minor device + * number is contained in the combination of bits 31 to 20 and + * 7 to 0; the major device number is in bits 15 to 8.) + * tpgid %d (8) The ID of the foreground process group of the controlling + * terminal of the process. + * flags %u (9) The kernel flags word of the process. For bit meanings, see + * the PF_* defines in the Linux kernel source file include/linux/sched.h. + * Details depend on the kernel version. + * minflt %lu (10) The number of minor faults the process has made which have + * not required loading a memory page from disk. + * cminflt %lu (11) The number of minor faults that the process's waited-for + * children have made. + * majflt %lu (12) The number of major faults the process has made which have + * required loading a memory page from disk. + * cmajflt %lu (13) The number of major faults that the process's waited-for + * children have made. + * utime %lu (14) Amount of time that this process has been scheduled in user + * mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). + * This includes guest time, guest_time (time spent running a + * virtual CPU, see below), so that applications that are not + * aware of the guest time field do not lose that time from + * their calculations. + * stime %lu (15) Amount of time that this process has been scheduled in + * kernel mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). + */ + +#if HAVE_LINUX_PROCFS + static char *loadfile = NULL; + static time_t last_call = 0; + static long ticks_per_s = 0; + static unsigned long last_utime, last_stime; + + char buffer[64*1024]; + FILE *stream = NULL; + time_t now = time(NULL); + + if (load == NULL) { + return false; + } else { + *load = 0.0; + } + + if (loadfile == NULL) { + last_call = 0; + last_utime = 0; + last_stime = 0; + + loadfile = find_cib_loadfile(server); + if (loadfile == NULL) { + crm_warn("Couldn't find CIB load file"); + return false; + } + + ticks_per_s = sysconf(_SC_CLK_TCK); + crm_trace("Found %s", loadfile); + } + + stream = fopen(loadfile, "r"); + if (stream == NULL) { + int rc = errno; + + crm_warn("Couldn't read %s: %s (%d)", loadfile, pcmk_rc_str(rc), rc); + free(loadfile); + loadfile = NULL; + return false; + } + + if (fgets(buffer, sizeof(buffer), stream) != NULL) { + char *comm = pcmk__assert_alloc(1, 256); + char state = 0; + int rc = 0, pid = 0, ppid = 0, pgrp = 0, session = 0, tty_nr = 0, tpgid = 0; + unsigned long flags = 0, minflt = 0, cminflt = 0, majflt = 0, cmajflt = 0, utime = 0, stime = 0; + + rc = sscanf(buffer, "%d %[^ ] %c %d %d %d %d %d %lu %lu %lu %lu %lu %lu %lu", + &pid, comm, &state, &ppid, &pgrp, &session, &tty_nr, &tpgid, + &flags, &minflt, &cminflt, &majflt, &cmajflt, &utime, &stime); + free(comm); + + if (rc != 15) { + crm_err("Only %d of 15 fields found in %s", rc, loadfile); + fclose(stream); + return false; + + } else if ((last_call > 0) && (last_call < now) && (last_utime <= utime) && + (last_stime <= stime)) { + time_t elapsed = now - last_call; + unsigned long delta_utime = utime - last_utime; + unsigned long delta_stime = stime - last_stime; + + *load = delta_utime + delta_stime; /* Cast to a float before division */ + *load /= ticks_per_s; + *load /= elapsed; + crm_debug("cib load: %f (%lu ticks in %lds)", *load, + delta_utime + delta_stime, (long) elapsed); + + } else { + crm_debug("Init %lu + %lu ticks at %ld (%lu tps)", utime, stime, + (long) now, ticks_per_s); + } + + last_call = now; + last_utime = utime; + last_stime = stime; + + fclose(stream); + return true; + } + + fclose(stream); +#endif // HAVE_LINUX_PROCFS + return false; +} + +bool +pcmk__throttle_load_avg(float *load) +{ +#if HAVE_LINUX_PROCFS + char buffer[256]; + FILE *stream = NULL; + const char *loadfile = "/proc/loadavg"; + + if (load == NULL) { + return false; + } + + stream = fopen(loadfile, "r"); + if (stream == NULL) { + int rc = errno; + crm_warn("Couldn't read %s: %s (%d)", loadfile, pcmk_rc_str(rc), rc); + return false; + } + + if (fgets(buffer, sizeof(buffer), stream) != NULL) { + char *nl = strstr(buffer, "\n"); + + /* Grab the 1-minute average, ignore the rest */ + *load = strtof(buffer, NULL); + if (nl != NULL) { + nl[0] = 0; + } + + fclose(stream); + return true; + } + + fclose(stream); +#endif // HAVE_LINUX_PROCFS + return false; +} diff --git a/lib/common/watchdog.c b/lib/common/watchdog.c index 6196e571a9..1db29815b3 100644 --- a/lib/common/watchdog.c +++ b/lib/common/watchdog.c @@ -1,315 +1,289 @@ /* * Copyright 2013-2024 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 static pid_t sbd_pid = 0; -/*! - * \internal - * \brief Trigger a sysrq command if supported on current platform - * - * \param[in] t Sysrq command to trigger - */ -static void -sysrq_trigger(char t) -{ -#if HAVE_LINUX_PROCFS - // Root can always write here, regardless of kernel.sysrq value - FILE *procf = fopen("/proc/sysrq-trigger", "a"); - - if (procf == NULL) { - crm_warn("Could not open sysrq-trigger: %s", strerror(errno)); - } else { - fprintf(procf, "%c\n", t); - fclose(procf); - } -#endif // HAVE_LINUX_PROCFS -} - /*! * \internal * \brief Tell pacemakerd to panic the local host * * \param[in] ppid Process ID of parent process */ static void panic_local_nonroot(pid_t ppid) { if (ppid > 1) { // pacemakerd is still our parent crm_emerg("Escalating panic to " PCMK__SERVER_PACEMAKERD "[%lld]", (long long) ppid); } else { // Signal (non-parent) pacemakerd if possible -#if HAVE_LINUX_PROCFS ppid = pcmk__procfs_pid_of(PCMK__SERVER_PACEMAKERD); if (ppid > 0) { union sigval signal_value; crm_emerg("Signaling " PCMK__SERVER_PACEMAKERD "[%lld] to panic", (long long) ppid); memset(&signal_value, 0, sizeof(signal_value)); if (sigqueue(ppid, SIGQUIT, signal_value) < 0) { crm_emerg("Exiting after signal failure: %s", strerror(errno)); } } else { -#endif crm_emerg("Exiting with no known " PCMK__SERVER_PACEMAKERD "process"); -#if HAVE_LINUX_PROCFS } -#endif } crm_exit(CRM_EX_PANIC); } /*! * \internal * \brief Panic the local host (if root) or tell pacemakerd to do so */ static void panic_local(void) { const char *full_panic_action = pcmk__env_option(PCMK__ENV_PANIC_ACTION); const char *panic_action = full_panic_action; int reboot_cmd = RB_AUTOBOOT; // Default panic action is reboot if (geteuid() != 0) { // Non-root caller such as the controller panic_local_nonroot(getppid()); return; } if (pcmk__starts_with(full_panic_action, "sync-")) { panic_action += sizeof("sync-") - 1; sync(); } if (pcmk__str_empty(full_panic_action) || pcmk__str_eq(panic_action, PCMK_VALUE_REBOOT, pcmk__str_none)) { - sysrq_trigger('b'); + pcmk__sysrq_trigger('b'); } else if (pcmk__str_eq(panic_action, PCMK_VALUE_CRASH, pcmk__str_none)) { - sysrq_trigger('c'); + pcmk__sysrq_trigger('c'); } else if (pcmk__str_eq(panic_action, PCMK_VALUE_OFF, pcmk__str_none)) { - sysrq_trigger('o'); + pcmk__sysrq_trigger('o'); #ifdef RB_POWER_OFF reboot_cmd = RB_POWER_OFF; #elif defined(RB_POWEROFF) reboot_cmd = RB_POWEROFF; #endif } else { crm_warn("Using default '" PCMK_VALUE_REBOOT "' for local option PCMK_" PCMK__ENV_PANIC_ACTION " because '%s' is not a valid value", full_panic_action); - sysrq_trigger('b'); + pcmk__sysrq_trigger('b'); } // sysrq failed or is not supported on this platform, so fall back to reboot reboot(reboot_cmd); // Even reboot failed, nothing left to do but exit crm_emerg("Exiting after reboot failed: %s", strerror(errno)); if (getppid() > 1) { // pacemakerd is parent process crm_exit(CRM_EX_PANIC); } else { // This is pacemakerd, or an orphaned subdaemon crm_exit(CRM_EX_FATAL); } } /*! * \internal * \brief Tell sbd to kill the local host, then exit */ static void panic_sbd(void) { union sigval signal_value; pid_t ppid = getppid(); memset(&signal_value, 0, sizeof(signal_value)); /* TODO: Arrange for a slightly less brutal option? */ if(sigqueue(sbd_pid, SIGKILL, signal_value) < 0) { crm_emerg("Panicking directly because couldn't signal sbd"); panic_local(); } if(ppid > 1) { /* child daemon */ crm_exit(CRM_EX_PANIC); } else { /* pacemakerd or orphan child */ crm_exit(CRM_EX_FATAL); } } /*! * \internal * \brief Panic the local host * * Panic the local host either by sbd (if running), directly, or by asking * pacemakerd. If trace logging this function, exit instead. * * \param[in] reason Why panic is needed (for logging only) */ void pcmk__panic(const char *reason) { if (pcmk__locate_sbd() > 1) { crm_emerg("Signaling sbd[%lld] to panic the system: %s", (long long) sbd_pid, reason); panic_sbd(); } else { crm_emerg("Panicking the system directly: %s", reason); panic_local(); } } /*! * \internal * \brief Return the process ID of sbd (or 0 if it is not running) */ pid_t pcmk__locate_sbd(void) { char *pidfile = NULL; char *sbd_path = NULL; int rc; if(sbd_pid > 1) { return sbd_pid; } /* Look for the pid file */ pidfile = crm_strdup_printf(PCMK__RUN_DIR "/sbd.pid"); sbd_path = crm_strdup_printf("%s/sbd", SBIN_DIR); /* Read the pid file */ rc = pcmk__pidfile_matches(pidfile, 0, sbd_path, &sbd_pid); if (rc == pcmk_rc_ok) { crm_trace("SBD detected at pid %lld (via PID file %s)", (long long) sbd_pid, pidfile); - -#if HAVE_LINUX_PROCFS } else { /* Fall back to /proc for systems that support it */ sbd_pid = pcmk__procfs_pid_of("sbd"); - crm_trace("SBD detected at pid %lld (via procfs)", - (long long) sbd_pid); -#endif // HAVE_LINUX_PROCFS + + if (sbd_pid != 0) { + crm_trace("SBD detected at pid %lld (via procfs)", + (long long) sbd_pid); + } } if(sbd_pid < 0) { sbd_pid = 0; crm_trace("SBD not detected"); } free(pidfile); free(sbd_path); return sbd_pid; } long pcmk__get_sbd_watchdog_timeout(void) { static long sbd_timeout = -2; if (sbd_timeout == -2) { sbd_timeout = crm_get_msec(getenv("SBD_WATCHDOG_TIMEOUT")); } return sbd_timeout; } bool pcmk__get_sbd_sync_resource_startup(void) { static int sync_resource_startup = PCMK__SBD_SYNC_DEFAULT; static bool checked_sync_resource_startup = false; if (!checked_sync_resource_startup) { const char *sync_env = getenv("SBD_SYNC_RESOURCE_STARTUP"); if (sync_env == NULL) { crm_trace("Defaulting to %sstart-up synchronization with sbd", (PCMK__SBD_SYNC_DEFAULT? "" : "no ")); } else if (crm_str_to_boolean(sync_env, &sync_resource_startup) < 0) { crm_warn("Defaulting to %sstart-up synchronization with sbd " "because environment value '%s' is invalid", (PCMK__SBD_SYNC_DEFAULT? "" : "no "), sync_env); } checked_sync_resource_startup = true; } return sync_resource_startup != 0; } long pcmk__auto_stonith_watchdog_timeout(void) { long sbd_timeout = pcmk__get_sbd_watchdog_timeout(); return (sbd_timeout <= 0)? 0 : (2 * sbd_timeout); } bool pcmk__valid_stonith_watchdog_timeout(const char *value) { /* @COMPAT At a compatibility break, accept either negative values or a * specific string like "auto" (but not both) to mean "auto-calculate the * timeout." Reject other values that aren't parsable as timeouts. */ long st_timeout = value? crm_get_msec(value) : 0; if (st_timeout < 0) { st_timeout = pcmk__auto_stonith_watchdog_timeout(); crm_debug("Using calculated value %ld for " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " (%s)", st_timeout, value); } if (st_timeout == 0) { crm_debug("Watchdog may be enabled but " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " is disabled (%s)", value? value : "default"); } else if (pcmk__locate_sbd() == 0) { crm_emerg("Shutting down: " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " configured (%s) but SBD not active", pcmk__s(value, "auto")); crm_exit(CRM_EX_FATAL); return false; } else { long sbd_timeout = pcmk__get_sbd_watchdog_timeout(); if (st_timeout < sbd_timeout) { crm_emerg("Shutting down: " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " (%s) too short (must be >%ldms)", value, sbd_timeout); crm_exit(CRM_EX_FATAL); return false; } crm_info("Watchdog configured with " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " %s and SBD timeout %ldms", value, sbd_timeout); } return true; }