diff --git a/lib/services/services.c b/lib/services/services.c index 1800a6b999..03da623435 100644 --- a/lib/services/services.c +++ b/lib/services/services.c @@ -1,1316 +1,1310 @@ /* * Copyright 2010-2021 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 #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include "services_private.h" #include "services_ocf.h" #include "services_lsb.h" #if SUPPORT_UPSTART # include #endif #if SUPPORT_SYSTEMD # include #endif #if SUPPORT_NAGIOS # 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, then upstart. It would be preferable * to put systemd first, but LSB merely requires a file existence check, * while systemd requires contacting D-Bus. */ const char * resources_find_service_class(const char *agent) { if (services__lsb_agent_exists(agent)) { return PCMK_RESOURCE_CLASS_LSB; } #if SUPPORT_SYSTEMD if (systemd_unit_exists(agent)) { return PCMK_RESOURCE_CLASS_SYSTEMD; } #endif #if SUPPORT_UPSTART if (upstart_job_exists(agent)) { return PCMK_RESOURCE_CLASS_UPSTART; } #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 or upstart op * * \param[in] op Operation to check * * \return TRUE if op is in-flight systemd or upstart op */ static inline gboolean inflight_systemd_or_upstart(svc_action_t *op) { return pcmk__strcase_any_of(op->standard, PCMK_RESOURCE_CLASS_SYSTEMD, PCMK_RESOURCE_CLASS_UPSTART, NULL) && 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 (strcasecmp(standard, PCMK_RESOURCE_CLASS_SERVICE) == 0) { const char *found_class = resources_find_service_class(agent); if (found_class) { crm_debug("Found %s agent %s for %s", found_class, agent, rsc); expanded_class = strdup(found_class); } else { crm_info("Assuming resource class lsb for agent %s for %s", agent, rsc); expanded_class = strdup(PCMK_RESOURCE_CLASS_LSB); } } else { expanded_class = strdup(standard); } CRM_ASSERT(expanded_class); 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, "monitor", pcmk__str_casei)) { 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; } 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); } else if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_LSB) == 0) { rc = services__lsb_prepare(op); #if SUPPORT_SYSTEMD } else if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_SYSTEMD) == 0) { - op->opaque->exec = strdup("systemd-dbus"); - if (op->opaque->exec == NULL) { - crm_crit("Cannot prepare %s action for %s: %s", - action, name, strerror(ENOMEM)); - services__handle_exec_error(op, ENOMEM); - return op; - } + rc = services__systemd_prepare(op); #endif #if SUPPORT_UPSTART } else if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_UPSTART) == 0) { op->opaque->exec = strdup("upstart-dbus"); if (op->opaque->exec == NULL) { crm_crit("Cannot prepare %s action for %s: %s", action, name, strerror(ENOMEM)); services__handle_exec_error(op, ENOMEM); return op; } #endif #if SUPPORT_NAGIOS } else if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_NAGIOS) == 0) { op->opaque->exec = pcmk__full_path(op->agent, NAGIOS_PLUGIN_DIR); op->opaque->args[0] = strdup(op->opaque->exec); if (op->opaque->args[0] == NULL) { crm_crit("Cannot prepare %s action for %s: %s", action, name, strerror(ENOMEM)); services__handle_exec_error(op, ENOMEM); return op; } if (pcmk__str_eq(op->action, "monitor", pcmk__str_casei) && (op->interval_ms == 0)) { /* Invoke --version for a nagios probe */ op->opaque->args[1] = strdup("--version"); if (op->opaque->args[1] == NULL) { crm_crit("Cannot prepare %s action for %s: %s", action, name, strerror(ENOMEM)); services__handle_exec_error(op, ENOMEM); return op; } } else if (op->params) { GHashTableIter iter; char *key = NULL; char *value = NULL; int index = 1; // 0 is already set to executable name g_hash_table_iter_init(&iter, op->params); while (g_hash_table_iter_next(&iter, (gpointer *) & key, (gpointer *) & value)) { if (index > (PCMK__NELEM(op->opaque->args) - 2)) { crm_info("Cannot prepare %s action for %s: Too many parameters", action, name); services__set_result(op, NAGIOS_STATE_UNKNOWN, PCMK_EXEC_ERROR_HARD, "Too many parameters"); break; } if (pcmk__str_eq(key, XML_ATTR_CRM_VERSION, pcmk__str_casei) || strstr(key, CRM_META "_")) { continue; } op->opaque->args[index++] = crm_strdup_printf("--%s", key); op->opaque->args[index++] = strdup(value); if (op->opaque->args[index - 1] == NULL) { crm_crit("Cannot prepare %s action for %s: %s", action, name, strerror(ENOMEM)); services__handle_exec_error(op, ENOMEM); return op; } } } // Nagios actions don't need to keep the parameters if (op->params != NULL) { g_hash_table_destroy(op->params); op->params = NULL; } #endif } else { crm_err("Unknown resource standard: %s", op->standard); rc = ENOENT; } if (rc != pcmk_rc_ok) { crm_err("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(); CRM_ASSERT(op != NULL); 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->timeout = timeout; action->id = strdup(id); 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] action 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 or upstart 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) { CRM_CHECK((op != NULL) && (user != NULL), return -EINVAL); return crm_user_lookup(user, &(op->opaque->uid), &(op->opaque->gid)); } /*! * \brief Execute an alert agent action * * \param[in] 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(). */ 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 SUPPORT_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 SUPPORT_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; } } 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); 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 SUPPORT_DBUS // In-flight systemd and upstart ops don't have a pid if (inflight_systemd_or_upstart(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_result(op, op->rc, PCMK_EXEC_CANCELLED, NULL); 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_or_upstart(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] 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] 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 already was or is * pending) * * \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_UPSTART if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_UPSTART, pcmk__str_casei)) { return services__execute_upstart(op); } #endif #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; } CRM_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(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 *)) { op->synchronous = false; if (action_callback) { op->opaque->callback = action_callback; } if (action_fork_callback) { op->opaque->fork_callback = action_fork_callback; } if (op->interval_ms > 0) { init_recurring_actions(); if (handle_duplicate_recurring(op) == TRUE) { /* 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 && 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_casei)) { 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] 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_err("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_err("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 (!strcmp(class, PCMK_RESOURCE_CLASS_SERVICE)) { class = resources_find_service_class(op->agent); } if (class == NULL) { crm_err("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; } if (pcmk__str_eq(class, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei)) { return pcmk_legacy2rc(services__get_lsb_metadata(op->agent, &op->stdout_data)); } #if SUPPORT_NAGIOS if (pcmk__str_eq(class, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_casei)) { return pcmk_legacy2rc(services__get_nagios_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, "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)); standards = g_list_append(standards, strdup(PCMK_RESOURCE_CLASS_LSB)); standards = g_list_append(standards, strdup(PCMK_RESOURCE_CLASS_SERVICE)); #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 #if SUPPORT_UPSTART { GList *agents = upstart_job_listall(); if (agents != NULL) { standards = g_list_append(standards, strdup(PCMK_RESOURCE_CLASS_UPSTART)); g_list_free_full(agents, free); } } #endif #if SUPPORT_NAGIOS { GList *agents = services__list_nagios_agents(); if (agents != NULL) { standards = g_list_append(standards, strdup(PCMK_RESOURCE_CLASS_NAGIOS)); 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) || (strcasecmp(standard, PCMK_RESOURCE_CLASS_SERVICE) == 0)) { GList *tmp1; GList *tmp2; GList *result = services__list_lsb_agents(); if (standard == NULL) { tmp1 = result; tmp2 = resources_os_list_ocf_agents(NULL); if (tmp2) { result = g_list_concat(tmp1, tmp2); } } #if SUPPORT_SYSTEMD tmp1 = result; tmp2 = systemd_unit_listall(); if (tmp2) { result = g_list_concat(tmp1, tmp2); } #endif #if SUPPORT_UPSTART tmp1 = result; tmp2 = upstart_job_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); } else if (strcasecmp(standard, PCMK_RESOURCE_CLASS_LSB) == 0) { return services__list_lsb_agents(); #if SUPPORT_SYSTEMD } else if (strcasecmp(standard, PCMK_RESOURCE_CLASS_SYSTEMD) == 0) { return systemd_unit_listall(); #endif #if SUPPORT_UPSTART } else if (strcasecmp(standard, PCMK_RESOURCE_CLASS_UPSTART) == 0) { return upstart_job_listall(); #endif #if SUPPORT_NAGIOS } else if (strcasecmp(standard, PCMK_RESOURCE_CLASS_NAGIOS) == 0) { return services__list_nagios_agents(); #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__str_eq(standard, PCMK_RESOURCE_CLASS_SERVICE, pcmk__str_casei)) { if (services__lsb_agent_exists(agent)) { rc = TRUE; #if SUPPORT_SYSTEMD } else if (systemd_unit_exists(agent)) { rc = TRUE; #endif #if SUPPORT_UPSTART } else if (upstart_job_exists(agent)) { rc = TRUE; #endif } else { rc = FALSE; } } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_casei)) { rc = services__ocf_agent_exists(provider, agent); } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei)) { rc = services__lsb_agent_exists(agent); #if SUPPORT_SYSTEMD } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_SYSTEMD, pcmk__str_casei)) { rc = systemd_unit_exists(agent); #endif #if SUPPORT_UPSTART } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_UPSTART, pcmk__str_casei)) { rc = upstart_job_exists(agent); #endif #if SUPPORT_NAGIOS } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_casei)) { rc = services__nagios_agent_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 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(svc_action_t *action) { return action->opaque->exit_reason; } diff --git a/lib/services/systemd.c b/lib/services/systemd.c index d3e2eecac4..f832f02494 100644 --- a/lib/services/systemd.c +++ b/lib/services/systemd.c @@ -1,1019 +1,1037 @@ /* * Copyright 2012-2021 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 static void invoke_unit_by_path(svc_action_t *op, const char *unit); #define BUS_NAME "org.freedesktop.systemd1" #define BUS_NAME_MANAGER BUS_NAME ".Manager" #define BUS_NAME_UNIT BUS_NAME ".Unit" #define BUS_PATH "/org/freedesktop/systemd1" +/*! + * \internal + * \brief Prepare a systemd action + * + * \param[in] op Action to prepare + * + * \return Standard Pacemaker return code + */ +int +services__systemd_prepare(svc_action_t *op) +{ + op->opaque->exec = strdup("systemd-dbus"); + if (op->opaque->exec == NULL) { + return ENOMEM; + } + return pcmk_rc_ok; +} + static inline DBusMessage * systemd_new_method(const char *method) { crm_trace("Calling: %s on " BUS_NAME_MANAGER, method); return dbus_message_new_method_call(BUS_NAME, BUS_PATH, BUS_NAME_MANAGER, method); } /* * Functions to manage a static DBus connection */ static DBusConnection* systemd_proxy = NULL; static inline DBusPendingCall * systemd_send(DBusMessage *msg, void(*done)(DBusPendingCall *pending, void *user_data), void *user_data, int timeout) { return pcmk_dbus_send(msg, systemd_proxy, done, user_data, timeout); } static inline DBusMessage * systemd_send_recv(DBusMessage *msg, DBusError *error, int timeout) { return pcmk_dbus_send_recv(msg, systemd_proxy, error, timeout); } /*! * \internal * \brief Send a method to systemd without arguments, and wait for reply * * \param[in] method Method to send * * \return Systemd reply on success, NULL (and error will be logged) otherwise * * \note The caller must call dbus_message_unref() on the reply after * handling it. */ static DBusMessage * systemd_call_simple_method(const char *method) { DBusMessage *msg = systemd_new_method(method); DBusMessage *reply = NULL; DBusError error; /* Don't call systemd_init() here, because that calls this */ CRM_CHECK(systemd_proxy, return NULL); if (msg == NULL) { crm_err("Could not create message to send %s to systemd", method); return NULL; } dbus_error_init(&error); reply = systemd_send_recv(msg, &error, DBUS_TIMEOUT_USE_DEFAULT); dbus_message_unref(msg); if (dbus_error_is_set(&error)) { crm_err("Could not send %s to systemd: %s (%s)", method, error.message, error.name); dbus_error_free(&error); return NULL; } else if (reply == NULL) { crm_err("Could not send %s to systemd: no reply received", method); return NULL; } return reply; } static gboolean systemd_init(void) { static int need_init = 1; // https://dbus.freedesktop.org/doc/api/html/group__DBusConnection.html if (systemd_proxy && dbus_connection_get_is_connected(systemd_proxy) == FALSE) { crm_warn("Connection to System DBus is closed. Reconnecting..."); pcmk_dbus_disconnect(systemd_proxy); systemd_proxy = NULL; need_init = 1; } if (need_init) { need_init = 0; systemd_proxy = pcmk_dbus_connect(); } if (systemd_proxy == NULL) { return FALSE; } return TRUE; } static inline char * systemd_get_property(const char *unit, const char *name, void (*callback)(const char *name, const char *value, void *userdata), void *userdata, DBusPendingCall **pending, int timeout) { return systemd_proxy? pcmk_dbus_get_property(systemd_proxy, BUS_NAME, unit, BUS_NAME_UNIT, name, callback, userdata, pending, timeout) : NULL; } void systemd_cleanup(void) { if (systemd_proxy) { pcmk_dbus_disconnect(systemd_proxy); systemd_proxy = NULL; } } /* * end of systemd_proxy functions */ /*! * \internal * \brief Check whether a file name represents a manageable systemd unit * * \param[in] name File name to check * * \return Pointer to "dot" before filename extension if so, NULL otherwise */ static const char * systemd_unit_extension(const char *name) { if (name) { const char *dot = strrchr(name, '.'); if (dot && (!strcmp(dot, ".service") || !strcmp(dot, ".socket") || !strcmp(dot, ".mount") || !strcmp(dot, ".timer") || !strcmp(dot, ".path"))) { return dot; } } return NULL; } static char * systemd_service_name(const char *name) { if (name == NULL) { return NULL; } if (systemd_unit_extension(name)) { return strdup(name); } return crm_strdup_printf("%s.service", name); } static void systemd_daemon_reload_complete(DBusPendingCall *pending, void *user_data) { DBusError error; DBusMessage *reply = NULL; unsigned int reload_count = GPOINTER_TO_UINT(user_data); dbus_error_init(&error); if(pending) { reply = dbus_pending_call_steal_reply(pending); } if (pcmk_dbus_find_error(pending, reply, &error)) { crm_err("Could not issue systemd reload %d: %s", reload_count, error.message); dbus_error_free(&error); } else { crm_trace("Reload %d complete", reload_count); } if(pending) { dbus_pending_call_unref(pending); } if(reply) { dbus_message_unref(reply); } } static bool systemd_daemon_reload(int timeout) { static unsigned int reload_count = 0; DBusMessage *msg = systemd_new_method("Reload"); reload_count++; CRM_ASSERT(msg != NULL); systemd_send(msg, systemd_daemon_reload_complete, GUINT_TO_POINTER(reload_count), timeout); dbus_message_unref(msg); return TRUE; } /*! * \internal * \brief Set an action result based on a method error * * \param[in] op Action to set result for * \param[in] error Method error */ static void set_result_from_method_error(svc_action_t *op, const DBusError *error) { services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, "Unable to invoke systemd DBus method"); if (strstr(error->name, "org.freedesktop.systemd1.InvalidName") || strstr(error->name, "org.freedesktop.systemd1.LoadFailed") || strstr(error->name, "org.freedesktop.systemd1.NoSuchUnit")) { if (pcmk__str_eq(op->action, "stop", pcmk__str_casei)) { crm_trace("Masking systemd stop failure (%s) for %s " "because unknown service can be considered stopped", error->name, crm_str(op->rsc)); services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); return; } services__set_result(op, PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_NOT_INSTALLED, "systemd unit not found"); } crm_err("DBus request for %s of systemd unit %s for resource %s failed: %s", op->action, op->agent, crm_str(op->rsc), error->message); } /*! * \internal * \brief Extract unit path from LoadUnit reply, and execute action * * \param[in] reply LoadUnit reply * \param[in] op Action to execute (or NULL to just return path) * * \return DBus object path for specified unit if successful (only valid for * lifetime of \p reply), otherwise NULL */ static const char * execute_after_loadunit(DBusMessage *reply, svc_action_t *op) { const char *path = NULL; DBusError error; /* path here is not used other than as a non-NULL flag to indicate that a * request was indeed sent */ if (pcmk_dbus_find_error((void *) &path, reply, &error)) { if (op != NULL) { set_result_from_method_error(op, &error); } dbus_error_free(&error); } else if (!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, __func__, __LINE__)) { if (op != NULL) { services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, "systemd DBus method had unexpected reply"); crm_err("Could not load systemd unit %s for %s: " "DBus reply has unexpected type", op->agent, op->id); } else { crm_err("Could not load systemd unit: " "DBus reply has unexpected type"); } } else { dbus_message_get_args (reply, NULL, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID); } if (op != NULL) { if (path != NULL) { invoke_unit_by_path(op, path); } else if (!(op->synchronous)) { services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, "No DBus object found for systemd unit"); services__finalize_async_op(op); } } return path; } /*! * \internal * \brief Execute a systemd action after its LoadUnit completes * * \param[in] pending If not NULL, DBus call associated with LoadUnit request * \param[in] user_data Action to execute */ static void loadunit_completed(DBusPendingCall *pending, void *user_data) { DBusMessage *reply = NULL; svc_action_t *op = user_data; crm_trace("LoadUnit result for %s arrived", op->id); // Grab the reply if (pending != NULL) { reply = dbus_pending_call_steal_reply(pending); } // The call is no longer pending CRM_LOG_ASSERT(pending == op->opaque->pending); services_set_op_pending(op, NULL); // Execute the desired action based on the reply execute_after_loadunit(reply, user_data); if (reply != NULL) { dbus_message_unref(reply); } } /*! * \internal * \brief Execute a systemd action, given the unit name * * \param[in] arg_name Unit name (possibly shortened, i.e. without ".service") * \param[in] op Action to execute (if NULL, just get the object path) * \param[out] path If non-NULL and \p op is NULL or synchronous, where to * store DBus object path for specified unit * * \return Standard Pacemaker return code (for NULL \p op, pcmk_rc_ok means unit * was found; for synchronous actions, pcmk_rc_ok means unit was * executed, with the actual result stored in \p op; for asynchronous * actions, pcmk_rc_ok means action was initiated) * \note It is the caller's responsibility to free the return value if non-NULL. */ static int invoke_unit_by_name(const char *arg_name, svc_action_t *op, char **path) { DBusMessage *msg; DBusMessage *reply = NULL; DBusPendingCall *pending = NULL; char *name = NULL; if (!systemd_init()) { if (op != NULL) { services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, "No DBus connection"); } return ENOTCONN; } /* Create a LoadUnit DBus method (equivalent to GetUnit if already loaded), * which makes the unit usable via further DBus methods. * * * * * */ msg = systemd_new_method("LoadUnit"); CRM_ASSERT(msg != NULL); // Add the (expanded) unit name as the argument name = systemd_service_name(arg_name); CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_STRING, &name, DBUS_TYPE_INVALID)); free(name); if ((op == NULL) || op->synchronous) { // For synchronous ops, wait for a reply and extract the result const char *unit = NULL; int rc = pcmk_rc_ok; reply = systemd_send_recv(msg, NULL, (op? op->timeout : DBUS_TIMEOUT_USE_DEFAULT)); dbus_message_unref(msg); unit = execute_after_loadunit(reply, op); if (unit == NULL) { rc = ENOENT; if (path != NULL) { *path = NULL; } } else if (path != NULL) { *path = strdup(unit); if (*path == NULL) { rc = ENOMEM; } } if (reply != NULL) { dbus_message_unref(reply); } return rc; } // For asynchronous ops, initiate the LoadUnit call and return pending = systemd_send(msg, loadunit_completed, op, op->timeout); if (pending == NULL) { services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, "Unable to send DBus message"); dbus_message_unref(msg); return ECOMM; } // LoadUnit was successfully initiated services__set_result(op, PCMK_OCF_UNKNOWN, PCMK_EXEC_PENDING, NULL); services_set_op_pending(op, pending); dbus_message_unref(msg); return pcmk_rc_ok; } /*! * \internal * \brief Compare two strings alphabetically (case-insensitive) * * \param[in] a First string to compare * \param[in] b Second string to compare * * \return 0 if strings are equal, -1 if a < b, 1 if a > b * * \note Usable as a GCompareFunc with g_list_sort(). * NULL is considered less than non-NULL. */ static gint sort_str(gconstpointer a, gconstpointer b) { if (!a && !b) { return 0; } else if (!a) { return -1; } else if (!b) { return 1; } return strcasecmp(a, b); } GList * systemd_unit_listall(void) { int nfiles = 0; GList *units = NULL; DBusMessageIter args; DBusMessageIter unit; DBusMessageIter elem; DBusMessage *reply = NULL; if (systemd_init() == FALSE) { return NULL; } /* " \n" \ " \n" \ " \n" \ */ reply = systemd_call_simple_method("ListUnitFiles"); if (reply == NULL) { return NULL; } if (!dbus_message_iter_init(reply, &args)) { crm_err("Could not list systemd unit files: systemd reply has no arguments"); dbus_message_unref(reply); return NULL; } if (!pcmk_dbus_type_check(reply, &args, DBUS_TYPE_ARRAY, __func__, __LINE__)) { crm_err("Could not list systemd unit files: systemd reply has invalid arguments"); dbus_message_unref(reply); return NULL; } dbus_message_iter_recurse(&args, &unit); for (; dbus_message_iter_get_arg_type(&unit) != DBUS_TYPE_INVALID; dbus_message_iter_next(&unit)) { DBusBasicValue value; const char *match = NULL; char *unit_name = NULL; char *basename = NULL; if(!pcmk_dbus_type_check(reply, &unit, DBUS_TYPE_STRUCT, __func__, __LINE__)) { crm_warn("Skipping systemd reply argument with unexpected type"); continue; } dbus_message_iter_recurse(&unit, &elem); if(!pcmk_dbus_type_check(reply, &elem, DBUS_TYPE_STRING, __func__, __LINE__)) { crm_warn("Skipping systemd reply argument with no string"); continue; } dbus_message_iter_get_basic(&elem, &value); if (value.str == NULL) { crm_debug("ListUnitFiles reply did not provide a string"); continue; } crm_trace("DBus ListUnitFiles listed: %s", value.str); match = systemd_unit_extension(value.str); if (match == NULL) { // This is not a unit file type we know how to manage crm_debug("ListUnitFiles entry '%s' is not supported as resource", value.str); continue; } // ListUnitFiles returns full path names, we just want base name basename = strrchr(value.str, '/'); if (basename) { basename = basename + 1; } else { basename = value.str; } if (!strcmp(match, ".service")) { // Service is the "default" unit type, so strip it unit_name = strndup(basename, match - basename); } else { unit_name = strdup(basename); } nfiles++; units = g_list_prepend(units, unit_name); } dbus_message_unref(reply); crm_trace("Found %d manageable systemd unit files", nfiles); units = g_list_sort(units, sort_str); return units; } gboolean systemd_unit_exists(const char *name) { char *path = NULL; char *state = NULL; /* Note: Makes a blocking dbus calls * Used by resources_find_service_class() when resource class=service */ if ((invoke_unit_by_name(name, NULL, &path) != pcmk_rc_ok) || (path == NULL)) { return FALSE; } /* A successful LoadUnit is not sufficient to determine the unit's * existence; it merely means the LoadUnit request received a reply. * We must make another blocking call to check the LoadState property. */ state = systemd_get_property(path, "LoadState", NULL, NULL, NULL, DBUS_TIMEOUT_USE_DEFAULT); if (pcmk__str_any_of(state, "loaded", "masked", NULL)) { free(state); return TRUE; } free(state); return FALSE; } #define METADATA_FORMAT \ "\n" \ "\n" \ "\n" \ " 1.1\n" \ " \n" \ " %s\n" \ " \n" \ " systemd unit file for %s\n" \ " \n" \ " \n" \ " \n" \ " \n" \ " \n" \ " \n" \ " \n" \ " \n" \ " \n" \ "\n" static char * systemd_unit_metadata(const char *name, int timeout) { char *meta = NULL; char *desc = NULL; char *path = NULL; if (invoke_unit_by_name(name, NULL, &path) == pcmk_rc_ok) { /* TODO: Worth a making blocking call for? Probably not. Possibly if cached. */ desc = systemd_get_property(path, "Description", NULL, NULL, NULL, timeout); } else { desc = crm_strdup_printf("Systemd unit file for %s", name); } meta = crm_strdup_printf(METADATA_FORMAT, name, desc, name); free(desc); free(path); return meta; } /*! * \internal * \brief Determine result of method from reply * * \param[in] reply Reply to start, stop, or restart request * \param[in] op Action that was executed */ static void process_unit_method_reply(DBusMessage *reply, svc_action_t *op) { DBusError error; /* The first use of error here is not used other than as a non-NULL flag to * indicate that a request was indeed sent */ if (pcmk_dbus_find_error((void *) &error, reply, &error)) { set_result_from_method_error(op, &error); dbus_error_free(&error); } else if (!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, __func__, __LINE__)) { crm_warn("DBus request for %s of %s succeeded but " "return type was unexpected", op->action, crm_str(op->rsc)); services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, "systemd DBus method had unexpected reply"); } else { const char *path = NULL; dbus_message_get_args(reply, NULL, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID); crm_debug("DBus request for %s of %s using %s succeeded", op->action, crm_str(op->rsc), path); services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); } } /*! * \internal * \brief Process the completion of an asynchronous unit start, stop, or restart * * \param[in] pending If not NULL, DBus call associated with request * \param[in] user_data Action that was executed */ static void unit_method_complete(DBusPendingCall *pending, void *user_data) { DBusMessage *reply = NULL; svc_action_t *op = user_data; crm_trace("Result for %s arrived", op->id); // Grab the reply if (pending != NULL) { reply = dbus_pending_call_steal_reply(pending); } // The call is no longer pending CRM_LOG_ASSERT(pending == op->opaque->pending); services_set_op_pending(op, NULL); // Determine result and finalize action process_unit_method_reply(reply, op); services__finalize_async_op(op); if (reply != NULL) { dbus_message_unref(reply); } } #define SYSTEMD_OVERRIDE_ROOT "/run/systemd/system/" /* When the cluster manages a systemd resource, we create a unit file override * to order the service "before" pacemaker. The "before" relationship won't * actually be used, since systemd won't ever start the resource -- we're * interested in the reverse shutdown ordering it creates, to ensure that * systemd doesn't stop the resource at shutdown while pacemaker is still * running. * * @TODO Add start timeout */ #define SYSTEMD_OVERRIDE_TEMPLATE \ "[Unit]\n" \ "Description=Cluster Controlled %s\n" \ "Before=pacemaker.service pacemaker_remote.service\n" \ "\n" \ "[Service]\n" \ "Restart=no\n" // Temporarily use rwxr-xr-x umask when opening a file for writing static FILE * create_world_readable(const char *filename) { mode_t orig_umask = umask(S_IWGRP | S_IWOTH); FILE *fp = fopen(filename, "w"); umask(orig_umask); return fp; } static void create_override_dir(const char *agent) { char *override_dir = crm_strdup_printf(SYSTEMD_OVERRIDE_ROOT "/%s.service.d", agent); int rc = pcmk__build_path(override_dir, 0755); if (rc != pcmk_rc_ok) { crm_warn("Could not create systemd override directory %s: %s", override_dir, pcmk_rc_str(rc)); } free(override_dir); } static char * get_override_filename(const char *agent) { return crm_strdup_printf(SYSTEMD_OVERRIDE_ROOT "/%s.service.d/50-pacemaker.conf", agent); } static void systemd_create_override(const char *agent, int timeout) { FILE *file_strm = NULL; char *override_file = get_override_filename(agent); create_override_dir(agent); /* Ensure the override file is world-readable. This is not strictly * necessary, but it avoids a systemd warning in the logs. */ file_strm = create_world_readable(override_file); if (file_strm == NULL) { crm_err("Cannot open systemd override file %s for writing", override_file); } else { char *override = crm_strdup_printf(SYSTEMD_OVERRIDE_TEMPLATE, agent); int rc = fprintf(file_strm, "%s\n", override); free(override); if (rc < 0) { crm_perror(LOG_WARNING, "Cannot write to systemd override file %s", override_file); } fflush(file_strm); fclose(file_strm); systemd_daemon_reload(timeout); } free(override_file); } static void systemd_remove_override(const char *agent, int timeout) { char *override_file = get_override_filename(agent); int rc = unlink(override_file); if (rc < 0) { // Stop may be called when already stopped, which is fine crm_perror(LOG_DEBUG, "Cannot remove systemd override file %s", override_file); } else { systemd_daemon_reload(timeout); } free(override_file); } /*! * \internal * \brief Parse result of systemd status check * * Set a status action's exit status and execution status based on a DBus * property check result, and finalize the action if asynchronous. * * \param[in] name DBus interface name for property that was checked * \param[in] state Property value * \param[in] userdata Status action that check was done for */ static void parse_status_result(const char *name, const char *state, void *userdata) { svc_action_t *op = userdata; crm_trace("Resource %s has %s='%s'", crm_str(op->rsc), name, crm_str(state)); if (pcmk__str_eq(state, "active", pcmk__str_none)) { services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); } else if (pcmk__str_eq(state, "reloading", pcmk__str_none)) { services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); } else if (pcmk__str_eq(state, "activating", pcmk__str_none)) { services__set_result(op, PCMK_OCF_UNKNOWN, PCMK_EXEC_PENDING, NULL); } else if (pcmk__str_eq(state, "deactivating", pcmk__str_none)) { services__set_result(op, PCMK_OCF_UNKNOWN, PCMK_EXEC_PENDING, NULL); } else { services__set_result(op, PCMK_OCF_NOT_RUNNING, PCMK_EXEC_DONE, state); } if (!(op->synchronous)) { services_set_op_pending(op, NULL); services__finalize_async_op(op); } } /*! * \internal * \brief Invoke a systemd unit, given its DBus object path * * \param[in] op Action to execute * \param[in] unit DBus object path of systemd unit to invoke */ static void invoke_unit_by_path(svc_action_t *op, const char *unit) { const char *method = NULL; DBusMessage *msg = NULL; DBusMessage *reply = NULL; if (pcmk__str_any_of(op->action, "monitor", "status", NULL)) { DBusPendingCall *pending = NULL; char *state; state = systemd_get_property(unit, "ActiveState", (op->synchronous? NULL : parse_status_result), op, (op->synchronous? NULL : &pending), op->timeout); if (op->synchronous) { parse_status_result("ActiveState", state, op); free(state); } else if (pending == NULL) { // Could not get ActiveState property services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, "Could not get unit state from DBus"); services__finalize_async_op(op); } else { services_set_op_pending(op, pending); } return; } else if (pcmk__str_eq(op->action, "start", pcmk__str_none)) { method = "StartUnit"; systemd_create_override(op->agent, op->timeout); } else if (pcmk__str_eq(op->action, "stop", pcmk__str_none)) { method = "StopUnit"; systemd_remove_override(op->agent, op->timeout); } else if (pcmk__str_eq(op->action, "restart", pcmk__str_none)) { method = "RestartUnit"; } else { services__set_result(op, PCMK_OCF_UNIMPLEMENT_FEATURE, PCMK_EXEC_ERROR, "Action not implemented for systemd resources"); if (!(op->synchronous)) { services__finalize_async_op(op); } return; } crm_trace("Calling %s for unit path %s named %s", method, unit, crm_str(op->rsc)); msg = systemd_new_method(method); CRM_ASSERT(msg != NULL); /* (ss) */ { const char *replace_s = "replace"; char *name = systemd_service_name(op->agent); CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_STRING, &name, DBUS_TYPE_INVALID)); CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_STRING, &replace_s, DBUS_TYPE_INVALID)); free(name); } if (op->synchronous) { reply = systemd_send_recv(msg, NULL, op->timeout); dbus_message_unref(msg); process_unit_method_reply(reply, op); if (reply != NULL) { dbus_message_unref(reply); } } else { DBusPendingCall *pending = systemd_send(msg, unit_method_complete, op, op->timeout); dbus_message_unref(msg); if (pending == NULL) { services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, "Unable to send DBus message"); services__finalize_async_op(op); } else { services_set_op_pending(op, pending); } } } static gboolean systemd_timeout_callback(gpointer p) { svc_action_t * op = p; op->opaque->timerid = 0; crm_warn("%s operation on systemd unit %s named '%s' timed out", op->action, op->agent, op->rsc); services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_TIMEOUT, "Systemd action did not complete within specified timeout"); services__finalize_async_op(op); return FALSE; } /*! * \internal * \brief Execute a systemd action * * \param[in] 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 already was or is * pending) * * \note If the return value for an asynchronous action is not pcmk_rc_ok, the * caller is responsible for freeing the action. */ int services__execute_systemd(svc_action_t *op) { CRM_ASSERT(op != NULL); if ((op->action == NULL) || (op->agent == NULL)) { services__set_result(op, PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_ERROR_FATAL, "Bug in action caller"); goto done; } if (!systemd_init()) { services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, "No DBus connection"); goto done; } crm_debug("Performing %ssynchronous %s op on systemd unit %s named '%s'", (op->synchronous? "" : "a"), op->action, op->agent, crm_str(op->rsc)); if (pcmk__str_eq(op->action, "meta-data", pcmk__str_casei)) { op->stdout_data = systemd_unit_metadata(op->agent, op->timeout); services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); goto done; } /* invoke_unit_by_name() should always override these values, which are here * just as a fail-safe in case there are any code paths that neglect to */ services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, "Bug in service library"); if (invoke_unit_by_name(op->agent, op, NULL) == pcmk_rc_ok) { op->opaque->timerid = g_timeout_add(op->timeout + 5000, systemd_timeout_callback, op); services_add_inflight_op(op); return pcmk_rc_ok; } done: if (op->synchronous) { return (op->rc == PCMK_OCF_OK)? pcmk_rc_ok : pcmk_rc_error; } else { return services__finalize_async_op(op); } } diff --git a/lib/services/systemd.h b/lib/services/systemd.h index 12ed69f40f..6d5dea9dee 100644 --- a/lib/services/systemd.h +++ b/lib/services/systemd.h @@ -1,24 +1,27 @@ /* * Copyright 2012-2021 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 SYSTEMD__H # define SYSTEMD__H # include # include "crm/services.h" G_GNUC_INTERNAL GList *systemd_unit_listall(void); +G_GNUC_INTERNAL +int services__systemd_prepare(svc_action_t *op); + G_GNUC_INTERNAL int services__execute_systemd(svc_action_t *op); G_GNUC_INTERNAL gboolean systemd_unit_exists(const gchar * name); G_GNUC_INTERNAL void systemd_cleanup(void); #endif /* SYSTEMD__H */