diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 06:53:20 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 06:53:20 +0000 |
commit | e5a812082ae033afb1eed82c0f2df3d0f6bdc93f (patch) | |
tree | a6716c9275b4b413f6c9194798b34b91affb3cc7 /lib/services | |
parent | Initial commit. (diff) | |
download | pacemaker-e5a812082ae033afb1eed82c0f2df3d0f6bdc93f.tar.xz pacemaker-e5a812082ae033afb1eed82c0f2df3d0f6bdc93f.zip |
Adding upstream version 2.1.6.upstream/2.1.6
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/services')
-rw-r--r-- | lib/services/Makefile.am | 43 | ||||
-rw-r--r-- | lib/services/dbus.c | 776 | ||||
-rw-r--r-- | lib/services/pcmk-dbus.h | 45 | ||||
-rw-r--r-- | lib/services/services.c | 1417 | ||||
-rw-r--r-- | lib/services/services_linux.c | 1438 | ||||
-rw-r--r-- | lib/services/services_lsb.c | 341 | ||||
-rw-r--r-- | lib/services/services_lsb.h | 21 | ||||
-rw-r--r-- | lib/services/services_nagios.c | 220 | ||||
-rw-r--r-- | lib/services/services_nagios.h | 28 | ||||
-rw-r--r-- | lib/services/services_ocf.c | 179 | ||||
-rw-r--r-- | lib/services/services_ocf.h | 31 | ||||
-rw-r--r-- | lib/services/services_private.h | 101 | ||||
-rw-r--r-- | lib/services/systemd.c | 1100 | ||||
-rw-r--r-- | lib/services/systemd.h | 30 | ||||
-rw-r--r-- | lib/services/upstart.c | 701 | ||||
-rw-r--r-- | lib/services/upstart.h | 31 |
16 files changed, 6502 insertions, 0 deletions
diff --git a/lib/services/Makefile.am b/lib/services/Makefile.am new file mode 100644 index 0000000..a7e10c9 --- /dev/null +++ b/lib/services/Makefile.am @@ -0,0 +1,43 @@ +# +# 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. +# + +MAINTAINERCLEANFILES = Makefile.in + +AM_CPPFLAGS = -I$(top_srcdir)/include + +lib_LTLIBRARIES = libcrmservice.la +noinst_HEADERS = pcmk-dbus.h upstart.h systemd.h \ + services_lsb.h services_nagios.h \ + services_ocf.h \ + services_private.h + +libcrmservice_la_LDFLAGS = -version-info 31:2:3 +libcrmservice_la_CFLAGS = + +libcrmservice_la_CFLAGS += $(CFLAGS_HARDENED_LIB) +libcrmservice_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB) + +libcrmservice_la_LIBADD = $(top_builddir)/lib/common/libcrmcommon.la $(DBUS_LIBS) + +libcrmservice_la_SOURCES = services.c +libcrmservice_la_SOURCES += services_linux.c +libcrmservice_la_SOURCES += services_lsb.c +libcrmservice_la_SOURCES += services_ocf.c +if BUILD_DBUS +libcrmservice_la_SOURCES += dbus.c +endif +if BUILD_UPSTART +libcrmservice_la_SOURCES += upstart.c +endif +if BUILD_SYSTEMD +libcrmservice_la_SOURCES += systemd.c +endif +if BUILD_NAGIOS +libcrmservice_la_SOURCES += services_nagios.c +endif diff --git a/lib/services/dbus.c b/lib/services/dbus.c new file mode 100644 index 0000000..f052c0a --- /dev/null +++ b/lib/services/dbus.c @@ -0,0 +1,776 @@ +/* + * Copyright 2014-2022 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 <crm_internal.h> +#include <crm/crm.h> +#include <dbus/dbus.h> +#include <pcmk-dbus.h> + +/* + * DBus message dispatch + */ + +// List of DBus connections (DBusConnection*) with messages available +static GList *conn_dispatches = NULL; + +/*! + * \internal + * \brief Save an indication that DBus messages need dispatching + * + * \param[in] connection DBus connection with messages to dispatch + * \param[in] new_status Dispatch status as reported by DBus library + * \param[in] data Ignored + * + * \note This is suitable to be used as a DBus status dispatch function. + * As mentioned in the DBus documentation, dbus_connection_dispatch() must + * not be called from within this function, and any re-entrancy is a bad + * idea. Instead, this should just flag the main loop that messages need + * to be dispatched. + */ +static void +update_dispatch_status(DBusConnection *connection, + DBusDispatchStatus new_status, void *data) +{ + if (new_status == DBUS_DISPATCH_DATA_REMAINS) { + crm_trace("DBus connection has messages available for dispatch"); + conn_dispatches = g_list_prepend(conn_dispatches, connection); + } else { + crm_trace("DBus connection has no messages available for dispatch " + "(status %d)", new_status); + } +} + +/*! + * \internal + * \brief Dispatch available messages on all DBus connections + */ +static void +dispatch_messages(void) +{ + for (GList *gIter = conn_dispatches; gIter != NULL; gIter = gIter->next) { + DBusConnection *connection = gIter->data; + + while (dbus_connection_get_dispatch_status(connection) + == DBUS_DISPATCH_DATA_REMAINS) { + crm_trace("Dispatching available messages on DBus connection"); + dbus_connection_dispatch(connection); + } + } + g_list_free(conn_dispatches); + conn_dispatches = NULL; +} + + +/* + * DBus file descriptor watches + * + * The DBus library allows the caller to register functions for the library to + * use for file descriptor notifications via a main loop. + */ + +/* Copied from dbus-watch.c */ +static const char* +dbus_watch_flags_to_string(int flags) +{ + const char *watch_type; + + if ((flags & DBUS_WATCH_READABLE) && (flags & DBUS_WATCH_WRITABLE)) { + watch_type = "read/write"; + } else if (flags & DBUS_WATCH_READABLE) { + watch_type = "read"; + } else if (flags & DBUS_WATCH_WRITABLE) { + watch_type = "write"; + } else { + watch_type = "neither read nor write"; + } + return watch_type; +} + +/*! + * \internal + * \brief Dispatch data available on a DBus file descriptor watch + * + * \param[in,out] userdata Pointer to the DBus watch + * + * \return Always 0 + * \note This is suitable for use as a dispatch function in + * struct mainloop_fd_callbacks (which means that a negative return value + * would indicate the file descriptor is no longer required). + */ +static int +dispatch_fd_data(gpointer userdata) +{ + bool oom = FALSE; + DBusWatch *watch = userdata; + int flags = dbus_watch_get_flags(watch); + bool enabled = dbus_watch_get_enabled (watch); + + crm_trace("Dispatching DBus watch for file descriptor %d " + "with flags %#x (%s)", + dbus_watch_get_unix_fd(watch), flags, + dbus_watch_flags_to_string(flags)); + + if (enabled && (flags & (DBUS_WATCH_READABLE|DBUS_WATCH_WRITABLE))) { + oom = !dbus_watch_handle(watch, flags); + + } else if (enabled) { + oom = !dbus_watch_handle(watch, DBUS_WATCH_ERROR); + } + + if (flags != dbus_watch_get_flags(watch)) { + flags = dbus_watch_get_flags(watch); + crm_trace("Dispatched DBus file descriptor watch: now %#x (%s)", + flags, dbus_watch_flags_to_string(flags)); + } + + if (oom) { + crm_crit("Could not dispatch DBus file descriptor data: Out of memory"); + } else { + dispatch_messages(); + } + return 0; +} + +static void +watch_fd_closed(gpointer userdata) +{ + crm_trace("DBus watch for file descriptor %d is now closed", + dbus_watch_get_unix_fd((DBusWatch *) userdata)); +} + +static struct mainloop_fd_callbacks pcmk_dbus_cb = { + .dispatch = dispatch_fd_data, + .destroy = watch_fd_closed, +}; + +static dbus_bool_t +add_dbus_watch(DBusWatch *watch, void *data) +{ + int fd = dbus_watch_get_unix_fd(watch); + + mainloop_io_t *client = mainloop_add_fd("dbus", G_PRIORITY_DEFAULT, fd, + watch, &pcmk_dbus_cb); + + crm_trace("Added DBus watch for file descriptor %d", fd); + dbus_watch_set_data(watch, client, NULL); + return TRUE; +} + +static void +toggle_dbus_watch(DBusWatch *watch, void *data) +{ + // @TODO Should this do something more? + crm_debug("DBus watch for file descriptor %d is now %s", + dbus_watch_get_unix_fd(watch), + (dbus_watch_get_enabled(watch)? "enabled" : "disabled")); +} + +static void +remove_dbus_watch(DBusWatch *watch, void *data) +{ + crm_trace("Removed DBus watch for file descriptor %d", + dbus_watch_get_unix_fd(watch)); + mainloop_del_fd((mainloop_io_t *) dbus_watch_get_data(watch)); +} + +static void +register_watch_functions(DBusConnection *connection) +{ + dbus_connection_set_watch_functions(connection, add_dbus_watch, + remove_dbus_watch, + toggle_dbus_watch, NULL, NULL); +} + +/* + * DBus main loop timeouts + * + * The DBus library allows the caller to register functions for the library to + * use for managing timers via a main loop. + */ + +static gboolean +timer_popped(gpointer data) +{ + crm_debug("%dms DBus timer expired", + dbus_timeout_get_interval((DBusTimeout *) data)); + dbus_timeout_handle(data); + return FALSE; +} + +static dbus_bool_t +add_dbus_timer(DBusTimeout *timeout, void *data) +{ + int interval_ms = dbus_timeout_get_interval(timeout); + guint id = g_timeout_add(interval_ms, timer_popped, timeout); + + if (id) { + dbus_timeout_set_data(timeout, GUINT_TO_POINTER(id), NULL); + } + crm_trace("Added %dms DBus timer", interval_ms); + return TRUE; +} + +static void +remove_dbus_timer(DBusTimeout *timeout, void *data) +{ + void *vid = dbus_timeout_get_data(timeout); + guint id = GPOINTER_TO_UINT(vid); + + crm_trace("Removing %dms DBus timer", dbus_timeout_get_interval(timeout)); + if (id) { + g_source_remove(id); + dbus_timeout_set_data(timeout, 0, NULL); + } +} + +static void +toggle_dbus_timer(DBusTimeout *timeout, void *data) +{ + bool enabled = dbus_timeout_get_enabled(timeout); + + crm_trace("Toggling %dms DBus timer %s", + dbus_timeout_get_interval(timeout), (enabled? "off": "on")); + if (enabled) { + add_dbus_timer(timeout, data); + } else { + remove_dbus_timer(timeout, data); + } +} + +static void +register_timer_functions(DBusConnection *connection) +{ + dbus_connection_set_timeout_functions(connection, add_dbus_timer, + remove_dbus_timer, + toggle_dbus_timer, NULL, NULL); +} + +/* + * General DBus utilities + */ + +DBusConnection * +pcmk_dbus_connect(void) +{ + DBusError err; + DBusConnection *connection; + + dbus_error_init(&err); + connection = dbus_bus_get(DBUS_BUS_SYSTEM, &err); + if (dbus_error_is_set(&err)) { + crm_err("Could not connect to DBus: %s", err.message); + dbus_error_free(&err); + return NULL; + } + if (connection == NULL) { + return NULL; + } + + /* Tell libdbus not to exit the process when a disconnect happens. This + * defaults to FALSE but is toggled on by the dbus_bus_get() call above. + */ + dbus_connection_set_exit_on_disconnect(connection, FALSE); + + // Set custom handlers for various situations + register_timer_functions(connection); + register_watch_functions(connection); + dbus_connection_set_dispatch_status_function(connection, + update_dispatch_status, + NULL, NULL); + + // Call the dispatch function to check for any messages waiting already + update_dispatch_status(connection, + dbus_connection_get_dispatch_status(connection), + NULL); + return connection; +} + +void +pcmk_dbus_disconnect(DBusConnection *connection) +{ + /* Per the DBus documentation, connections created with + * dbus_connection_open() are owned by libdbus and should never be closed. + * + * @TODO Should we call dbus_connection_unref() here? + */ + return; +} + +// Custom DBus error names to use +#define ERR_NO_REQUEST "org.clusterlabs.pacemaker.NoRequest" +#define ERR_NO_REPLY "org.clusterlabs.pacemaker.NoReply" +#define ERR_INVALID_REPLY "org.clusterlabs.pacemaker.InvalidReply" +#define ERR_INVALID_REPLY_METHOD "org.clusterlabs.pacemaker.InvalidReply.Method" +#define ERR_INVALID_REPLY_SIGNAL "org.clusterlabs.pacemaker.InvalidReply.Signal" +#define ERR_INVALID_REPLY_TYPE "org.clusterlabs.pacemaker.InvalidReply.Type" +#define ERR_SEND_FAILED "org.clusterlabs.pacemaker.SendFailed" + +/*! + * \internal + * \brief Check whether a DBus reply indicates an error occurred + * + * \param[in] pending If non-NULL, indicates that a DBus request was sent + * \param[in] reply Reply received from DBus + * \param[out] ret If non-NULL, will be set to DBus error, if any + * + * \return TRUE if an error was found, FALSE otherwise + * + * \note Following the DBus API convention, a TRUE return is exactly equivalent + * to ret being set. If ret is provided and this function returns TRUE, + * the caller is responsible for calling dbus_error_free() on ret when + * done using it. + */ +bool +pcmk_dbus_find_error(const DBusPendingCall *pending, DBusMessage *reply, + DBusError *ret) +{ + DBusError error; + + dbus_error_init(&error); + + if (pending == NULL) { + dbus_set_error_const(&error, ERR_NO_REQUEST, "No request sent"); + + } else if (reply == NULL) { + dbus_set_error_const(&error, ERR_NO_REPLY, "No reply"); + + } else { + DBusMessageIter args; + int dtype = dbus_message_get_type(reply); + + switch (dtype) { + case DBUS_MESSAGE_TYPE_METHOD_RETURN: + { + char *sig = NULL; + + dbus_message_iter_init(reply, &args); + crm_trace("Received DBus reply with argument type '%s'", + (sig = dbus_message_iter_get_signature(&args))); + if (sig != NULL) { + dbus_free(sig); + } + } + break; + case DBUS_MESSAGE_TYPE_INVALID: + dbus_set_error_const(&error, ERR_INVALID_REPLY, + "Invalid reply"); + break; + case DBUS_MESSAGE_TYPE_METHOD_CALL: + dbus_set_error_const(&error, ERR_INVALID_REPLY_METHOD, + "Invalid reply (method call)"); + break; + case DBUS_MESSAGE_TYPE_SIGNAL: + dbus_set_error_const(&error, ERR_INVALID_REPLY_SIGNAL, + "Invalid reply (signal)"); + break; + case DBUS_MESSAGE_TYPE_ERROR: + dbus_set_error_from_message(&error, reply); + break; + default: + dbus_set_error(&error, ERR_INVALID_REPLY_TYPE, + "Unknown reply type %d", dtype); + } + } + + if (dbus_error_is_set(&error)) { + crm_trace("DBus reply indicated error '%s' (%s)", + error.name, error.message); + if (ret) { + dbus_error_init(ret); + dbus_move_error(&error, ret); + } else { + dbus_error_free(&error); + } + return TRUE; + } + + return FALSE; +} + +/*! + * \internal + * \brief Send a DBus request and wait for the reply + * + * \param[in,out] msg DBus request to send + * \param[in,out] connection DBus connection to use + * \param[out] error If non-NULL, will be set to error, if any + * \param[in] timeout Timeout to use for request + * + * \return DBus reply + * + * \note If error is non-NULL, it is initialized, so the caller may always use + * dbus_error_is_set() to determine whether an error occurred; the caller + * is responsible for calling dbus_error_free() in this case. + */ +DBusMessage * +pcmk_dbus_send_recv(DBusMessage *msg, DBusConnection *connection, + DBusError *error, int timeout) +{ + const char *method = NULL; + DBusMessage *reply = NULL; + DBusPendingCall* pending = NULL; + + CRM_ASSERT(dbus_message_get_type (msg) == DBUS_MESSAGE_TYPE_METHOD_CALL); + method = dbus_message_get_member (msg); + + /* Ensure caller can reliably check whether error is set */ + if (error) { + dbus_error_init(error); + } + + if (timeout <= 0) { + /* DBUS_TIMEOUT_USE_DEFAULT (-1) tells DBus to use a sane default */ + timeout = DBUS_TIMEOUT_USE_DEFAULT; + } + + // send message and get a handle for a reply + if (!dbus_connection_send_with_reply(connection, msg, &pending, timeout)) { + if (error) { + dbus_set_error(error, ERR_SEND_FAILED, + "Could not queue DBus '%s' request", method); + } + return NULL; + } + + dbus_connection_flush(connection); + + if (pending) { + /* block until we receive a reply */ + dbus_pending_call_block(pending); + + /* get the reply message */ + reply = dbus_pending_call_steal_reply(pending); + } + + (void) pcmk_dbus_find_error(pending, reply, error); + + if (pending) { + /* free the pending message handle */ + dbus_pending_call_unref(pending); + } + + return reply; +} + +/*! + * \internal + * \brief Send a DBus message with a callback for the reply + * + * \param[in,out] msg DBus message to send + * \param[in,out] connection DBus connection to send on + * \param[in] done Function to call when pending call completes + * \param[in] user_data Data to pass to done callback + * + * \return Handle for reply on success, NULL on error + * \note The caller can assume that the done callback is called always and + * only when the return value is non-NULL. (This allows the caller to + * know where it should free dynamically allocated user_data.) + */ +DBusPendingCall * +pcmk_dbus_send(DBusMessage *msg, DBusConnection *connection, + void (*done)(DBusPendingCall *pending, void *user_data), + void *user_data, int timeout) +{ + const char *method = NULL; + DBusPendingCall* pending = NULL; + + CRM_ASSERT(done); + CRM_ASSERT(dbus_message_get_type(msg) == DBUS_MESSAGE_TYPE_METHOD_CALL); + method = dbus_message_get_member(msg); + + if (timeout <= 0) { + /* DBUS_TIMEOUT_USE_DEFAULT (-1) tells DBus to use a sane default */ + timeout = DBUS_TIMEOUT_USE_DEFAULT; + } + + // send message and get a handle for a reply + if (!dbus_connection_send_with_reply(connection, msg, &pending, timeout)) { + crm_err("Could not send DBus %s message: failed", method); + return NULL; + + } else if (pending == NULL) { + crm_err("Could not send DBus %s message: connection may be closed", + method); + return NULL; + } + + if (dbus_pending_call_get_completed(pending)) { + crm_info("DBus %s message completed too soon", method); + /* Calling done() directly in this case instead of setting notify below + * breaks things + */ + } + if (!dbus_pending_call_set_notify(pending, done, user_data, NULL)) { + return NULL; + } + return pending; +} + +bool +pcmk_dbus_type_check(DBusMessage *msg, DBusMessageIter *field, int expected, + const char *function, int line) +{ + int dtype = 0; + DBusMessageIter lfield; + + if (field == NULL) { + if (dbus_message_iter_init(msg, &lfield)) { + field = &lfield; + } + } + + if (field == NULL) { + do_crm_log_alias(LOG_INFO, __FILE__, function, line, + "DBus reply has empty parameter list (expected '%c')", + expected); + return FALSE; + } + + dtype = dbus_message_iter_get_arg_type(field); + + if (dtype != expected) { + DBusMessageIter args; + char *sig; + + dbus_message_iter_init(msg, &args); + sig = dbus_message_iter_get_signature(&args); + do_crm_log_alias(LOG_INFO, __FILE__, function, line, + "DBus reply has unexpected type " + "(expected '%c' not '%c' in '%s')", + expected, dtype, sig); + dbus_free(sig); + return FALSE; + } + + return TRUE; +} + + +/* + * Property queries + */ + +/* DBus APIs often provide queryable properties that use this standard + * interface. See: + * https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-properties + */ +#define BUS_PROPERTY_IFACE "org.freedesktop.DBus.Properties" + +// Callback prototype for when a DBus property query result is received +typedef void (*property_callback_func)(const char *name, // Property name + const char *value, // Property value + void *userdata); // Caller-provided data + +// Data needed by DBus property queries +struct property_query { + char *name; // Property name being queried + char *target; // Name of DBus bus that query should be sent to + char *object; // DBus object path for object with the property + void *userdata; // Caller-provided data to supply to callback + property_callback_func callback; // Function to call when result is received +}; + +static void +free_property_query(struct property_query *data) +{ + free(data->target); + free(data->object); + free(data->name); + free(data); +} + +static char * +handle_query_result(DBusMessage *reply, struct property_query *data) +{ + DBusError error; + char *output = NULL; + DBusMessageIter args; + DBusMessageIter variant_iter; + DBusBasicValue value; + + // First, check if the reply contains an error + if (pcmk_dbus_find_error((void*)&error, reply, &error)) { + crm_err("DBus query for %s property '%s' failed: %s", + data->object, data->name, error.message); + dbus_error_free(&error); + goto cleanup; + } + + // The lone output argument should be a DBus variant type + dbus_message_iter_init(reply, &args); + if (!pcmk_dbus_type_check(reply, &args, DBUS_TYPE_VARIANT, + __func__, __LINE__)) { + crm_err("DBus query for %s property '%s' failed: Unexpected reply type", + data->object, data->name); + goto cleanup; + } + + // The variant should be a string + dbus_message_iter_recurse(&args, &variant_iter); + if (!pcmk_dbus_type_check(reply, &variant_iter, DBUS_TYPE_STRING, + __func__, __LINE__)) { + crm_err("DBus query for %s property '%s' failed: " + "Unexpected variant type", data->object, data->name); + goto cleanup; + } + dbus_message_iter_get_basic(&variant_iter, &value); + + // There should be no more arguments (in variant or reply) + dbus_message_iter_next(&variant_iter); + if (dbus_message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_INVALID) { + crm_err("DBus query for %s property '%s' failed: " + "Too many arguments in reply", + data->object, data->name); + goto cleanup; + } + dbus_message_iter_next(&args); + if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_INVALID) { + crm_err("DBus query for %s property '%s' failed: " + "Too many arguments in reply", data->object, data->name); + goto cleanup; + } + + crm_trace("DBus query result for %s: %s='%s'", + data->object, data->name, (value.str? value.str : "")); + + if (data->callback) { // Query was asynchronous + data->callback(data->name, (value.str? value.str : ""), data->userdata); + + } else { // Query was synchronous + output = strdup(value.str? value.str : ""); + } + + cleanup: + free_property_query(data); + return output; +} + +static void +async_query_result_cb(DBusPendingCall *pending, void *user_data) +{ + DBusMessage *reply = NULL; + char *value = NULL; + + if (pending) { + reply = dbus_pending_call_steal_reply(pending); + } + + value = handle_query_result(reply, user_data); + free(value); + + if (reply) { + dbus_message_unref(reply); + } +} + +/*! + * \internal + * \brief Query a property on a DBus object + * + * \param[in,out] connection An active connection to DBus + * \param[in] target DBus name that the query should be sent to + * \param[in] obj DBus object path for object with the property + * \param[in] iface DBus interface for property to query + * \param[in] name Name of property to query + * \param[in] callback If not NULL, perform query asynchronously and call + * this function when query completes + * \param[in,out] userdata Caller-provided data to provide to \p callback + * \param[out] pending If \p callback is not NULL, this will be set to + * handle for the reply (or NULL on error) + * \param[in] timeout Abort query if it takes longer than this (ms) + * + * \return NULL if \p callback is non-NULL (i.e. asynchronous), otherwise a + * newly allocated string with property value + * \note It is the caller's responsibility to free the result with free(). + */ +char * +pcmk_dbus_get_property(DBusConnection *connection, const char *target, + const char *obj, const gchar * iface, const char *name, + property_callback_func callback, void *userdata, + DBusPendingCall **pending, int timeout) +{ + DBusMessage *msg; + char *output = NULL; + struct property_query *query_data = NULL; + + CRM_CHECK((connection != NULL) && (target != NULL) && (obj != NULL) + && (iface != NULL) && (name != NULL), return NULL); + + crm_trace("Querying DBus %s for %s property '%s'", + target, obj, name); + + // Create a new message to use to invoke method + msg = dbus_message_new_method_call(target, obj, BUS_PROPERTY_IFACE, "Get"); + if (msg == NULL) { + crm_err("DBus query for %s property '%s' failed: " + "Unable to create message", obj, name); + return NULL; + } + + // Add the interface name and property name as message arguments + if (!dbus_message_append_args(msg, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) { + crm_err("DBus query for %s property '%s' failed: " + "Could not append arguments", obj, name); + dbus_message_unref(msg); + return NULL; + } + + query_data = malloc(sizeof(struct property_query)); + if (query_data == NULL) { + crm_crit("DBus query for %s property '%s' failed: Out of memory", + obj, name); + dbus_message_unref(msg); + return NULL; + } + + query_data->target = strdup(target); + query_data->object = strdup(obj); + query_data->callback = callback; + query_data->userdata = userdata; + query_data->name = strdup(name); + CRM_CHECK((query_data->target != NULL) + && (query_data->object != NULL) + && (query_data->name != NULL), + free_property_query(query_data); + dbus_message_unref(msg); + return NULL); + + if (query_data->callback) { // Asynchronous + DBusPendingCall *local_pending; + + local_pending = pcmk_dbus_send(msg, connection, async_query_result_cb, + query_data, timeout); + if (local_pending == NULL) { + // async_query_result_cb() was not called in this case + free_property_query(query_data); + query_data = NULL; + } + + if (pending) { + *pending = local_pending; + } + + } else { // Synchronous + DBusMessage *reply = pcmk_dbus_send_recv(msg, connection, NULL, + timeout); + + output = handle_query_result(reply, query_data); + + if (reply) { + dbus_message_unref(reply); + } + } + + dbus_message_unref(msg); + + return output; +} diff --git a/lib/services/pcmk-dbus.h b/lib/services/pcmk-dbus.h new file mode 100644 index 0000000..a9d0cbc --- /dev/null +++ b/lib/services/pcmk-dbus.h @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef PCMK_DBUS__H +# define PCMK_DBUS__H + +# include <dbus/dbus.h> + +# ifndef DBUS_TIMEOUT_USE_DEFAULT +# define DBUS_TIMEOUT_USE_DEFAULT -1 +# endif + +G_GNUC_INTERNAL +DBusConnection *pcmk_dbus_connect(void); + +G_GNUC_INTERNAL +void pcmk_dbus_disconnect(DBusConnection *connection); + +G_GNUC_INTERNAL +DBusPendingCall *pcmk_dbus_send(DBusMessage *msg, DBusConnection *connection, + void(*done)(DBusPendingCall *pending, void *user_data), void *user_data, int timeout); + +G_GNUC_INTERNAL +DBusMessage *pcmk_dbus_send_recv(DBusMessage *msg, DBusConnection *connection, DBusError *error, int timeout); + +G_GNUC_INTERNAL +bool pcmk_dbus_type_check(DBusMessage *msg, DBusMessageIter *field, int expected, const char *function, int line); + +G_GNUC_INTERNAL +char *pcmk_dbus_get_property( + DBusConnection *connection, const char *target, const char *obj, const gchar * iface, const char *name, + void (*callback)(const char *name, const char *value, void *userdata), void *userdata, + DBusPendingCall **pending, int timeout); + +G_GNUC_INTERNAL +bool pcmk_dbus_find_error(const DBusPendingCall *pending, DBusMessage *reply, + DBusError *error); + +#endif /* PCMK_DBUS__H */ diff --git a/lib/services/services.c b/lib/services/services.c new file mode 100644 index 0000000..b60d8bd --- /dev/null +++ b/lib/services/services.c @@ -0,0 +1,1417 @@ +/* + * Copyright 2010-2022 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 <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <sys/types.h> +#include <sys/stat.h> +#include <stdio.h> +#include <errno.h> +#include <unistd.h> +#include <dirent.h> +#include <fcntl.h> + +#include <crm/crm.h> +#include <crm/common/mainloop.h> +#include <crm/services.h> +#include <crm/services_internal.h> +#include <crm/stonith-ng.h> +#include <crm/msg_xml.h> +#include "services_private.h" +#include "services_ocf.h" +#include "services_lsb.h" + +#if SUPPORT_UPSTART +# include <upstart.h> +#endif + +#if SUPPORT_SYSTEMD +# include <systemd.h> +#endif + +#if SUPPORT_NAGIOS +# include <services_nagios.h> +#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(const 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) { + rc = services__systemd_prepare(op); +#endif +#if SUPPORT_UPSTART + } else if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_UPSTART) == 0) { + rc = services__upstart_prepare(op); +#endif +#if SUPPORT_NAGIOS + } else if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_NAGIOS) == 0) { + rc = services__nagios_prepare(op); +#endif + } else { + crm_info("Unknown resource standard: %s", op->standard); + rc = ENOENT; + } + + if (rc != pcmk_rc_ok) { + crm_info("Cannot prepare %s operation for %s: %s", + action, name, strerror(rc)); + services__handle_exec_error(op, rc); + } + return op; +} + +svc_action_t * +resources_action_create(const char *name, const char *standard, + const char *provider, const char *agent, + const char *action, guint interval_ms, int timeout, + GHashTable *params, enum svc_action_flags flags) +{ + svc_action_t *op = services__create_resource_action(name, standard, + provider, agent, action, interval_ms, timeout, + params, flags); + if (op == NULL || op->rc != 0) { + services_action_free(op); + return NULL; + } else { + // Preserve public API backward compatibility + op->rc = PCMK_OCF_OK; + op->status = PCMK_EXEC_DONE; + + return op; + } +} + +svc_action_t * +services_action_create_generic(const char *exec, const char *args[]) +{ + svc_action_t *op = new_action(); + + 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->id = strdup(id); + action->standard = strdup(PCMK_RESOURCE_CLASS_ALERT); + CRM_ASSERT((action->id != NULL) && (action->standard != NULL)); + + action->timeout = timeout; + action->params = params; + action->sequence = sequence; + action->cb_data = cb_data; + return action; +} + +/*! + * \brief Set the user and group that an action will execute as + * + * \param[in,out] op Action to modify + * \param[in] user Name of user to execute action as + * \param[in] group Name of group to execute action as + * + * \return pcmk_ok on success, -errno otherwise + * + * \note This will have no effect unless the process executing the action runs + * as root, and the action is not a systemd 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,out] action Action to execute + * \param[in] cb Function to call when action completes + * + * \return TRUE if the library will free action, FALSE otherwise + * + * \note If this function returns FALSE, it is the caller's responsibility to + * free the action with services_action_free(). However, unless someone + * intentionally creates a recurring alert action, this will never return + * FALSE. + */ +gboolean +services_alert_async(svc_action_t *action, void (*cb)(svc_action_t *op)) +{ + action->synchronous = false; + action->opaque->callback = cb; + return services__execute_file(action) == pcmk_rc_ok; +} + +#if HAVE_DBUS +/*! + * \internal + * \brief Update operation's pending DBus call, unreferencing old one if needed + * + * \param[in,out] op Operation to modify + * \param[in] pending Pending call to set + */ +void +services_set_op_pending(svc_action_t *op, DBusPendingCall *pending) +{ + if (op->opaque->pending && (op->opaque->pending != pending)) { + if (pending) { + crm_info("Lost pending %s DBus call (%p)", op->id, op->opaque->pending); + } else { + crm_trace("Done with pending %s DBus call (%p)", op->id, op->opaque->pending); + } + dbus_pending_call_unref(op->opaque->pending); + } + op->opaque->pending = pending; + if (pending) { + crm_trace("Updated pending %s DBus call (%p)", op->id, pending); + } else { + crm_trace("Cleared pending %s DBus call", op->id); + } +} +#endif + +void +services_action_cleanup(svc_action_t * op) +{ + if ((op == NULL) || (op->opaque == NULL)) { + return; + } + +#if HAVE_DBUS + if(op->opaque->timerid != 0) { + crm_trace("Removing timer for call %s to %s", op->action, op->rsc); + g_source_remove(op->opaque->timerid); + op->opaque->timerid = 0; + } + + if(op->opaque->pending) { + if (dbus_pending_call_get_completed(op->opaque->pending)) { + // This should never be the case + crm_warn("Result of %s op %s was unhandled", + op->standard, op->id); + } else { + crm_debug("Will ignore any result of canceled %s op %s", + op->standard, op->id); + } + dbus_pending_call_cancel(op->opaque->pending); + services_set_op_pending(op, NULL); + } +#endif + + if (op->opaque->stderr_gsource) { + mainloop_del_fd(op->opaque->stderr_gsource); + op->opaque->stderr_gsource = NULL; + } + + if (op->opaque->stdout_gsource) { + mainloop_del_fd(op->opaque->stdout_gsource); + op->opaque->stdout_gsource = NULL; + } +} + +/*! + * \internal + * \brief Map an actual resource action result to a standard OCF result + * + * \param[in] standard Agent standard (must not be "service") + * \param[in] action Action that result is for + * \param[in] exit_status Actual agent exit status + * + * \return Standard OCF result + */ +enum ocf_exitcode +services_result2ocf(const char *standard, const char *action, int exit_status) +{ + if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_casei)) { + return services__ocf2ocf(exit_status); + +#if SUPPORT_SYSTEMD + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_SYSTEMD, + pcmk__str_casei)) { + return services__systemd2ocf(exit_status); +#endif + +#if SUPPORT_UPSTART + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_UPSTART, + pcmk__str_casei)) { + return services__upstart2ocf(exit_status); +#endif + +#if SUPPORT_NAGIOS + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_NAGIOS, + pcmk__str_casei)) { + return services__nagios2ocf(exit_status); +#endif + + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_LSB, + pcmk__str_casei)) { + return services__lsb2ocf(action, exit_status); + + } else { + crm_warn("Treating result from unknown standard '%s' as OCF", + ((standard == NULL)? "unspecified" : standard)); + return services__ocf2ocf(exit_status); + } +} + +void +services_action_free(svc_action_t * op) +{ + unsigned int i; + + if (op == NULL) { + return; + } + + /* The operation should be removed from all tracking lists by this point. + * If it's not, we have a bug somewhere, so bail. That may lead to a + * memory leak, but it's better than a use-after-free segmentation fault. + */ + CRM_CHECK(g_list_find(inflight_ops, op) == NULL, return); + CRM_CHECK(g_list_find(blocked_ops, op) == NULL, return); + CRM_CHECK((recurring_actions == NULL) + || (g_hash_table_lookup(recurring_actions, op->id) == NULL), + return); + + services_action_cleanup(op); + + if (op->opaque->repeat_timer) { + g_source_remove(op->opaque->repeat_timer); + op->opaque->repeat_timer = 0; + } + + free(op->id); + free(op->opaque->exec); + + for (i = 0; i < PCMK__NELEM(op->opaque->args); i++) { + free(op->opaque->args[i]); + } + + free(op->opaque->exit_reason); + free(op->opaque); + free(op->rsc); + free(op->action); + + free(op->standard); + free(op->agent); + free(op->provider); + + free(op->stdout_data); + free(op->stderr_data); + + if (op->params) { + g_hash_table_destroy(op->params); + op->params = NULL; + } + + free(op); +} + +gboolean +cancel_recurring_action(svc_action_t * op) +{ + crm_info("Cancelling %s operation %s", op->standard, op->id); + + if (recurring_actions) { + g_hash_table_remove(recurring_actions, op->id); + } + + if (op->opaque->repeat_timer) { + g_source_remove(op->opaque->repeat_timer); + op->opaque->repeat_timer = 0; + } + + return TRUE; +} + +/*! + * \brief Cancel a recurring action + * + * \param[in] name Name of resource that operation is for + * \param[in] action Name of operation to cancel + * \param[in] interval_ms Interval of operation to cancel + * + * \return TRUE if action was successfully cancelled, FALSE otherwise + */ +gboolean +services_action_cancel(const char *name, const char *action, guint interval_ms) +{ + gboolean cancelled = FALSE; + char *id = pcmk__op_key(name, action, interval_ms); + svc_action_t *op = NULL; + + /* We can only cancel a recurring action */ + init_recurring_actions(); + op = g_hash_table_lookup(recurring_actions, id); + if (op == NULL) { + goto done; + } + + // Tell services__finalize_async_op() not to reschedule the operation + op->cancel = TRUE; + + /* Stop tracking it as a recurring operation, and stop its repeat timer */ + cancel_recurring_action(op); + + /* If the op has a PID, it's an in-flight child process, so kill it. + * + * Whether the kill succeeds or fails, the main loop will send the op to + * async_action_complete() (and thus services__finalize_async_op()) when the + * process goes away. + */ + if (op->pid != 0) { + crm_info("Terminating in-flight op %s[%d] early because it was cancelled", + id, op->pid); + cancelled = mainloop_child_kill(op->pid); + if (cancelled == FALSE) { + crm_err("Termination of %s[%d] failed", id, op->pid); + } + goto done; + } + +#if HAVE_DBUS + // In-flight systemd 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_cancelled(op); + if (op->opaque->callback) { + op->opaque->callback(op); + } + + blocked_ops = g_list_remove(blocked_ops, op); + services_action_free(op); + cancelled = TRUE; + // @TODO Initiate handle_blocked_ops() asynchronously + +done: + free(id); + return cancelled; +} + +gboolean +services_action_kick(const char *name, const char *action, guint interval_ms) +{ + svc_action_t * op = NULL; + char *id = pcmk__op_key(name, action, interval_ms); + + init_recurring_actions(); + op = g_hash_table_lookup(recurring_actions, id); + free(id); + + if (op == NULL) { + return FALSE; + } + + + if (op->pid || inflight_systemd_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,out] op Operation to add + * + * \return TRUE if duplicate found (and reschedule), FALSE otherwise + */ +static gboolean +handle_duplicate_recurring(svc_action_t *op) +{ + svc_action_t * dup = NULL; + + /* check for duplicates */ + dup = g_hash_table_lookup(recurring_actions, op->id); + + if (dup && (dup != op)) { + /* update user data */ + if (op->opaque->callback) { + dup->opaque->callback = op->opaque->callback; + dup->cb_data = op->cb_data; + op->cb_data = NULL; + } + /* immediately execute the next interval */ + if (dup->pid != 0) { + if (op->opaque->repeat_timer) { + g_source_remove(op->opaque->repeat_timer); + op->opaque->repeat_timer = 0; + } + recurring_action_timer(dup); + } + /* free the duplicate */ + services_action_free(op); + return TRUE; + } + + return FALSE; +} + +/*! + * \internal + * \brief Execute an action appropriately according to its standard + * + * \param[in,out] op Action to execute + * + * \return Standard Pacemaker return code + * \retval EBUSY Recurring operation could not be initiated + * \retval pcmk_rc_error Synchronous action failed + * \retval pcmk_rc_ok Synchronous action succeeded, or asynchronous action + * should not be freed (because it's pending or because + * it failed to execute and was already freed) + * + * \note If the return value for an asynchronous action is not pcmk_rc_ok, the + * caller is responsible for freeing the action. + */ +static int +execute_action(svc_action_t *op) +{ +#if SUPPORT_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(const svc_action_t *op) +{ + /* Op is no longer in-flight or blocked */ + inflight_ops = g_list_remove(inflight_ops, op); + blocked_ops = g_list_remove(blocked_ops, op); + + /* Op is no longer blocking other ops, so check if any need to run */ + handle_blocked_ops(); +} + +gboolean +services_action_async_fork_notify(svc_action_t * op, + void (*action_callback) (svc_action_t *), + void (*action_fork_callback) (svc_action_t *)) +{ + CRM_CHECK(op != NULL, return TRUE); + + op->synchronous = false; + if (action_callback != NULL) { + op->opaque->callback = action_callback; + } + if (action_fork_callback != NULL) { + op->opaque->fork_callback = action_fork_callback; + } + + if (op->interval_ms > 0) { + init_recurring_actions(); + if (handle_duplicate_recurring(op)) { + /* entry rescheduled, dup freed */ + /* exit early */ + return TRUE; + } + g_hash_table_replace(recurring_actions, op->id, op); + } + + if (!pcmk_is_set(op->flags, SVC_ACTION_NON_BLOCKED) + && op->rsc && 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,out] op Meta-data action to execute + * + * \return Standard Pacemaker return code + */ +static int +execute_metadata_action(svc_action_t *op) +{ + const char *class = op->standard; + + if (op->agent == NULL) { + crm_info("Meta-data requested without specifying agent"); + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR_FATAL, "Agent not specified"); + return EINVAL; + } + + if (class == NULL) { + crm_info("Meta-data requested for agent %s without specifying class", + op->agent); + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR_FATAL, + "Agent standard not specified"); + return EINVAL; + } + + if (!strcmp(class, PCMK_RESOURCE_CLASS_SERVICE)) { + class = resources_find_service_class(op->agent); + } + if (class == NULL) { + crm_info("Meta-data requested for %s, but could not determine class", + op->agent); + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR_HARD, + "Agent standard could not be determined"); + return EINVAL; + } + + 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 Set the result of an action, with a formatted exit reason + * + * \param[out] action Where to set action result + * \param[in] agent_status Exit status to set + * \param[in] exec_status Execution status to set + * \param[in] format printf-style format for a human-friendly + * description of reason for result + * \param[in] ... arguments for \p format + */ +void +services__format_result(svc_action_t *action, int agent_status, + enum pcmk_exec_status exec_status, + const char *format, ...) +{ + va_list ap; + int len = 0; + char *reason = NULL; + + if (action == NULL) { + return; + } + + action->rc = agent_status; + action->status = exec_status; + + if (format != NULL) { + va_start(ap, format); + len = vasprintf(&reason, format, ap); + CRM_ASSERT(len > 0); + va_end(ap); + } + free(action->opaque->exit_reason); + action->opaque->exit_reason = reason; +} + +/*! + * \internal + * \brief Set the result of an action to cancelled + * + * \param[out] action Where to set action result + * + * \note This sets execution status but leaves the exit status unchanged + */ +void +services__set_cancelled(svc_action_t *action) +{ + if (action != NULL) { + action->status = PCMK_EXEC_CANCELLED; + free(action->opaque->exit_reason); + action->opaque->exit_reason = NULL; + } +} + +/*! + * \internal + * \brief Get a readable description of what an action is for + * + * \param[in] action Action to check + * + * \return Readable name for the kind of \p action + */ +const char * +services__action_kind(const svc_action_t *action) +{ + if ((action == NULL) || (action->standard == NULL)) { + return "Process"; + } else if (pcmk__str_eq(action->standard, PCMK_RESOURCE_CLASS_STONITH, + pcmk__str_none)) { + return "Fence agent"; + } else if (pcmk__str_eq(action->standard, PCMK_RESOURCE_CLASS_ALERT, + pcmk__str_none)) { + return "Alert agent"; + } else { + return "Resource agent"; + } +} + +/*! + * \internal + * \brief Get the exit reason of an action + * + * \param[in] action Action to check + * + * \return Action's exit reason (or NULL if none) + */ +const char * +services__exit_reason(const svc_action_t *action) +{ + return action->opaque->exit_reason; +} + +/*! + * \internal + * \brief Steal stdout from an action + * + * \param[in,out] action Action whose stdout is desired + * + * \return Action's stdout (which may be NULL) + * \note Upon return, \p action will no longer track the output, so it is the + * caller's responsibility to free the return value. + */ +char * +services__grab_stdout(svc_action_t *action) +{ + char *output = action->stdout_data; + + action->stdout_data = NULL; + return output; +} + +/*! + * \internal + * \brief Steal stderr from an action + * + * \param[in,out] action Action whose stderr is desired + * + * \return Action's stderr (which may be NULL) + * \note Upon return, \p action will no longer track the output, so it is the + * caller's responsibility to free the return value. + */ +char * +services__grab_stderr(svc_action_t *action) +{ + char *output = action->stderr_data; + + action->stderr_data = NULL; + return output; +} diff --git a/lib/services/services_linux.c b/lib/services/services_linux.c new file mode 100644 index 0000000..fb12f73 --- /dev/null +++ b/lib/services/services_linux.c @@ -0,0 +1,1438 @@ +/* + * Copyright 2010-2022 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 <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <errno.h> +#include <unistd.h> +#include <dirent.h> +#include <grp.h> +#include <string.h> +#include <sys/time.h> +#include <sys/resource.h> + +#include "crm/crm.h" +#include "crm/common/mainloop.h" +#include "crm/services.h" +#include "crm/services_internal.h" + +#include "services_private.h" + +static void close_pipe(int fildes[]); + +/* We have two alternative ways of handling SIGCHLD when synchronously waiting + * for spawned processes to complete. Both rely on polling a file descriptor to + * discover SIGCHLD events. + * + * If sys/signalfd.h is available (e.g. on Linux), we call signalfd() to + * generate the file descriptor. Otherwise, we use the "self-pipe trick" + * (opening a pipe and writing a byte to it when SIGCHLD is received). + */ +#ifdef HAVE_SYS_SIGNALFD_H + +// signalfd() implementation + +#include <sys/signalfd.h> + +// Everything needed to manage SIGCHLD handling +struct sigchld_data_s { + sigset_t mask; // Signals to block now (including SIGCHLD) + sigset_t old_mask; // Previous set of blocked signals +}; + +// Initialize SIGCHLD data and prepare for use +static bool +sigchld_setup(struct sigchld_data_s *data) +{ + sigemptyset(&(data->mask)); + sigaddset(&(data->mask), SIGCHLD); + + sigemptyset(&(data->old_mask)); + + // Block SIGCHLD (saving previous set of blocked signals to restore later) + if (sigprocmask(SIG_BLOCK, &(data->mask), &(data->old_mask)) < 0) { + crm_info("Wait for child process completion failed: %s " + CRM_XS " source=sigprocmask", pcmk_rc_str(errno)); + return false; + } + return true; +} + +// Get a file descriptor suitable for polling for SIGCHLD events +static int +sigchld_open(struct sigchld_data_s *data) +{ + int fd; + + CRM_CHECK(data != NULL, return -1); + + fd = signalfd(-1, &(data->mask), SFD_NONBLOCK); + if (fd < 0) { + crm_info("Wait for child process completion failed: %s " + CRM_XS " source=signalfd", pcmk_rc_str(errno)); + } + return fd; +} + +// Close a file descriptor returned by sigchld_open() +static void +sigchld_close(int fd) +{ + if (fd > 0) { + close(fd); + } +} + +// Return true if SIGCHLD was received from polled fd +static bool +sigchld_received(int fd) +{ + struct signalfd_siginfo fdsi; + ssize_t s; + + if (fd < 0) { + return false; + } + s = read(fd, &fdsi, sizeof(struct signalfd_siginfo)); + if (s != sizeof(struct signalfd_siginfo)) { + crm_info("Wait for child process completion failed: %s " + CRM_XS " source=read", pcmk_rc_str(errno)); + + } else if (fdsi.ssi_signo == SIGCHLD) { + return true; + } + return false; +} + +// Do anything needed after done waiting for SIGCHLD +static void +sigchld_cleanup(struct sigchld_data_s *data) +{ + // Restore the original set of blocked signals + if ((sigismember(&(data->old_mask), SIGCHLD) == 0) + && (sigprocmask(SIG_UNBLOCK, &(data->mask), NULL) < 0)) { + crm_warn("Could not clean up after child process completion: %s", + pcmk_rc_str(errno)); + } +} + +#else // HAVE_SYS_SIGNALFD_H not defined + +// Self-pipe implementation (see above for function descriptions) + +struct sigchld_data_s { + int pipe_fd[2]; // Pipe file descriptors + struct sigaction sa; // Signal handling info (with SIGCHLD) + struct sigaction old_sa; // Previous signal handling info +}; + +// We need a global to use in the signal handler +volatile struct sigchld_data_s *last_sigchld_data = NULL; + +static void +sigchld_handler(void) +{ + // We received a SIGCHLD, so trigger pipe polling + if ((last_sigchld_data != NULL) + && (last_sigchld_data->pipe_fd[1] >= 0) + && (write(last_sigchld_data->pipe_fd[1], "", 1) == -1)) { + crm_info("Wait for child process completion failed: %s " + CRM_XS " source=write", pcmk_rc_str(errno)); + } +} + +static bool +sigchld_setup(struct sigchld_data_s *data) +{ + int rc; + + data->pipe_fd[0] = data->pipe_fd[1] = -1; + + if (pipe(data->pipe_fd) == -1) { + crm_info("Wait for child process completion failed: %s " + CRM_XS " source=pipe", pcmk_rc_str(errno)); + return false; + } + + rc = pcmk__set_nonblocking(data->pipe_fd[0]); + if (rc != pcmk_rc_ok) { + crm_info("Could not set pipe input non-blocking: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + } + rc = pcmk__set_nonblocking(data->pipe_fd[1]); + if (rc != pcmk_rc_ok) { + crm_info("Could not set pipe output non-blocking: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + } + + // Set SIGCHLD handler + data->sa.sa_handler = (sighandler_t) sigchld_handler; + data->sa.sa_flags = 0; + sigemptyset(&(data->sa.sa_mask)); + if (sigaction(SIGCHLD, &(data->sa), &(data->old_sa)) < 0) { + crm_info("Wait for child process completion failed: %s " + CRM_XS " source=sigaction", pcmk_rc_str(errno)); + } + + // Remember data for use in signal handler + last_sigchld_data = data; + return true; +} + +static int +sigchld_open(struct sigchld_data_s *data) +{ + CRM_CHECK(data != NULL, return -1); + return data->pipe_fd[0]; +} + +static void +sigchld_close(int fd) +{ + // Pipe will be closed in sigchld_cleanup() + return; +} + +static bool +sigchld_received(int fd) +{ + char ch; + + if (fd < 0) { + return false; + } + + // Clear out the self-pipe + while (read(fd, &ch, 1) == 1) /*omit*/; + return true; +} + +static void +sigchld_cleanup(struct sigchld_data_s *data) +{ + // Restore the previous SIGCHLD handler + if (sigaction(SIGCHLD, &(data->old_sa), NULL) < 0) { + crm_warn("Could not clean up after child process completion: %s", + pcmk_rc_str(errno)); + } + + close_pipe(data->pipe_fd); +} + +#endif + +/*! + * \internal + * \brief Close the two file descriptors of a pipe + * + * \param[in,out] fildes Array of file descriptors opened by pipe() + */ +static void +close_pipe(int fildes[]) +{ + if (fildes[0] >= 0) { + close(fildes[0]); + fildes[0] = -1; + } + if (fildes[1] >= 0) { + close(fildes[1]); + fildes[1] = -1; + } +} + +static gboolean +svc_read_output(int fd, svc_action_t * op, bool is_stderr) +{ + char *data = NULL; + int rc = 0, len = 0; + char buf[500]; + static const size_t buf_read_len = sizeof(buf) - 1; + + + if (fd < 0) { + crm_trace("No fd for %s", op->id); + return FALSE; + } + + if (is_stderr && op->stderr_data) { + len = strlen(op->stderr_data); + data = op->stderr_data; + crm_trace("Reading %s stderr into offset %d", op->id, len); + + } else if (is_stderr == FALSE && op->stdout_data) { + len = strlen(op->stdout_data); + data = op->stdout_data; + crm_trace("Reading %s stdout into offset %d", op->id, len); + + } else { + crm_trace("Reading %s %s into offset %d", op->id, is_stderr?"stderr":"stdout", len); + } + + do { + rc = read(fd, buf, buf_read_len); + if (rc > 0) { + buf[rc] = 0; + crm_trace("Got %d chars: %.80s", rc, buf); + data = pcmk__realloc(data, len + rc + 1); + len += sprintf(data + len, "%s", buf); + + } else if (errno != EINTR) { + /* error or EOF + * Cleanup happens in pipe_done() + */ + rc = FALSE; + break; + } + + } while (rc == buf_read_len || rc < 0); + + if (is_stderr) { + op->stderr_data = data; + } else { + op->stdout_data = data; + } + + return rc; +} + +static int +dispatch_stdout(gpointer userdata) +{ + svc_action_t *op = (svc_action_t *) userdata; + + return svc_read_output(op->opaque->stdout_fd, op, FALSE); +} + +static int +dispatch_stderr(gpointer userdata) +{ + svc_action_t *op = (svc_action_t *) userdata; + + return svc_read_output(op->opaque->stderr_fd, op, TRUE); +} + +static void +pipe_out_done(gpointer user_data) +{ + svc_action_t *op = (svc_action_t *) user_data; + + crm_trace("%p", op); + + op->opaque->stdout_gsource = NULL; + if (op->opaque->stdout_fd > STDOUT_FILENO) { + close(op->opaque->stdout_fd); + } + op->opaque->stdout_fd = -1; +} + +static void +pipe_err_done(gpointer user_data) +{ + svc_action_t *op = (svc_action_t *) user_data; + + op->opaque->stderr_gsource = NULL; + if (op->opaque->stderr_fd > STDERR_FILENO) { + close(op->opaque->stderr_fd); + } + op->opaque->stderr_fd = -1; +} + +static struct mainloop_fd_callbacks stdout_callbacks = { + .dispatch = dispatch_stdout, + .destroy = pipe_out_done, +}; + +static struct mainloop_fd_callbacks stderr_callbacks = { + .dispatch = dispatch_stderr, + .destroy = pipe_err_done, +}; + +static void +set_ocf_env(const char *key, const char *value, gpointer user_data) +{ + if (setenv(key, value, 1) != 0) { + crm_perror(LOG_ERR, "setenv failed for key:%s and value:%s", key, value); + } +} + +static void +set_ocf_env_with_prefix(gpointer key, gpointer value, gpointer user_data) +{ + char buffer[500]; + + snprintf(buffer, sizeof(buffer), strcmp(key, "OCF_CHECK_LEVEL") != 0 ? "OCF_RESKEY_%s" : "%s", (char *)key); + set_ocf_env(buffer, value, user_data); +} + +static void +set_alert_env(gpointer key, gpointer value, gpointer user_data) +{ + int rc; + + if (value != NULL) { + rc = setenv(key, value, 1); + } else { + rc = unsetenv(key); + } + + if (rc < 0) { + crm_perror(LOG_ERR, "setenv %s=%s", + (char*)key, (value? (char*)value : "")); + } else { + crm_trace("setenv %s=%s", (char*)key, (value? (char*)value : "")); + } +} + +/*! + * \internal + * \brief Add environment variables suitable for an action + * + * \param[in] op Action to use + */ +static void +add_action_env_vars(const svc_action_t *op) +{ + void (*env_setter)(gpointer, gpointer, gpointer) = NULL; + if (op->agent == NULL) { + env_setter = set_alert_env; /* we deal with alert handler */ + + } else if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_casei)) { + env_setter = set_ocf_env_with_prefix; + } + + if (env_setter != NULL && op->params != NULL) { + g_hash_table_foreach(op->params, env_setter, NULL); + } + + if (env_setter == NULL || env_setter == set_alert_env) { + return; + } + + set_ocf_env("OCF_RA_VERSION_MAJOR", PCMK_OCF_MAJOR_VERSION, NULL); + set_ocf_env("OCF_RA_VERSION_MINOR", PCMK_OCF_MINOR_VERSION, NULL); + set_ocf_env("OCF_ROOT", OCF_ROOT_DIR, NULL); + set_ocf_env("OCF_EXIT_REASON_PREFIX", PCMK_OCF_REASON_PREFIX, NULL); + + if (op->rsc) { + set_ocf_env("OCF_RESOURCE_INSTANCE", op->rsc, NULL); + } + + if (op->agent != NULL) { + set_ocf_env("OCF_RESOURCE_TYPE", op->agent, NULL); + } + + /* Notes: this is not added to specification yet. Sept 10,2004 */ + if (op->provider != NULL) { + set_ocf_env("OCF_RESOURCE_PROVIDER", op->provider, NULL); + } +} + +static void +pipe_in_single_parameter(gpointer key, gpointer value, gpointer user_data) +{ + svc_action_t *op = user_data; + char *buffer = crm_strdup_printf("%s=%s\n", (char *)key, (char *) value); + int ret, total = 0, len = strlen(buffer); + + do { + errno = 0; + ret = write(op->opaque->stdin_fd, buffer + total, len - total); + if (ret > 0) { + total += ret; + } + + } while ((errno == EINTR) && (total < len)); + free(buffer); +} + +/*! + * \internal + * \brief Pipe parameters in via stdin for action + * + * \param[in] op Action to use + */ +static void +pipe_in_action_stdin_parameters(const svc_action_t *op) +{ + if (op->params) { + g_hash_table_foreach(op->params, pipe_in_single_parameter, (gpointer) op); + } +} + +gboolean +recurring_action_timer(gpointer data) +{ + svc_action_t *op = data; + + crm_debug("Scheduling another invocation of %s", op->id); + + /* Clean out the old result */ + free(op->stdout_data); + op->stdout_data = NULL; + free(op->stderr_data); + op->stderr_data = NULL; + op->opaque->repeat_timer = 0; + + services_action_async(op, NULL); + return FALSE; +} + +/*! + * \internal + * \brief Finalize handling of an asynchronous operation + * + * Given a completed asynchronous operation, cancel or reschedule it as + * appropriate if recurring, call its callback if registered, stop tracking it, + * and clean it up. + * + * \param[in,out] op Operation to finalize + * + * \return Standard Pacemaker return code + * \retval EINVAL Caller supplied NULL or invalid \p op + * \retval EBUSY Uncanceled recurring action has only been cleaned up + * \retval pcmk_rc_ok Action has been freed + * + * \note If the return value is not pcmk_rc_ok, the caller is responsible for + * freeing the action. + */ +int +services__finalize_async_op(svc_action_t *op) +{ + CRM_CHECK((op != NULL) && !(op->synchronous), return EINVAL); + + if (op->interval_ms != 0) { + // Recurring operations must be either cancelled or rescheduled + if (op->cancel) { + services__set_cancelled(op); + cancel_recurring_action(op); + } else { + op->opaque->repeat_timer = g_timeout_add(op->interval_ms, + recurring_action_timer, + (void *) op); + } + } + + if (op->opaque->callback != NULL) { + op->opaque->callback(op); + } + + // Stop tracking the operation (as in-flight or blocked) + op->pid = 0; + services_untrack_op(op); + + if ((op->interval_ms != 0) && !(op->cancel)) { + // Do not free recurring actions (they will get freed when cancelled) + services_action_cleanup(op); + return EBUSY; + } + + services_action_free(op); + return pcmk_rc_ok; +} + +static void +close_op_input(svc_action_t *op) +{ + if (op->opaque->stdin_fd >= 0) { + close(op->opaque->stdin_fd); + } +} + +static void +finish_op_output(svc_action_t *op, bool is_stderr) +{ + mainloop_io_t **source; + int fd; + + if (is_stderr) { + source = &(op->opaque->stderr_gsource); + fd = op->opaque->stderr_fd; + } else { + source = &(op->opaque->stdout_gsource); + fd = op->opaque->stdout_fd; + } + + if (op->synchronous || *source) { + crm_trace("Finish reading %s[%d] %s", + op->id, op->pid, (is_stderr? "stderr" : "stdout")); + svc_read_output(fd, op, is_stderr); + if (op->synchronous) { + close(fd); + } else { + mainloop_del_fd(*source); + *source = NULL; + } + } +} + +// Log an operation's stdout and stderr +static void +log_op_output(svc_action_t *op) +{ + char *prefix = crm_strdup_printf("%s[%d] error output", op->id, op->pid); + + /* The library caller has better context to know how important the output + * is, so log it at info and debug severity here. They can log it again at + * higher severity if appropriate. + */ + crm_log_output(LOG_INFO, prefix, op->stderr_data); + strcpy(prefix + strlen(prefix) - strlen("error output"), "output"); + crm_log_output(LOG_DEBUG, prefix, op->stdout_data); + free(prefix); +} + +// Truncate exit reasons at this many characters +#define EXIT_REASON_MAX_LEN 128 + +static void +parse_exit_reason_from_stderr(svc_action_t *op) +{ + const char *reason_start = NULL; + const char *reason_end = NULL; + const int prefix_len = strlen(PCMK_OCF_REASON_PREFIX); + + if ((op->stderr_data == NULL) || + // Only OCF agents have exit reasons in stderr + !pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_none)) { + return; + } + + // Find the last occurrence of the magic string indicating an exit reason + for (const char *cur = strstr(op->stderr_data, PCMK_OCF_REASON_PREFIX); + cur != NULL; cur = strstr(cur, PCMK_OCF_REASON_PREFIX)) { + + cur += prefix_len; // Skip over magic string + reason_start = cur; + } + + if ((reason_start == NULL) || (reason_start[0] == '\n') + || (reason_start[0] == '\0')) { + return; // No or empty exit reason + } + + // Exit reason goes to end of line (or end of output) + reason_end = strchr(reason_start, '\n'); + if (reason_end == NULL) { + reason_end = reason_start + strlen(reason_start); + } + + // Limit size of exit reason to something reasonable + if (reason_end > (reason_start + EXIT_REASON_MAX_LEN)) { + reason_end = reason_start + EXIT_REASON_MAX_LEN; + } + + free(op->opaque->exit_reason); + op->opaque->exit_reason = strndup(reason_start, reason_end - reason_start); +} + +/*! + * \internal + * \brief Process the completion of an asynchronous child process + * + * \param[in,out] p Child process that completed + * \param[in] pid Process ID of child + * \param[in] core (Unused) + * \param[in] signo Signal that interrupted child, if any + * \param[in] exitcode Exit status of child process + */ +static void +async_action_complete(mainloop_child_t *p, pid_t pid, int core, int signo, + int exitcode) +{ + svc_action_t *op = mainloop_child_userdata(p); + + mainloop_clear_child_userdata(p); + CRM_CHECK(op->pid == pid, + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR, "Bug in mainloop handling"); + return); + + /* Depending on the priority the mainloop gives the stdout and stderr + * file descriptors, this function could be called before everything has + * been read from them, so force a final read now. + */ + finish_op_output(op, true); + finish_op_output(op, false); + + close_op_input(op); + + if (signo == 0) { + crm_debug("%s[%d] exited with status %d", op->id, op->pid, exitcode); + services__set_result(op, exitcode, PCMK_EXEC_DONE, NULL); + log_op_output(op); + parse_exit_reason_from_stderr(op); + + } else if (mainloop_child_timeout(p)) { + const char *kind = services__action_kind(op); + + crm_info("%s %s[%d] timed out after %s", + kind, op->id, op->pid, pcmk__readable_interval(op->timeout)); + services__format_result(op, services__generic_error(op), + PCMK_EXEC_TIMEOUT, + "%s did not complete within %s", + kind, pcmk__readable_interval(op->timeout)); + + } else if (op->cancel) { + /* If an in-flight recurring operation was killed because it was + * cancelled, don't treat that as a failure. + */ + crm_info("%s[%d] terminated with signal %d (%s)", + op->id, op->pid, signo, strsignal(signo)); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_CANCELLED, NULL); + + } else { + crm_info("%s[%d] terminated with signal %d (%s)", + op->id, op->pid, signo, strsignal(signo)); + services__format_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "%s interrupted by %s signal", + services__action_kind(op), strsignal(signo)); + } + + services__finalize_async_op(op); +} + +/*! + * \internal + * \brief Return agent standard's exit status for "generic error" + * + * When returning an internal error for an action, a value that is appropriate + * to the action's agent standard must be used. This function returns a value + * appropriate for errors in general. + * + * \param[in] op Action that error is for + * + * \return Exit status appropriate to agent standard + * \note Actions without a standard will get PCMK_OCF_UNKNOWN_ERROR. + */ +int +services__generic_error(const svc_action_t *op) +{ + if ((op == NULL) || (op->standard == NULL)) { + return PCMK_OCF_UNKNOWN_ERROR; + } + + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei) + && pcmk__str_eq(op->action, "status", pcmk__str_casei)) { + + return PCMK_LSB_STATUS_UNKNOWN; + } + +#if SUPPORT_NAGIOS + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_casei)) { + return NAGIOS_STATE_UNKNOWN; + } +#endif + + return PCMK_OCF_UNKNOWN_ERROR; +} + +/*! + * \internal + * \brief Return agent standard's exit status for "not installed" + * + * When returning an internal error for an action, a value that is appropriate + * to the action's agent standard must be used. This function returns a value + * appropriate for "not installed" errors. + * + * \param[in] op Action that error is for + * + * \return Exit status appropriate to agent standard + * \note Actions without a standard will get PCMK_OCF_UNKNOWN_ERROR. + */ +int +services__not_installed_error(const svc_action_t *op) +{ + if ((op == NULL) || (op->standard == NULL)) { + return PCMK_OCF_UNKNOWN_ERROR; + } + + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei) + && pcmk__str_eq(op->action, "status", pcmk__str_casei)) { + + return PCMK_LSB_STATUS_NOT_INSTALLED; + } + +#if SUPPORT_NAGIOS + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_casei)) { + return NAGIOS_STATE_UNKNOWN; + } +#endif + + return PCMK_OCF_NOT_INSTALLED; +} + +/*! + * \internal + * \brief Return agent standard's exit status for "insufficient privileges" + * + * When returning an internal error for an action, a value that is appropriate + * to the action's agent standard must be used. This function returns a value + * appropriate for "insufficient privileges" errors. + * + * \param[in] op Action that error is for + * + * \return Exit status appropriate to agent standard + * \note Actions without a standard will get PCMK_OCF_UNKNOWN_ERROR. + */ +int +services__authorization_error(const svc_action_t *op) +{ + if ((op == NULL) || (op->standard == NULL)) { + return PCMK_OCF_UNKNOWN_ERROR; + } + + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei) + && pcmk__str_eq(op->action, "status", pcmk__str_casei)) { + + return PCMK_LSB_STATUS_INSUFFICIENT_PRIV; + } + +#if SUPPORT_NAGIOS + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_casei)) { + return NAGIOS_INSUFFICIENT_PRIV; + } +#endif + + return PCMK_OCF_INSUFFICIENT_PRIV; +} + +/*! + * \internal + * \brief Return agent standard's exit status for "not configured" + * + * When returning an internal error for an action, a value that is appropriate + * to the action's agent standard must be used. This function returns a value + * appropriate for "not configured" errors. + * + * \param[in] op Action that error is for + * \param[in] is_fatal Whether problem is cluster-wide instead of only local + * + * \return Exit status appropriate to agent standard + * \note Actions without a standard will get PCMK_OCF_UNKNOWN_ERROR. + */ +int +services__configuration_error(const svc_action_t *op, bool is_fatal) +{ + if ((op == NULL) || (op->standard == NULL)) { + return PCMK_OCF_UNKNOWN_ERROR; + } + + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei) + && pcmk__str_eq(op->action, "status", pcmk__str_casei)) { + + return PCMK_LSB_NOT_CONFIGURED; + } + +#if SUPPORT_NAGIOS + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_casei)) { + return NAGIOS_STATE_UNKNOWN; + } +#endif + + return is_fatal? PCMK_OCF_NOT_CONFIGURED : PCMK_OCF_INVALID_PARAM; +} + + +/*! + * \internal + * \brief Set operation rc and status per errno from stat(), fork() or execvp() + * + * \param[in,out] op Operation to set rc and status for + * \param[in] error Value of errno after system call + * + * \return void + */ +void +services__handle_exec_error(svc_action_t * op, int error) +{ + const char *name = op->opaque->exec; + + if (name == NULL) { + name = op->agent; + if (name == NULL) { + name = op->id; + } + } + + switch (error) { /* see execve(2), stat(2) and fork(2) */ + case ENOENT: /* No such file or directory */ + case EISDIR: /* Is a directory */ + case ENOTDIR: /* Path component is not a directory */ + case EINVAL: /* Invalid executable format */ + case ENOEXEC: /* Invalid executable format */ + services__format_result(op, services__not_installed_error(op), + PCMK_EXEC_NOT_INSTALLED, "%s: %s", + name, pcmk_rc_str(error)); + break; + case EACCES: /* permission denied (various errors) */ + case EPERM: /* permission denied (various errors) */ + services__format_result(op, services__authorization_error(op), + PCMK_EXEC_ERROR, "%s: %s", + name, pcmk_rc_str(error)); + break; + default: + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR, pcmk_rc_str(error)); + } +} + +/*! + * \internal + * \brief Exit a child process that failed before executing agent + * + * \param[in] op Action that failed + * \param[in] exit_status Exit status code to use + * \param[in] exit_reason Exit reason to output if for OCF agent + */ +static void +exit_child(const svc_action_t *op, int exit_status, const char *exit_reason) +{ + if ((op != NULL) && (exit_reason != NULL) + && pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_OCF, + pcmk__str_none)) { + fprintf(stderr, PCMK_OCF_REASON_PREFIX "%s\n", exit_reason); + } + _exit(exit_status); +} + +static void +action_launch_child(svc_action_t *op) +{ + int rc; + + /* SIGPIPE is ignored (which is different from signal blocking) by the gnutls library. + * Depending on the libqb version in use, libqb may set SIGPIPE to be ignored as well. + * We do not want this to be inherited by the child process. By resetting this the signal + * to the default behavior, we avoid some potential odd problems that occur during OCF + * scripts when SIGPIPE is ignored by the environment. */ + signal(SIGPIPE, SIG_DFL); + +#if defined(HAVE_SCHED_SETSCHEDULER) + if (sched_getscheduler(0) != SCHED_OTHER) { + struct sched_param sp; + + memset(&sp, 0, sizeof(sp)); + sp.sched_priority = 0; + + if (sched_setscheduler(0, SCHED_OTHER, &sp) == -1) { + crm_info("Could not reset scheduling policy for %s", op->id); + } + } +#endif + if (setpriority(PRIO_PROCESS, 0, 0) == -1) { + crm_info("Could not reset process priority for %s", op->id); + } + + /* Man: The call setpgrp() is equivalent to setpgid(0,0) + * _and_ compiles on BSD variants too + * need to investigate if it works the same too. + */ + setpgid(0, 0); + + pcmk__close_fds_in_child(false); + + /* It would be nice if errors in this function could be reported as + * execution status (for example, PCMK_EXEC_NO_SECRETS for the secrets error + * below) instead of exit status. However, we've already forked, so + * exit status is all we have. At least for OCF actions, we can output an + * exit reason for the parent to parse. + */ + +#if SUPPORT_CIBSECRETS + rc = pcmk__substitute_secrets(op->rsc, op->params); + if (rc != pcmk_rc_ok) { + if (pcmk__str_eq(op->action, "stop", pcmk__str_casei)) { + crm_info("Proceeding with stop operation for %s " + "despite being unable to load CIB secrets (%s)", + op->rsc, pcmk_rc_str(rc)); + } else { + crm_err("Considering %s unconfigured " + "because unable to load CIB secrets: %s", + op->rsc, pcmk_rc_str(rc)); + exit_child(op, services__configuration_error(op, false), + "Unable to load CIB secrets"); + } + } +#endif + + add_action_env_vars(op); + + /* Become the desired user */ + if (op->opaque->uid && (geteuid() == 0)) { + + // If requested, set effective group + if (op->opaque->gid && (setgid(op->opaque->gid) < 0)) { + crm_err("Considering %s unauthorized because could not set " + "child group to %d: %s", + op->id, op->opaque->gid, strerror(errno)); + exit_child(op, services__authorization_error(op), + "Could not set group for child process"); + } + + // Erase supplementary group list + // (We could do initgroups() if we kept a copy of the username) + if (setgroups(0, NULL) < 0) { + crm_err("Considering %s unauthorized because could not " + "clear supplementary groups: %s", op->id, strerror(errno)); + exit_child(op, services__authorization_error(op), + "Could not clear supplementary groups for child process"); + } + + // Set effective user + if (setuid(op->opaque->uid) < 0) { + crm_err("Considering %s unauthorized because could not set user " + "to %d: %s", op->id, op->opaque->uid, strerror(errno)); + exit_child(op, services__authorization_error(op), + "Could not set user for child process"); + } + } + + // Execute the agent (doesn't return if successful) + execvp(op->opaque->exec, op->opaque->args); + + // An earlier stat() should have avoided most possible errors + rc = errno; + services__handle_exec_error(op, rc); + crm_err("Unable to execute %s: %s", op->id, strerror(rc)); + exit_child(op, op->rc, "Child process was unable to execute file"); +} + +/*! + * \internal + * \brief Wait for synchronous action to complete, and set its result + * + * \param[in,out] op Action to wait for + * \param[in,out] data Child signal data + */ +static void +wait_for_sync_result(svc_action_t *op, struct sigchld_data_s *data) +{ + int status = 0; + int timeout = op->timeout; + time_t start = time(NULL); + struct pollfd fds[3]; + int wait_rc = 0; + const char *wait_reason = NULL; + + fds[0].fd = op->opaque->stdout_fd; + fds[0].events = POLLIN; + fds[0].revents = 0; + + fds[1].fd = op->opaque->stderr_fd; + fds[1].events = POLLIN; + fds[1].revents = 0; + + fds[2].fd = sigchld_open(data); + fds[2].events = POLLIN; + fds[2].revents = 0; + + crm_trace("Waiting for %s[%d]", op->id, op->pid); + do { + int poll_rc = poll(fds, 3, timeout); + + wait_reason = NULL; + + if (poll_rc > 0) { + if (fds[0].revents & POLLIN) { + svc_read_output(op->opaque->stdout_fd, op, FALSE); + } + + if (fds[1].revents & POLLIN) { + svc_read_output(op->opaque->stderr_fd, op, TRUE); + } + + if ((fds[2].revents & POLLIN) && sigchld_received(fds[2].fd)) { + wait_rc = waitpid(op->pid, &status, WNOHANG); + + if ((wait_rc > 0) || ((wait_rc < 0) && (errno == ECHILD))) { + // Child process exited or doesn't exist + break; + + } else if (wait_rc < 0) { + wait_reason = pcmk_rc_str(errno); + crm_info("Wait for completion of %s[%d] failed: %s " + CRM_XS " source=waitpid", + op->id, op->pid, wait_reason); + wait_rc = 0; // Act as if process is still running + } + } + + } else if (poll_rc == 0) { + // Poll timed out with no descriptors ready + timeout = 0; + break; + + } else if ((poll_rc < 0) && (errno != EINTR)) { + wait_reason = pcmk_rc_str(errno); + crm_info("Wait for completion of %s[%d] failed: %s " + CRM_XS " source=poll", op->id, op->pid, wait_reason); + break; + } + + timeout = op->timeout - (time(NULL) - start) * 1000; + + } while ((op->timeout < 0 || timeout > 0)); + + crm_trace("Stopped waiting for %s[%d]", op->id, op->pid); + finish_op_output(op, true); + finish_op_output(op, false); + close_op_input(op); + sigchld_close(fds[2].fd); + + if (wait_rc <= 0) { + + if ((op->timeout > 0) && (timeout <= 0)) { + services__format_result(op, services__generic_error(op), + PCMK_EXEC_TIMEOUT, + "%s did not exit within specified timeout", + services__action_kind(op)); + crm_info("%s[%d] timed out after %dms", + op->id, op->pid, op->timeout); + + } else { + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR, wait_reason); + } + + /* If only child hasn't been successfully waited for, yet. + This is to limit killing wrong target a bit more. */ + if ((wait_rc == 0) && (waitpid(op->pid, &status, WNOHANG) == 0)) { + if (kill(op->pid, SIGKILL)) { + crm_warn("Could not kill rogue child %s[%d]: %s", + op->id, op->pid, pcmk_rc_str(errno)); + } + /* Safe to skip WNOHANG here as we sent non-ignorable signal. */ + while ((waitpid(op->pid, &status, 0) == (pid_t) -1) + && (errno == EINTR)) { + /* keep waiting */; + } + } + + } else if (WIFEXITED(status)) { + services__set_result(op, WEXITSTATUS(status), PCMK_EXEC_DONE, NULL); + parse_exit_reason_from_stderr(op); + crm_info("%s[%d] exited with status %d", op->id, op->pid, op->rc); + + } else if (WIFSIGNALED(status)) { + int signo = WTERMSIG(status); + + services__format_result(op, services__generic_error(op), + PCMK_EXEC_ERROR, "%s interrupted by %s signal", + services__action_kind(op), strsignal(signo)); + crm_info("%s[%d] terminated with signal %d (%s)", + op->id, op->pid, signo, strsignal(signo)); + +#ifdef WCOREDUMP + if (WCOREDUMP(status)) { + crm_warn("%s[%d] dumped core", op->id, op->pid); + } +#endif + + } else { + // Shouldn't be possible to get here + services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR, + "Unable to wait for child to complete"); + } +} + +/*! + * \internal + * \brief Execute an action whose standard uses executable files + * + * \param[in,out] op Action to execute + * + * \return Standard Pacemaker return value + * \retval EBUSY Recurring operation could not be initiated + * \retval pcmk_rc_error Synchronous action failed + * \retval pcmk_rc_ok Synchronous action succeeded, or asynchronous action + * should not be freed (because it's pending or because + * it failed to execute and was already freed) + * + * \note If the return value for an asynchronous action is not pcmk_rc_ok, the + * caller is responsible for freeing the action. + */ +int +services__execute_file(svc_action_t *op) +{ + int stdout_fd[2]; + int stderr_fd[2]; + int stdin_fd[2] = {-1, -1}; + int rc; + struct stat st; + struct sigchld_data_s data; + + // Catch common failure conditions early + if (stat(op->opaque->exec, &st) != 0) { + rc = errno; + crm_info("Cannot execute '%s': %s " CRM_XS " stat rc=%d", + op->opaque->exec, pcmk_strerror(rc), rc); + services__handle_exec_error(op, rc); + goto done; + } + + if (pipe(stdout_fd) < 0) { + rc = errno; + crm_info("Cannot execute '%s': %s " CRM_XS " pipe(stdout) rc=%d", + op->opaque->exec, pcmk_strerror(rc), rc); + services__handle_exec_error(op, rc); + goto done; + } + + if (pipe(stderr_fd) < 0) { + rc = errno; + + close_pipe(stdout_fd); + + crm_info("Cannot execute '%s': %s " CRM_XS " pipe(stderr) rc=%d", + op->opaque->exec, pcmk_strerror(rc), rc); + services__handle_exec_error(op, rc); + goto done; + } + + if (pcmk_is_set(pcmk_get_ra_caps(op->standard), pcmk_ra_cap_stdin)) { + if (pipe(stdin_fd) < 0) { + rc = errno; + + close_pipe(stdout_fd); + close_pipe(stderr_fd); + + crm_info("Cannot execute '%s': %s " CRM_XS " pipe(stdin) rc=%d", + op->opaque->exec, pcmk_strerror(rc), rc); + services__handle_exec_error(op, rc); + goto done; + } + } + + if (op->synchronous && !sigchld_setup(&data)) { + close_pipe(stdin_fd); + close_pipe(stdout_fd); + close_pipe(stderr_fd); + sigchld_cleanup(&data); + services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR, + "Could not manage signals for child process"); + goto done; + } + + op->pid = fork(); + switch (op->pid) { + case -1: + rc = errno; + close_pipe(stdin_fd); + close_pipe(stdout_fd); + close_pipe(stderr_fd); + + crm_info("Cannot execute '%s': %s " CRM_XS " fork rc=%d", + op->opaque->exec, pcmk_strerror(rc), rc); + services__handle_exec_error(op, rc); + if (op->synchronous) { + sigchld_cleanup(&data); + } + goto done; + break; + + case 0: /* Child */ + close(stdout_fd[0]); + close(stderr_fd[0]); + if (stdin_fd[1] >= 0) { + close(stdin_fd[1]); + } + if (STDOUT_FILENO != stdout_fd[1]) { + if (dup2(stdout_fd[1], STDOUT_FILENO) != STDOUT_FILENO) { + crm_warn("Can't redirect output from '%s': %s " + CRM_XS " errno=%d", + op->opaque->exec, pcmk_rc_str(errno), errno); + } + close(stdout_fd[1]); + } + if (STDERR_FILENO != stderr_fd[1]) { + if (dup2(stderr_fd[1], STDERR_FILENO) != STDERR_FILENO) { + crm_warn("Can't redirect error output from '%s': %s " + CRM_XS " errno=%d", + op->opaque->exec, pcmk_rc_str(errno), errno); + } + close(stderr_fd[1]); + } + if ((stdin_fd[0] >= 0) && + (STDIN_FILENO != stdin_fd[0])) { + if (dup2(stdin_fd[0], STDIN_FILENO) != STDIN_FILENO) { + crm_warn("Can't redirect input to '%s': %s " + CRM_XS " errno=%d", + op->opaque->exec, pcmk_rc_str(errno), errno); + } + close(stdin_fd[0]); + } + + if (op->synchronous) { + sigchld_cleanup(&data); + } + + action_launch_child(op); + CRM_ASSERT(0); /* action_launch_child is effectively noreturn */ + } + + /* Only the parent reaches here */ + close(stdout_fd[1]); + close(stderr_fd[1]); + if (stdin_fd[0] >= 0) { + close(stdin_fd[0]); + } + + op->opaque->stdout_fd = stdout_fd[0]; + rc = pcmk__set_nonblocking(op->opaque->stdout_fd); + if (rc != pcmk_rc_ok) { + crm_info("Could not set '%s' output non-blocking: %s " + CRM_XS " rc=%d", + op->opaque->exec, pcmk_rc_str(rc), rc); + } + + op->opaque->stderr_fd = stderr_fd[0]; + rc = pcmk__set_nonblocking(op->opaque->stderr_fd); + if (rc != pcmk_rc_ok) { + crm_info("Could not set '%s' error output non-blocking: %s " + CRM_XS " rc=%d", + op->opaque->exec, pcmk_rc_str(rc), rc); + } + + op->opaque->stdin_fd = stdin_fd[1]; + if (op->opaque->stdin_fd >= 0) { + // using buffer behind non-blocking-fd here - that could be improved + // as long as no other standard uses stdin_fd assume stonith + rc = pcmk__set_nonblocking(op->opaque->stdin_fd); + if (rc != pcmk_rc_ok) { + crm_info("Could not set '%s' input non-blocking: %s " + CRM_XS " fd=%d,rc=%d", op->opaque->exec, + pcmk_rc_str(rc), op->opaque->stdin_fd, rc); + } + pipe_in_action_stdin_parameters(op); + // as long as we are handling parameters directly in here just close + close(op->opaque->stdin_fd); + op->opaque->stdin_fd = -1; + } + + // after fds are setup properly and before we plug anything into mainloop + if (op->opaque->fork_callback) { + op->opaque->fork_callback(op); + } + + if (op->synchronous) { + wait_for_sync_result(op, &data); + sigchld_cleanup(&data); + goto done; + } + + crm_trace("Waiting async for '%s'[%d]", op->opaque->exec, op->pid); + mainloop_child_add_with_flags(op->pid, op->timeout, op->id, op, + pcmk_is_set(op->flags, SVC_ACTION_LEAVE_GROUP)? mainloop_leave_pid_group : 0, + async_action_complete); + + op->opaque->stdout_gsource = mainloop_add_fd(op->id, + G_PRIORITY_LOW, + op->opaque->stdout_fd, op, + &stdout_callbacks); + op->opaque->stderr_gsource = mainloop_add_fd(op->id, + G_PRIORITY_LOW, + op->opaque->stderr_fd, op, + &stderr_callbacks); + 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); + } +} + +GList * +services_os_get_single_directory_list(const char *root, gboolean files, gboolean executable) +{ + GList *list = NULL; + struct dirent **namelist; + int entries = 0, lpc = 0; + char buffer[PATH_MAX]; + + entries = scandir(root, &namelist, NULL, alphasort); + if (entries <= 0) { + return list; + } + + for (lpc = 0; lpc < entries; lpc++) { + struct stat sb; + + if ('.' == namelist[lpc]->d_name[0]) { + free(namelist[lpc]); + continue; + } + + snprintf(buffer, sizeof(buffer), "%s/%s", root, namelist[lpc]->d_name); + + if (stat(buffer, &sb)) { + continue; + } + + if (S_ISDIR(sb.st_mode)) { + if (files) { + free(namelist[lpc]); + continue; + } + + } else if (S_ISREG(sb.st_mode)) { + if (files == FALSE) { + free(namelist[lpc]); + continue; + + } else if (executable + && (sb.st_mode & S_IXUSR) == 0 + && (sb.st_mode & S_IXGRP) == 0 && (sb.st_mode & S_IXOTH) == 0) { + free(namelist[lpc]); + continue; + } + } + + list = g_list_append(list, strdup(namelist[lpc]->d_name)); + + free(namelist[lpc]); + } + + free(namelist); + return list; +} + +GList * +services_os_get_directory_list(const char *root, gboolean files, gboolean executable) +{ + GList *result = NULL; + char *dirs = strdup(root); + char *dir = NULL; + + if (pcmk__str_empty(dirs)) { + free(dirs); + return result; + } + + for (dir = strtok(dirs, ":"); dir != NULL; dir = strtok(NULL, ":")) { + GList *tmp = services_os_get_single_directory_list(dir, files, executable); + + if (tmp) { + result = g_list_concat(result, tmp); + } + } + + free(dirs); + + return result; +} diff --git a/lib/services/services_lsb.c b/lib/services/services_lsb.c new file mode 100644 index 0000000..134cc70 --- /dev/null +++ b/lib/services/services_lsb.c @@ -0,0 +1,341 @@ +/* + * Copyright 2010-2022 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 <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <stdio.h> +#include <errno.h> +#include <sys/stat.h> + +#include <crm/crm.h> +#include <crm/services.h> +#include "services_private.h" +#include "services_lsb.h" + +#define lsb_metadata_template \ + "<?xml version='1.0'?>\n" \ + "<!DOCTYPE resource-agent SYSTEM 'ra-api-1.dtd'>\n" \ + "<resource-agent name='%s' version='" PCMK_DEFAULT_AGENT_VERSION "'>\n" \ + " <version>1.0</version>\n" \ + " <longdesc lang='en'>\n" \ + "%s" \ + " </longdesc>\n" \ + " <shortdesc lang='en'>%s</shortdesc>\n" \ + " <parameters>\n" \ + " </parameters>\n" \ + " <actions>\n" \ + " <action name='meta-data' timeout='5' />\n" \ + " <action name='start' timeout='15' />\n" \ + " <action name='stop' timeout='15' />\n" \ + " <action name='status' timeout='15' />\n" \ + " <action name='restart' timeout='15' />\n" \ + " <action name='force-reload' timeout='15' />\n" \ + " <action name='monitor' timeout='15' interval='15' />\n" \ + " </actions>\n" \ + " <special tag='LSB'>\n" \ + " <Provides>%s</Provides>\n" \ + " <Required-Start>%s</Required-Start>\n" \ + " <Required-Stop>%s</Required-Stop>\n" \ + " <Should-Start>%s</Should-Start>\n" \ + " <Should-Stop>%s</Should-Stop>\n" \ + " <Default-Start>%s</Default-Start>\n" \ + " <Default-Stop>%s</Default-Stop>\n" \ + " </special>\n" \ + "</resource-agent>\n" + +/* See "Comment Conventions for Init Scripts" in the LSB core specification at: + * http://refspecs.linuxfoundation.org/lsb.shtml + */ +#define LSB_INITSCRIPT_INFOBEGIN_TAG "### BEGIN INIT INFO" +#define LSB_INITSCRIPT_INFOEND_TAG "### END INIT INFO" +#define PROVIDES "# Provides:" +#define REQ_START "# Required-Start:" +#define REQ_STOP "# Required-Stop:" +#define SHLD_START "# Should-Start:" +#define SHLD_STOP "# Should-Stop:" +#define DFLT_START "# Default-Start:" +#define DFLT_STOP "# Default-Stop:" +#define SHORT_DSCR "# Short-Description:" +#define DESCRIPTION "# Description:" + +#define lsb_meta_helper_free_value(m) \ + do { \ + if ((m) != NULL) { \ + xmlFree(m); \ + (m) = NULL; \ + } \ + } while(0) + +/*! + * \internal + * \brief Grab an LSB header value + * + * \param[in] line Line read from LSB init script + * \param[in,out] value If not set, will be set to XML-safe copy of value + * \param[in] prefix Set value if line starts with this pattern + * + * \return TRUE if value was set, FALSE otherwise + */ +static inline gboolean +lsb_meta_helper_get_value(const char *line, char **value, const char *prefix) +{ + if (!*value && pcmk__starts_with(line, prefix)) { + *value = (char *)xmlEncodeEntitiesReentrant(NULL, BAD_CAST line+strlen(prefix)); + return TRUE; + } + return FALSE; +} + +int +services__get_lsb_metadata(const char *type, char **output) +{ + char ra_pathname[PATH_MAX] = { 0, }; + FILE *fp = NULL; + char buffer[1024] = { 0, }; + char *provides = NULL; + char *req_start = NULL; + char *req_stop = NULL; + char *shld_start = NULL; + char *shld_stop = NULL; + char *dflt_start = NULL; + char *dflt_stop = NULL; + char *s_dscrpt = NULL; + char *xml_l_dscrpt = NULL; + bool in_header = FALSE; + + if (type[0] == '/') { + snprintf(ra_pathname, sizeof(ra_pathname), "%s", type); + } else { + snprintf(ra_pathname, sizeof(ra_pathname), "%s/%s", + PCMK__LSB_INIT_DIR, type); + } + + crm_trace("Looking into %s", ra_pathname); + fp = fopen(ra_pathname, "r"); + if (fp == NULL) { + return -errno; + } + + /* Enter into the LSB-compliant comment block */ + while (fgets(buffer, sizeof(buffer), fp)) { + + // Ignore lines up to and including the block delimiter + if (pcmk__starts_with(buffer, LSB_INITSCRIPT_INFOBEGIN_TAG)) { + in_header = TRUE; + continue; + } + if (!in_header) { + continue; + } + + /* Assume each of the following eight arguments contain one line */ + if (lsb_meta_helper_get_value(buffer, &provides, PROVIDES)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &req_start, REQ_START)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &req_stop, REQ_STOP)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &shld_start, SHLD_START)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &shld_stop, SHLD_STOP)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &dflt_start, DFLT_START)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &dflt_stop, DFLT_STOP)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &s_dscrpt, SHORT_DSCR)) { + continue; + } + + /* Long description may cross multiple lines */ + if ((xml_l_dscrpt == NULL) // haven't already found long description + && pcmk__starts_with(buffer, DESCRIPTION)) { + bool processed_line = TRUE; + GString *desc = g_string_sized_new(2048); + + // Get remainder of description line itself + g_string_append(desc, buffer + sizeof(DESCRIPTION) - 1); + + // Read any continuation lines of the description + buffer[0] = '\0'; + while (fgets(buffer, sizeof(buffer), fp)) { + if (pcmk__starts_with(buffer, "# ") + || pcmk__starts_with(buffer, "#\t")) { + /* '#' followed by a tab or more than one space indicates a + * continuation of the long description. + */ + g_string_append(desc, buffer + 1); + } else { + /* This line is not part of the long description, + * so continue with normal processing. + */ + processed_line = FALSE; + break; + } + } + + // Make long description safe to use in XML + xml_l_dscrpt = + (char *) xmlEncodeEntitiesReentrant(NULL, + (pcmkXmlStr) desc->str); + g_string_free(desc, TRUE); + + if (processed_line) { + // We grabbed the line into the long description + continue; + } + } + + // Stop if we leave the header block + if (pcmk__starts_with(buffer, LSB_INITSCRIPT_INFOEND_TAG)) { + break; + } + if (buffer[0] != '#') { + break; + } + } + fclose(fp); + + *output = crm_strdup_printf(lsb_metadata_template, type, + (xml_l_dscrpt? xml_l_dscrpt : type), + (s_dscrpt? s_dscrpt : type), + (provides? provides : ""), + (req_start? req_start : ""), + (req_stop? req_stop : ""), + (shld_start? shld_start : ""), + (shld_stop? shld_stop : ""), + (dflt_start? dflt_start : ""), + (dflt_stop? dflt_stop : "")); + + lsb_meta_helper_free_value(xml_l_dscrpt); + lsb_meta_helper_free_value(s_dscrpt); + lsb_meta_helper_free_value(provides); + lsb_meta_helper_free_value(req_start); + lsb_meta_helper_free_value(req_stop); + lsb_meta_helper_free_value(shld_start); + lsb_meta_helper_free_value(shld_stop); + lsb_meta_helper_free_value(dflt_start); + lsb_meta_helper_free_value(dflt_stop); + + crm_trace("Created fake metadata: %llu", + (unsigned long long) strlen(*output)); + return pcmk_ok; +} + +GList * +services__list_lsb_agents(void) +{ + return services_os_get_directory_list(PCMK__LSB_INIT_DIR, TRUE, TRUE); +} + +bool +services__lsb_agent_exists(const char *agent) +{ + bool rc = FALSE; + struct stat st; + char *path = pcmk__full_path(agent, PCMK__LSB_INIT_DIR); + + rc = (stat(path, &st) == 0); + free(path); + return rc; +} + +/*! + * \internal + * \brief Prepare an LSB action + * + * \param[in,out] op Action to prepare + * + * \return Standard Pacemaker return code + */ +int +services__lsb_prepare(svc_action_t *op) +{ + op->opaque->exec = pcmk__full_path(op->agent, PCMK__LSB_INIT_DIR); + op->opaque->args[0] = strdup(op->opaque->exec); + op->opaque->args[1] = strdup(op->action); + if ((op->opaque->args[0] == NULL) || (op->opaque->args[1] == NULL)) { + return ENOMEM; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Map an LSB result to a standard OCF result + * + * \param[in] action Action that result is for + * \param[in] exit_status LSB agent exit status + * + * \return Standard OCF result + */ +enum ocf_exitcode +services__lsb2ocf(const char *action, int exit_status) +{ + // For non-status actions, LSB and OCF share error codes <= 7 + if (!pcmk__str_any_of(action, "status", "monitor", NULL)) { + if ((exit_status < 0) || (exit_status > PCMK_LSB_NOT_RUNNING)) { + return PCMK_OCF_UNKNOWN_ERROR; + } + return (enum ocf_exitcode) exit_status; + } + + // LSB status actions have their own codes + switch (exit_status) { + case PCMK_LSB_STATUS_OK: + return PCMK_OCF_OK; + + case PCMK_LSB_STATUS_NOT_INSTALLED: + return PCMK_OCF_NOT_INSTALLED; + + case PCMK_LSB_STATUS_INSUFFICIENT_PRIV: + return PCMK_OCF_INSUFFICIENT_PRIV; + + case PCMK_LSB_STATUS_VAR_PID: + case PCMK_LSB_STATUS_VAR_LOCK: + case PCMK_LSB_STATUS_NOT_RUNNING: + return PCMK_OCF_NOT_RUNNING; + + default: + return PCMK_OCF_UNKNOWN_ERROR; + } +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/services_compat.h> + +svc_action_t * +services_action_create(const char *name, const char *action, + guint interval_ms, int timeout) +{ + return resources_action_create(name, PCMK_RESOURCE_CLASS_LSB, NULL, name, + action, interval_ms, timeout, NULL, 0); +} + +GList * +services_list(void) +{ + return resources_list_agents(PCMK_RESOURCE_CLASS_LSB, NULL); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/services/services_lsb.h b/lib/services/services_lsb.h new file mode 100644 index 0000000..8174833 --- /dev/null +++ b/lib/services/services_lsb.h @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2022 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 SERVICES_LSB__H +# define SERVICES_LSB__H + +G_GNUC_INTERNAL int services__get_lsb_metadata(const char *type, char **output); +G_GNUC_INTERNAL GList *services__list_lsb_agents(void); +G_GNUC_INTERNAL bool services__lsb_agent_exists(const char *agent); +G_GNUC_INTERNAL int services__lsb_prepare(svc_action_t *op); + +G_GNUC_INTERNAL +enum ocf_exitcode services__lsb2ocf(const char *action, int exit_status); + +#endif diff --git a/lib/services/services_nagios.c b/lib/services/services_nagios.c new file mode 100644 index 0000000..abddca8 --- /dev/null +++ b/lib/services/services_nagios.c @@ -0,0 +1,220 @@ +/* + * Copyright 2010-2022 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 <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <errno.h> +#include <unistd.h> +#include <dirent.h> +#include <grp.h> +#include <string.h> +#include <sys/time.h> +#include <sys/resource.h> + +#include "crm/crm.h" +#include <crm/msg_xml.h> +#include "crm/common/mainloop.h" +#include "crm/services.h" + +#include "services_private.h" +#include "services_nagios.h" + +/*! + * \internal + * \brief Prepare a Nagios action + * + * \param[in,out] op Action to prepare + * + * \return Standard Pacemaker return code + */ +int +services__nagios_prepare(svc_action_t *op) +{ + 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) { + return ENOMEM; + } + + 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) { + return ENOMEM; + } + + } else if (op->params != NULL) { + 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)) { + return E2BIG; + } + + 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) { + return ENOMEM; + } + } + } + + // Nagios actions don't need to keep the parameters + if (op->params != NULL) { + g_hash_table_destroy(op->params); + op->params = NULL; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Map a Nagios result to a standard OCF result + * + * \param[in] exit_status Nagios exit status + * + * \return Standard OCF result + */ +enum ocf_exitcode +services__nagios2ocf(int exit_status) +{ + switch (exit_status) { + case NAGIOS_STATE_OK: + return PCMK_OCF_OK; + + case NAGIOS_INSUFFICIENT_PRIV: + return PCMK_OCF_INSUFFICIENT_PRIV; + + case NAGIOS_STATE_WARNING: + return PCMK_OCF_DEGRADED; + + case NAGIOS_STATE_CRITICAL: + case NAGIOS_STATE_UNKNOWN: + default: + return PCMK_OCF_UNKNOWN_ERROR; + } +} + +static inline char * +nagios_metadata_name(const char *plugin) +{ + return crm_strdup_printf(NAGIOS_METADATA_DIR "/%s.xml", plugin); +} + +GList * +services__list_nagios_agents(void) +{ + GList *plugin_list = NULL; + GList *result = NULL; + + plugin_list = services_os_get_directory_list(NAGIOS_PLUGIN_DIR, TRUE, TRUE); + + // Return only the plugins that have metadata + for (GList *gIter = plugin_list; gIter != NULL; gIter = gIter->next) { + struct stat st; + const char *plugin = gIter->data; + char *metadata = nagios_metadata_name(plugin); + + if (stat(metadata, &st) == 0) { + result = g_list_append(result, strdup(plugin)); + } + free(metadata); + } + g_list_free_full(plugin_list, free); + return result; +} + +gboolean +services__nagios_agent_exists(const char *name) +{ + char *buf = NULL; + gboolean rc = FALSE; + struct stat st; + + if (name == NULL) { + return rc; + } + + buf = crm_strdup_printf(NAGIOS_PLUGIN_DIR "/%s", name); + if (stat(buf, &st) == 0) { + rc = TRUE; + } + + free(buf); + return rc; +} + +int +services__get_nagios_metadata(const char *type, char **output) +{ + int rc = pcmk_ok; + FILE *file_strm = NULL; + int start = 0, length = 0, read_len = 0; + char *metadata_file = nagios_metadata_name(type); + + file_strm = fopen(metadata_file, "r"); + if (file_strm == NULL) { + crm_err("Metadata file %s does not exist", metadata_file); + free(metadata_file); + return -EIO; + } + + /* see how big the file is */ + start = ftell(file_strm); + fseek(file_strm, 0L, SEEK_END); + length = ftell(file_strm); + fseek(file_strm, 0L, start); + + CRM_ASSERT(length >= 0); + CRM_ASSERT(start == ftell(file_strm)); + + if (length <= 0) { + crm_info("%s was not valid", metadata_file); + free(*output); + *output = NULL; + rc = -EIO; + + } else { + crm_trace("Reading %d bytes from file", length); + *output = calloc(1, (length + 1)); + read_len = fread(*output, 1, length, file_strm); + if (read_len != length) { + crm_err("Calculated and read bytes differ: %d vs. %d", + length, read_len); + free(*output); + *output = NULL; + rc = -EIO; + } + } + + fclose(file_strm); + free(metadata_file); + return rc; +} diff --git a/lib/services/services_nagios.h b/lib/services/services_nagios.h new file mode 100644 index 0000000..2e447e0 --- /dev/null +++ b/lib/services/services_nagios.h @@ -0,0 +1,28 @@ +/* + * 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. + */ + +#ifndef SERVICES_NAGIOS__H +# define SERVICES_NAGIOS__H + +G_GNUC_INTERNAL +int services__nagios_prepare(svc_action_t *op); + +G_GNUC_INTERNAL +enum ocf_exitcode services__nagios2ocf(int exit_status); + +G_GNUC_INTERNAL +GList *services__list_nagios_agents(void); + +G_GNUC_INTERNAL +gboolean services__nagios_agent_exists(const char *agent); + +G_GNUC_INTERNAL +int services__get_nagios_metadata(const char *type, char **output); + +#endif diff --git a/lib/services/services_ocf.c b/lib/services/services_ocf.c new file mode 100644 index 0000000..d7fb9bd --- /dev/null +++ b/lib/services/services_ocf.c @@ -0,0 +1,179 @@ +/* + * Copyright 2012-2022 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 <crm_internal.h> + +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> + +#include <crm/crm.h> +#include <crm/services.h> +#include <crm/services_internal.h> + +#include "services_private.h" +#include "services_ocf.h" + +GList * +resources_os_list_ocf_providers(void) +{ + return get_directory_list(OCF_RA_PATH, FALSE, TRUE); +} + +static GList * +services_os_get_directory_list_provider(const char *root, const char *provider, + gboolean files, gboolean executable) +{ + GList *result = NULL; + char *dirs = strdup(root); + char *dir = NULL; + char buffer[PATH_MAX]; + + if (pcmk__str_empty(dirs)) { + free(dirs); + return result; + } + + for (dir = strtok(dirs, ":"); dir != NULL; dir = strtok(NULL, ":")) { + GList *tmp = NULL; + + sprintf(buffer, "%s/%s", dir, provider); + tmp = services_os_get_single_directory_list(buffer, files, executable); + + if (tmp) { + result = g_list_concat(result, tmp); + } + } + + free(dirs); + + return result; +} + +GList * +resources_os_list_ocf_agents(const char *provider) +{ + GList *gIter = NULL; + GList *result = NULL; + GList *providers = NULL; + + if (provider) { + return services_os_get_directory_list_provider(OCF_RA_PATH, provider, + TRUE, TRUE); + } + + providers = resources_os_list_ocf_providers(); + for (gIter = providers; gIter != NULL; gIter = gIter->next) { + GList *tmp1 = result; + GList *tmp2 = resources_os_list_ocf_agents(gIter->data); + + if (tmp2) { + result = g_list_concat(tmp1, tmp2); + } + } + g_list_free_full(providers, free); + return result; +} + +gboolean +services__ocf_agent_exists(const char *provider, const char *agent) +{ + gboolean rc = FALSE; + struct stat st; + char *dirs = strdup(OCF_RA_PATH); + char *dir = NULL; + char *buf = NULL; + + if (provider == NULL || agent == NULL || pcmk__str_empty(dirs)) { + free(dirs); + return rc; + } + + for (dir = strtok(dirs, ":"); dir != NULL; dir = strtok(NULL, ":")) { + buf = crm_strdup_printf("%s/%s/%s", dir, provider, agent); + if (stat(buf, &st) == 0) { + free(buf); + rc = TRUE; + break; + } + + free(buf); + } + + free(dirs); + + return rc; +} + +/*! + * \internal + * \brief Prepare an OCF action + * + * \param[in,out] op Action to prepare + * + * \return Standard Pacemaker return code + */ +int +services__ocf_prepare(svc_action_t *op) +{ + char *dirs = strdup(OCF_RA_PATH); + struct stat st; + + if (dirs == NULL) { + return ENOMEM; + } + + // Look for agent on path + for (char *dir = strtok(dirs, ":"); dir != NULL; dir = strtok(NULL, ":")) { + char *buf = crm_strdup_printf("%s/%s/%s", dir, op->provider, op->agent); + + if (stat(buf, &st) == 0) { + op->opaque->exec = buf; + break; + } + free(buf); + } + free(dirs); + + if (op->opaque->exec == NULL) { + return ENOENT; + } + + op->opaque->args[0] = strdup(op->opaque->exec); + op->opaque->args[1] = strdup(op->action); + if ((op->opaque->args[0] == NULL) || (op->opaque->args[1] == NULL)) { + return ENOMEM; + } + + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Map an actual OCF result to a standard OCF result + * + * \param[in] exit_status Actual OCF agent exit status + * + * \return Standard OCF result + */ +enum ocf_exitcode +services__ocf2ocf(int exit_status) +{ + switch (exit_status) { + case PCMK_OCF_DEGRADED: + case PCMK_OCF_DEGRADED_PROMOTED: + break; + default: + if ((exit_status < 0) || (exit_status > PCMK_OCF_FAILED_PROMOTED)) { + exit_status = PCMK_OCF_UNKNOWN_ERROR; + } + break; + } + return (enum ocf_exitcode) exit_status; +} diff --git a/lib/services/services_ocf.h b/lib/services/services_ocf.h new file mode 100644 index 0000000..1c40552 --- /dev/null +++ b/lib/services/services_ocf.h @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2011 Red Hat, Inc. + * Later changes 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 PCMK__SERVICES_OCF__H +#define PCMK__SERVICES_OCF__H + +#include <glib.h> + +G_GNUC_INTERNAL +GList *resources_os_list_ocf_providers(void); + +G_GNUC_INTERNAL +GList *resources_os_list_ocf_agents(const char *provider); + +G_GNUC_INTERNAL +gboolean services__ocf_agent_exists(const char *provider, const char *agent); + +G_GNUC_INTERNAL +int services__ocf_prepare(svc_action_t *op); + +G_GNUC_INTERNAL +enum ocf_exitcode services__ocf2ocf(int exit_status); + +#endif // PCMK__SERVICES_OCF__H diff --git a/lib/services/services_private.h b/lib/services/services_private.h new file mode 100644 index 0000000..48269b8 --- /dev/null +++ b/lib/services/services_private.h @@ -0,0 +1,101 @@ +/* + * Copyright 2010-2011 Red Hat, Inc. + * Later changes copyright 2012-2022 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 SERVICES_PRIVATE__H +# define SERVICES_PRIVATE__H + +# include <glib.h> +# include "crm/services.h" + +#if HAVE_DBUS +# include <dbus/dbus.h> +#endif + +#define MAX_ARGC 255 +struct svc_action_private_s { + char *exec; + char *exit_reason; + char *args[MAX_ARGC]; + + uid_t uid; + gid_t gid; + + guint repeat_timer; + void (*callback) (svc_action_t * op); + void (*fork_callback) (svc_action_t * op); + + int stderr_fd; + mainloop_io_t *stderr_gsource; + + int stdout_fd; + mainloop_io_t *stdout_gsource; + + int stdin_fd; +#if HAVE_DBUS + DBusPendingCall* pending; + unsigned timerid; +#endif +}; + +G_GNUC_INTERNAL +const char *services__action_kind(const svc_action_t *action); + +G_GNUC_INTERNAL +GList *services_os_get_single_directory_list(const char *root, gboolean files, + gboolean executable); + +G_GNUC_INTERNAL +GList *services_os_get_directory_list(const char *root, gboolean files, gboolean executable); + +G_GNUC_INTERNAL +int services__execute_file(svc_action_t *op); + +G_GNUC_INTERNAL +gboolean cancel_recurring_action(svc_action_t * op); + +G_GNUC_INTERNAL +gboolean recurring_action_timer(gpointer data); + +G_GNUC_INTERNAL +int services__finalize_async_op(svc_action_t *op); + +G_GNUC_INTERNAL +int services__generic_error(const svc_action_t *op); + +G_GNUC_INTERNAL +int services__not_installed_error(const svc_action_t *op); + +G_GNUC_INTERNAL +int services__authorization_error(const svc_action_t *op); + +G_GNUC_INTERNAL +int services__configuration_error(const svc_action_t *op, bool is_fatal); + +G_GNUC_INTERNAL +void services__handle_exec_error(svc_action_t * op, int error); + +G_GNUC_INTERNAL +void services__set_cancelled(svc_action_t *action); + +G_GNUC_INTERNAL +void services_add_inflight_op(svc_action_t *op); + +G_GNUC_INTERNAL +void services_untrack_op(const svc_action_t *op); + +G_GNUC_INTERNAL +gboolean is_op_blocked(const char *rsc); + +#if HAVE_DBUS +G_GNUC_INTERNAL +void services_set_op_pending(svc_action_t *op, DBusPendingCall *pending); +#endif + +#endif /* SERVICES_PRIVATE__H */ diff --git a/lib/services/systemd.c b/lib/services/systemd.c new file mode 100644 index 0000000..0c38ae0 --- /dev/null +++ b/lib/services/systemd.c @@ -0,0 +1,1100 @@ +/* + * Copyright 2012-2022 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 <crm_internal.h> +#include <crm/crm.h> +#include <crm/services.h> +#include <crm/services_internal.h> +#include <crm/common/mainloop.h> + +#include <sys/stat.h> +#include <gio/gio.h> +#include <services_private.h> +#include <systemd.h> +#include <dbus/dbus.h> +#include <pcmk-dbus.h> + +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,out] 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; +} + +/*! + * \internal + * \brief Map a systemd result to a standard OCF result + * + * \param[in] exit_status Systemd result + * + * \return Standard OCF result + */ +enum ocf_exitcode +services__systemd2ocf(int exit_status) +{ + // This library uses OCF codes for systemd actions + return (enum ocf_exitcode) exit_status; +} + +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, bool add_instance_name) +{ + const char *dot = NULL; + + if (pcmk__str_empty(name)) { + return NULL; + } + + /* Services that end with an @ sign are systemd templates. They expect an + * instance name to follow the service name. If no instance name was + * provided, just add "pacemaker" to the string as the instance name. It + * doesn't seem to matter for purposes of looking up whether a service + * exists or not. + * + * A template can be specified either with or without the unit extension, + * so this block handles both cases. + */ + dot = systemd_unit_extension(name); + + if (dot) { + if (dot != name && *(dot-1) == '@') { + char *s = NULL; + + if (asprintf(&s, "%.*spacemaker%s", (int) (dot-name), name, dot) == -1) { + /* If asprintf fails, just return name. */ + return strdup(name); + } + + return s; + } else { + return strdup(name); + } + + } else if (add_instance_name && *(name+strlen(name)-1) == '@') { + return crm_strdup_printf("%spacemaker.service", name); + + } else { + 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_warn("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,out] 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, pcmk__s(op->rsc, "unknown resource")); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + return; + } + + services__format_result(op, PCMK_OCF_NOT_INSTALLED, + PCMK_EXEC_NOT_INSTALLED, + "systemd unit %s not found", op->agent); + } + + crm_info("DBus request for %s of systemd unit %s%s%s failed: %s", + op->action, op->agent, + ((op->rsc == NULL)? "" : " for resource "), pcmk__s(op->rsc, ""), + error->message); +} + +/*! + * \internal + * \brief Extract unit path from LoadUnit reply, and execute action + * + * \param[in] reply LoadUnit reply + * \param[in,out] 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_info("Could not load systemd unit %s for %s: " + "DBus reply has unexpected type", op->agent, op->id); + } else { + crm_info("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__format_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "No DBus object found for systemd unit %s", + op->agent); + services__finalize_async_op(op); + } + } + + return path; +} + +/*! + * \internal + * \brief Execute a systemd action after its LoadUnit completes + * + * \param[in,out] pending If not NULL, DBus call associated with LoadUnit + * \param[in,out] 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 without ".service" extension) + * \param[in,out] op Action to execute (if NULL, just get 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 path. + */ +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. + * + * <method name="LoadUnit"> + * <arg name="name" type="s" direction="in"/> + * <arg name="unit" type="o" direction="out"/> + * </method> + */ + msg = systemd_new_method("LoadUnit"); + CRM_ASSERT(msg != NULL); + + // Add the (expanded) unit name as the argument + name = systemd_service_name(arg_name, op == NULL || pcmk__str_eq(op->action, "meta-data", pcmk__str_none)); + 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; + } + +/* + " <method name=\"ListUnitFiles\">\n" \ + " <arg name=\"files\" type=\"a(ss)\" direction=\"out\"/>\n" \ + " </method>\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); + free(path); + if (pcmk__str_any_of(state, "loaded", "masked", NULL)) { + free(state); + return TRUE; + } + free(state); + return FALSE; +} + +#define METADATA_FORMAT \ + "<?xml version=\"1.0\"?>\n" \ + "<!DOCTYPE resource-agent SYSTEM \"ra-api-1.dtd\">\n" \ + "<resource-agent name=\"%s\" version=\"" PCMK_DEFAULT_AGENT_VERSION "\">\n" \ + " <version>1.1</version>\n" \ + " <longdesc lang=\"en\">\n" \ + " %s\n" \ + " </longdesc>\n" \ + " <shortdesc lang=\"en\">systemd unit file for %s</shortdesc>\n" \ + " <parameters/>\n" \ + " <actions>\n" \ + " <action name=\"start\" timeout=\"100\" />\n" \ + " <action name=\"stop\" timeout=\"100\" />\n" \ + " <action name=\"status\" timeout=\"100\" />\n" \ + " <action name=\"monitor\" timeout=\"100\" interval=\"60\"/>\n" \ + " <action name=\"meta-data\" timeout=\"5\" />\n" \ + " </actions>\n" \ + " <special tag=\"systemd\"/>\n" \ + "</resource-agent>\n" + +static char * +systemd_unit_metadata(const char *name, int timeout) +{ + char *meta = NULL; + char *desc = NULL; + char *path = NULL; + + char *escaped = 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); + } + + escaped = crm_xml_escape(desc); + + meta = crm_strdup_printf(METADATA_FORMAT, name, escaped, name); + free(desc); + free(path); + free(escaped); + return meta; +} + +/*! + * \internal + * \brief Determine result of method from reply + * + * \param[in] reply Reply to start, stop, or restart request + * \param[in,out] 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_info("DBus request for %s of %s succeeded but " + "return type was unexpected", + op->action, pcmk__s(op->rsc, "unknown resource")); + 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, pcmk__s(op->rsc, "unknown resource"), 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,out] pending If not NULL, DBus call associated with request + * \param[in,out] 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,out] 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'", + pcmk__s(op->rsc, "(unspecified)"), name, + pcmk__s(state, "<null>")); + + 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,out] 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__format_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "Could not get state for unit %s from DBus", + op->agent); + 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__format_result(op, PCMK_OCF_UNIMPLEMENT_FEATURE, + PCMK_EXEC_ERROR, + "Action %s not implemented " + "for systemd resources", + pcmk__s(op->action, "(unspecified)")); + if (!(op->synchronous)) { + services__finalize_async_op(op); + } + return; + } + + crm_trace("Calling %s for unit path %s%s%s", + method, unit, + ((op->rsc == NULL)? "" : " for resource "), pcmk__s(op->rsc, "")); + + msg = systemd_new_method(method); + CRM_ASSERT(msg != NULL); + + /* (ss) */ + { + const char *replace_s = "replace"; + char *name = systemd_service_name(op->agent, pcmk__str_eq(op->action, "meta-data", pcmk__str_none)); + + 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_info("%s action for systemd unit %s named '%s' timed out", + op->action, op->agent, op->rsc); + services__format_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_TIMEOUT, + "%s action for systemd unit %s " + "did not complete in time", op->action, op->agent); + services__finalize_async_op(op); + return FALSE; +} + +/*! + * \internal + * \brief Execute a systemd action + * + * \param[in,out] op Action to execute + * + * \return Standard Pacemaker return code + * \retval EBUSY Recurring operation could not be initiated + * \retval pcmk_rc_error Synchronous action failed + * \retval pcmk_rc_ok Synchronous action succeeded, or asynchronous action + * should not be freed (because it's pending or because + * it failed to execute and was already freed) + * + * \note If the return value for an asynchronous action is not pcmk_rc_ok, the + * caller is responsible for freeing the action. + */ +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%s%s", + (op->synchronous? "" : "a"), op->action, op->agent, + ((op->rsc == NULL)? "" : " for resource "), pcmk__s(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 new file mode 100644 index 0000000..0d3dbe6 --- /dev/null +++ b/lib/services/systemd.h @@ -0,0 +1,30 @@ +/* + * 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 <glib.h> +# 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 +enum ocf_exitcode services__systemd2ocf(int exit_status); + +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 */ diff --git a/lib/services/upstart.c b/lib/services/upstart.c new file mode 100644 index 0000000..459b572 --- /dev/null +++ b/lib/services/upstart.c @@ -0,0 +1,701 @@ +/* + * Original copyright 2010 Senko Rasic <senko.rasic@dobarkod.hr> + * and Ante Karamatic <ivoks@init.hr> + * Later changes copyright 2012-2022 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 <crm_internal.h> + +#include <stdio.h> + +#include <crm/crm.h> +#include <crm/services.h> +#include <crm/common/mainloop.h> + +#include <services_private.h> +#include <upstart.h> +#include <dbus/dbus.h> +#include <pcmk-dbus.h> + +#include <glib.h> +#include <gio/gio.h> + +#define BUS_NAME "com.ubuntu.Upstart" +#define BUS_PATH "/com/ubuntu/Upstart" + +#define UPSTART_06_API BUS_NAME"0_6" +#define UPSTART_JOB_IFACE UPSTART_06_API".Job" +#define BUS_PROPERTY_IFACE "org.freedesktop.DBus.Properties" + +/* + http://upstart.ubuntu.com/wiki/DBusInterface +*/ +static DBusConnection *upstart_proxy = NULL; + +/*! + * \internal + * \brief Prepare an Upstart action + * + * \param[in,out] op Action to prepare + * + * \return Standard Pacemaker return code + */ +int +services__upstart_prepare(svc_action_t *op) +{ + op->opaque->exec = strdup("upstart-dbus"); + if (op->opaque->exec == NULL) { + return ENOMEM; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Map a Upstart result to a standard OCF result + * + * \param[in] exit_status Upstart result + * + * \return Standard OCF result + */ +enum ocf_exitcode +services__upstart2ocf(int exit_status) +{ + // This library uses OCF codes for Upstart actions + return (enum ocf_exitcode) exit_status; +} + +static gboolean +upstart_init(void) +{ + static int need_init = 1; + + if (need_init) { + need_init = 0; + upstart_proxy = pcmk_dbus_connect(); + } + if (upstart_proxy == NULL) { + return FALSE; + } + return TRUE; +} + +void +upstart_cleanup(void) +{ + if (upstart_proxy) { + pcmk_dbus_disconnect(upstart_proxy); + upstart_proxy = NULL; + } +} + +/*! + * \internal + * \brief Get the DBus object path corresponding to a job name + * + * \param[in] arg_name Name of job to get path for + * \param[out] path If not NULL, where to store DBus object path + * \param[in] timeout Give up after this many seconds + * + * \return true if object path was found, false otherwise + * \note The caller is responsible for freeing *path if it is non-NULL. + */ +static bool +object_path_for_job(const gchar *arg_name, char **path, int timeout) +{ + /* + com.ubuntu.Upstart0_6.GetJobByName (in String name, out ObjectPath job) + */ + DBusError error; + DBusMessage *msg; + DBusMessage *reply = NULL; + bool rc = false; + + if (path != NULL) { + *path = NULL; + } + + if (!upstart_init()) { + return false; + } + msg = dbus_message_new_method_call(BUS_NAME, // target for the method call + BUS_PATH, // object to call on + UPSTART_06_API, // interface to call on + "GetJobByName"); // method name + + dbus_error_init(&error); + CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_STRING, &arg_name, + DBUS_TYPE_INVALID)); + reply = pcmk_dbus_send_recv(msg, upstart_proxy, &error, timeout); + dbus_message_unref(msg); + + if (dbus_error_is_set(&error)) { + crm_err("Could not get DBus object path for %s: %s", + arg_name, error.message); + dbus_error_free(&error); + + } else if (!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, + __func__, __LINE__)) { + crm_err("Could not get DBus object path for %s: Invalid return type", + arg_name); + + } else { + if (path != NULL) { + dbus_message_get_args(reply, NULL, DBUS_TYPE_OBJECT_PATH, path, + DBUS_TYPE_INVALID); + if (*path != NULL) { + *path = strdup(*path); + } + } + rc = true; + } + + if (reply != NULL) { + dbus_message_unref(reply); + } + return rc; +} + +static void +fix(char *input, const char *search, char replace) +{ + char *match = NULL; + int shuffle = strlen(search) - 1; + + while (TRUE) { + int len, lpc; + + match = strstr(input, search); + if (match == NULL) { + break; + } + crm_trace("Found: %s", match); + match[0] = replace; + len = strlen(match) - shuffle; + for (lpc = 1; lpc <= len; lpc++) { + match[lpc] = match[lpc + shuffle]; + } + } +} + +static char * +fix_upstart_name(const char *input) +{ + char *output = strdup(input); + + fix(output, "_2b", '+'); + fix(output, "_2c", ','); + fix(output, "_2d", '-'); + fix(output, "_2e", '.'); + fix(output, "_40", '@'); + fix(output, "_5f", '_'); + return output; +} + +GList * +upstart_job_listall(void) +{ + GList *units = NULL; + DBusMessageIter args; + DBusMessageIter unit; + DBusMessage *msg = NULL; + DBusMessage *reply = NULL; + const char *method = "GetAllJobs"; + DBusError error; + int lpc = 0; + + if (upstart_init() == FALSE) { + return NULL; + } + +/* + com.ubuntu.Upstart0_6.GetAllJobs (out <Array of ObjectPath> jobs) +*/ + + dbus_error_init(&error); + msg = dbus_message_new_method_call(BUS_NAME, // target for the method call + BUS_PATH, // object to call on + UPSTART_06_API, // interface to call on + method); // method name + CRM_ASSERT(msg != NULL); + + reply = pcmk_dbus_send_recv(msg, upstart_proxy, &error, DBUS_TIMEOUT_USE_DEFAULT); + dbus_message_unref(msg); + + if (dbus_error_is_set(&error)) { + crm_err("Call to %s failed: %s", method, error.message); + dbus_error_free(&error); + return NULL; + + } else if (!dbus_message_iter_init(reply, &args)) { + crm_err("Call to %s failed: Message has no arguments", method); + dbus_message_unref(reply); + return NULL; + } + + if(!pcmk_dbus_type_check(reply, &args, DBUS_TYPE_ARRAY, __func__, __LINE__)) { + crm_err("Call to %s failed: Message has invalid arguments", method); + dbus_message_unref(reply); + return NULL; + } + + dbus_message_iter_recurse(&args, &unit); + while (dbus_message_iter_get_arg_type (&unit) != DBUS_TYPE_INVALID) { + DBusBasicValue value; + const char *job = NULL; + char *path = NULL; + + if(!pcmk_dbus_type_check(reply, &unit, DBUS_TYPE_OBJECT_PATH, __func__, __LINE__)) { + crm_warn("Skipping Upstart reply argument with unexpected type"); + continue; + } + + dbus_message_iter_get_basic(&unit, &value); + + if(value.str) { + int llpc = 0; + path = value.str; + job = value.str; + while (path[llpc] != 0) { + if (path[llpc] == '/') { + job = path + llpc + 1; + } + llpc++; + } + lpc++; + crm_trace("%s -> %s", path, job); + units = g_list_append(units, fix_upstart_name(job)); + } + dbus_message_iter_next (&unit); + } + + dbus_message_unref(reply); + crm_trace("Found %d upstart jobs", lpc); + return units; +} + +gboolean +upstart_job_exists(const char *name) +{ + return object_path_for_job(name, NULL, DBUS_TIMEOUT_USE_DEFAULT); +} + +static char * +get_first_instance(const gchar * job, int timeout) +{ + char *instance = NULL; + const char *method = "GetAllInstances"; + DBusError error; + DBusMessage *msg; + DBusMessage *reply; + DBusMessageIter args; + DBusMessageIter unit; + + dbus_error_init(&error); + msg = dbus_message_new_method_call(BUS_NAME, // target for the method call + job, // object to call on + UPSTART_JOB_IFACE, // interface to call on + method); // method name + CRM_ASSERT(msg != NULL); + + dbus_message_append_args(msg, DBUS_TYPE_INVALID); + reply = pcmk_dbus_send_recv(msg, upstart_proxy, &error, timeout); + dbus_message_unref(msg); + + if (dbus_error_is_set(&error)) { + crm_info("Call to %s failed: %s", method, error.message); + dbus_error_free(&error); + goto done; + + } else if(reply == NULL) { + crm_info("Call to %s failed: no reply", method); + goto done; + + } else if (!dbus_message_iter_init(reply, &args)) { + crm_info("Call to %s failed: Message has no arguments", method); + goto done; + } + + if(!pcmk_dbus_type_check(reply, &args, DBUS_TYPE_ARRAY, __func__, __LINE__)) { + crm_info("Call to %s failed: Message has invalid arguments", method); + goto done; + } + + dbus_message_iter_recurse(&args, &unit); + if(pcmk_dbus_type_check(reply, &unit, DBUS_TYPE_OBJECT_PATH, __func__, __LINE__)) { + DBusBasicValue value; + + dbus_message_iter_get_basic(&unit, &value); + + if(value.str) { + instance = strdup(value.str); + crm_trace("Result: %s", instance); + } + } + + done: + if(reply) { + dbus_message_unref(reply); + } + return instance; +} + +/*! + * \internal + * \brief Parse result of Upstart status check + * + * \param[in] name DBus interface name for property that was checked + * \param[in] state Property value + * \param[in,out] 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; + + if (pcmk__str_eq(state, "running", pcmk__str_none)) { + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, 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); + } +} + +#define METADATA_FORMAT \ + "<?xml version=\"1.0\"?>\n" \ + "<!DOCTYPE resource-agent SYSTEM \"ra-api-1.dtd\">\n" \ + "<resource-agent name=\"%s\" version=\"" PCMK_DEFAULT_AGENT_VERSION "\">\n" \ + " <version>1.1</version>\n" \ + " <longdesc lang=\"en\">\n" \ + " Upstart agent for controlling the system %s service\n" \ + " </longdesc>\n" \ + " <shortdesc lang=\"en\">Upstart job for %s</shortdesc>\n" \ + " <parameters/>\n" \ + " <actions>\n" \ + " <action name=\"start\" timeout=\"15\" />\n" \ + " <action name=\"stop\" timeout=\"15\" />\n" \ + " <action name=\"status\" timeout=\"15\" />\n" \ + " <action name=\"restart\" timeout=\"15\" />\n" \ + " <action name=\"monitor\" timeout=\"15\" interval=\"15\" start-delay=\"15\" />\n" \ + " <action name=\"meta-data\" timeout=\"5\" />\n" \ + " </actions>\n" \ + " <special tag=\"upstart\"/>\n" \ + "</resource-agent>\n" + +static char * +upstart_job_metadata(const char *name) +{ + return crm_strdup_printf(METADATA_FORMAT, name, name, name); +} + +/*! + * \internal + * \brief Set an action result based on a method error + * + * \param[in,out] 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 Upstart DBus method"); + + if (strstr(error->name, UPSTART_06_API ".Error.UnknownInstance")) { + + if (pcmk__str_eq(op->action, "stop", pcmk__str_casei)) { + crm_trace("Masking stop failure (%s) for %s " + "because unknown service can be considered stopped", + error->name, pcmk__s(op->rsc, "unknown resource")); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + return; + } + + services__set_result(op, PCMK_OCF_NOT_INSTALLED, + PCMK_EXEC_NOT_INSTALLED, "Upstart job not found"); + + } else if (pcmk__str_eq(op->action, "start", pcmk__str_casei) + && strstr(error->name, UPSTART_06_API ".Error.AlreadyStarted")) { + crm_trace("Masking start failure (%s) for %s " + "because already started resource is OK", + error->name, pcmk__s(op->rsc, "unknown resource")); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + return; + } + + crm_info("DBus request for %s of Upstart job %s for resource %s failed: %s", + op->action, op->agent, pcmk__s(op->rsc, "with unknown name"), + error->message); +} + +/*! + * \internal + * \brief Process the completion of an asynchronous job start, stop, or restart + * + * \param[in,out] pending If not NULL, DBus call associated with request + * \param[in,out] user_data Action that was executed + */ +static void +job_method_complete(DBusPendingCall *pending, void *user_data) +{ + DBusError error; + DBusMessage *reply = NULL; + svc_action_t *op = user_data; + + // Grab the reply + if (pending != NULL) { + reply = dbus_pending_call_steal_reply(pending); + } + + // Determine result + dbus_error_init(&error); + if (pcmk_dbus_find_error(pending, reply, &error)) { + set_result_from_method_error(op, &error); + dbus_error_free(&error); + + } else if (pcmk__str_eq(op->action, "stop", pcmk__str_none)) { + // Call has no return value + crm_debug("DBus request for stop of %s succeeded", + pcmk__s(op->rsc, "unknown resource")); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + + } else if (!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, + __func__, __LINE__)) { + crm_info("DBus request for %s of %s succeeded but " + "return type was unexpected", op->action, + pcmk__s(op->rsc, "unknown resource")); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + + } 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, pcmk__s(op->rsc, "unknown resource"), path); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + } + + // The call is no longer pending + CRM_LOG_ASSERT(pending == op->opaque->pending); + services_set_op_pending(op, NULL); + + // Finalize action + services__finalize_async_op(op); + if (reply != NULL) { + dbus_message_unref(reply); + } +} + +/*! + * \internal + * \brief Execute an Upstart action + * + * \param[in,out] op Action to execute + * + * \return Standard Pacemaker return code + * \retval EBUSY Recurring operation could not be initiated + * \retval pcmk_rc_error Synchronous action failed + * \retval pcmk_rc_ok Synchronous action succeeded, or asynchronous action + * should not be freed (because it's pending or because + * it failed to execute and was already freed) + * + * \note If the return value for an asynchronous action is not pcmk_rc_ok, the + * caller is responsible for freeing the action. + */ +int +services__execute_upstart(svc_action_t *op) +{ + char *job = NULL; + int arg_wait = TRUE; + const char *arg_env = "pacemaker=1"; + const char *action = op->action; + + DBusError error; + DBusMessage *msg = NULL; + DBusMessage *reply = NULL; + DBusMessageIter iter, array_iter; + + 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 cleanup; + } + + if (!upstart_init()) { + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "No DBus connection"); + goto cleanup; + } + + if (pcmk__str_eq(op->action, "meta-data", pcmk__str_casei)) { + op->stdout_data = upstart_job_metadata(op->agent); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + goto cleanup; + } + + if (!object_path_for_job(op->agent, &job, op->timeout)) { + if (pcmk__str_eq(action, "stop", pcmk__str_none)) { + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + } else { + services__set_result(op, PCMK_OCF_NOT_INSTALLED, + PCMK_EXEC_NOT_INSTALLED, + "Upstart job not found"); + } + goto cleanup; + } + + if (job == NULL) { + // Shouldn't normally be possible -- maybe a memory error + op->rc = PCMK_OCF_UNKNOWN_ERROR; + op->status = PCMK_EXEC_ERROR; + goto cleanup; + } + + if (pcmk__strcase_any_of(op->action, "monitor", "status", NULL)) { + DBusPendingCall *pending = NULL; + char *state = NULL; + char *path = get_first_instance(job, op->timeout); + + services__set_result(op, PCMK_OCF_NOT_RUNNING, PCMK_EXEC_DONE, + "No Upstart job instances found"); + if (path == NULL) { + goto cleanup; + } + state = pcmk_dbus_get_property(upstart_proxy, BUS_NAME, path, + UPSTART_06_API ".Instance", "state", + op->synchronous? NULL : parse_status_result, + op, + op->synchronous? NULL : &pending, + op->timeout); + free(path); + + if (op->synchronous) { + parse_status_result("state", state, op); + free(state); + + } else if (pending == NULL) { + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "Could not get job state from DBus"); + + } else { // Successfully initiated async op + free(job); + services_set_op_pending(op, pending); + services_add_inflight_op(op); + return pcmk_rc_ok; + } + + goto cleanup; + + } else if (pcmk__str_eq(action, "start", pcmk__str_none)) { + action = "Start"; + + } else if (pcmk__str_eq(action, "stop", pcmk__str_none)) { + action = "Stop"; + + } else if (pcmk__str_eq(action, "restart", pcmk__str_none)) { + action = "Restart"; + + } else { + services__set_result(op, PCMK_OCF_UNIMPLEMENT_FEATURE, + PCMK_EXEC_ERROR_HARD, + "Action not implemented for Upstart resources"); + goto cleanup; + } + + // Initialize rc/status in case called functions don't set them + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_DONE, + "Bug in service library"); + + crm_debug("Calling %s for %s on %s", + action, pcmk__s(op->rsc, "unknown resource"), job); + + msg = dbus_message_new_method_call(BUS_NAME, // target for the method call + job, // object to call on + UPSTART_JOB_IFACE, // interface to call on + action); // method name + CRM_ASSERT(msg != NULL); + + dbus_message_iter_init_append (msg, &iter); + CRM_LOG_ASSERT(dbus_message_iter_open_container(&iter, + DBUS_TYPE_ARRAY, + DBUS_TYPE_STRING_AS_STRING, + &array_iter)); + CRM_LOG_ASSERT(dbus_message_iter_append_basic(&array_iter, + DBUS_TYPE_STRING, &arg_env)); + CRM_LOG_ASSERT(dbus_message_iter_close_container(&iter, &array_iter)); + CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_BOOLEAN, &arg_wait, + DBUS_TYPE_INVALID)); + + if (!(op->synchronous)) { + DBusPendingCall *pending = pcmk_dbus_send(msg, upstart_proxy, + job_method_complete, op, + op->timeout); + + if (pending == NULL) { + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "Unable to send DBus message"); + goto cleanup; + + } else { // Successfully initiated async op + free(job); + services_set_op_pending(op, pending); + services_add_inflight_op(op); + return pcmk_rc_ok; + } + } + + // Synchronous call + + dbus_error_init(&error); + reply = pcmk_dbus_send_recv(msg, upstart_proxy, &error, op->timeout); + + if (dbus_error_is_set(&error)) { + set_result_from_method_error(op, &error); + dbus_error_free(&error); + + } else if (pcmk__str_eq(op->action, "stop", pcmk__str_none)) { + // DBus call does not return a value + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + + } else if (!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, + __func__, __LINE__)) { + crm_info("Call to %s passed but return type was unexpected", + op->action); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + + } else { + const char *path = NULL; + + dbus_message_get_args(reply, NULL, DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID); + crm_debug("Call to %s passed: %s", op->action, path); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + } + +cleanup: + free(job); + if (msg != NULL) { + dbus_message_unref(msg); + } + if (reply != NULL) { + dbus_message_unref(reply); + } + + 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/upstart.h b/lib/services/upstart.h new file mode 100644 index 0000000..b6c4eff --- /dev/null +++ b/lib/services/upstart.h @@ -0,0 +1,31 @@ +/* + * Copyright 2010 Senko Rasic <senko.rasic@dobarkod.hr> + * Copyright 2010 Ante Karamatic <ivoks@init.hr> + * Later changes 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 UPSTART__H +# define UPSTART__H + +# include <glib.h> +# include "crm/services.h" + +G_GNUC_INTERNAL GList *upstart_job_listall(void); + +G_GNUC_INTERNAL +int services__upstart_prepare(svc_action_t *op); + +G_GNUC_INTERNAL +enum ocf_exitcode services__upstart2ocf(int exit_status); + +G_GNUC_INTERNAL +int services__execute_upstart(svc_action_t *op); + +G_GNUC_INTERNAL gboolean upstart_job_exists(const gchar * name); +G_GNUC_INTERNAL void upstart_cleanup(void); + +#endif /* UPSTART__H */ |