diff --git a/lib/services/dbus.c b/lib/services/dbus.c index 229e12b6bf..57fcead124 100644 --- a/lib/services/dbus.c +++ b/lib/services/dbus.c @@ -1,666 +1,777 @@ /* - * Copyright (C) 2014-2016 Andrew Beekhof + * Copyright 2014-2020 the Pacemaker project contributors + * + * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include -#define BUS_PROPERTY_IFACE "org.freedesktop.DBus.Properties" +/* + * DBus message dispatch + */ +// List of DBus connections (DBusConnection*) with messages available static GList *conn_dispatches = NULL; -struct db_getall_data { - char *name; - char *target; - char *object; - void *userdata; - void (*callback)(const char *name, const char *value, void *userdata); +/*! + * \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] 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 0x%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 0x%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 -free_db_getall_data(struct db_getall_data *data) +toggle_dbus_watch(DBusWatch *watch, void *data) { - free(data->target); - free(data->object); - free(data->name); - free(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 System DBus: %s", err.message); + crm_err("Could not connect to DBus: %s", err.message); dbus_error_free(&err); return NULL; } - if(connection) { - pcmk_dbus_connection_setup_with_select(connection); + 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(DBusPendingCall *pending, DBusMessage *reply, DBusError *ret) { DBusError error; dbus_error_init(&error); - if(pending == NULL) { - dbus_set_error_const(&error, "org.clusterlabs.pacemaker.NoRequest", - "No request sent"); + if (pending == NULL) { + dbus_set_error_const(&error, ERR_NO_REQUEST, "No request sent"); - } else if(reply == NULL) { - dbus_set_error_const(&error, "org.clusterlabs.pacemaker.NoReply", - "No reply"); + } else if (reply == NULL) { + dbus_set_error_const(&error, ERR_NO_REPLY, "No reply"); } else { DBusMessageIter args; int dtype = dbus_message_get_type(reply); - char *sig; - switch(dtype) { + switch (dtype) { case DBUS_MESSAGE_TYPE_METHOD_RETURN: - dbus_message_iter_init(reply, &args); - sig = dbus_message_iter_get_signature(&args); - crm_trace("DBus call returned output args '%s'", sig); - dbus_free(sig); + { + 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, - "org.clusterlabs.pacemaker.InvalidReply", + dbus_set_error_const(&error, ERR_INVALID_REPLY, "Invalid reply"); break; case DBUS_MESSAGE_TYPE_METHOD_CALL: - dbus_set_error_const(&error, - "org.clusterlabs.pacemaker.InvalidReply.Method", + dbus_set_error_const(&error, ERR_INVALID_REPLY_METHOD, "Invalid reply (method call)"); break; case DBUS_MESSAGE_TYPE_SIGNAL: - dbus_set_error_const(&error, - "org.clusterlabs.pacemaker.InvalidReply.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, - "org.clusterlabs.pacemaker.InvalidReply.Type", + 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] msg DBus request to send * \param[in] 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, "org.clusterlabs.pacemaker.SendFailed", + if (error) { + dbus_set_error(error, ERR_SEND_FAILED, "Could not queue DBus '%s' request", method); } return NULL; } dbus_connection_flush(connection); - if(pending) { + 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); + (void) pcmk_dbus_find_error(pending, reply, error); - if(pending) { + 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] 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 (*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); - + 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("Send with reply failed for %s", method); + crm_err("Could not send DBus %s message: failed", method); return NULL; } else if (pending == NULL) { - crm_err("No pending call found for %s: Connection to System DBus may be closed", method); + crm_err("Could not send DBus %s message: connection may be closed", + method); return NULL; } - crm_trace("DBus %s call sent", method); if (dbus_pending_call_get_completed(pending)) { - crm_info("DBus %s call completed too soon", method); - if(done) { -#if 0 - /* This sounds like a good idea, but allegedly it breaks things */ - done(pending, user_data); - pending = NULL; -#else - CRM_ASSERT(dbus_pending_call_set_notify(pending, done, user_data, NULL)); -#endif - } - - } else if(done) { - CRM_ASSERT(dbus_pending_call_set_notify(pending, done, user_data, NULL)); + 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)) { + if (field == NULL) { + if (dbus_message_iter_init(msg, &lfield)) { field = &lfield; } } - if(field == NULL) { - do_crm_log_alias(LOG_ERR, __FILE__, function, line, - "Empty parameter list in reply expecting '%c'", expected); + 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) { + 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_ERR, __FILE__, function, line, - "Unexpected DBus type, expected %c in '%s' instead of %c", - expected, sig, dtype); + 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 * -pcmk_dbus_lookup_result(DBusMessage *reply, struct db_getall_data *data) +handle_query_result(DBusMessage *reply, struct property_query *data) { DBusError error; char *output = NULL; - DBusMessageIter dict; 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("Cannot get properties from %s for %s: %s", - data->target, data->object, error.message); + 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_ARRAY, __func__, __LINE__)) { - crm_err("Invalid reply from %s for %s", data->target, data->object); + 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; } - dbus_message_iter_recurse(&args, &dict); - while (dbus_message_iter_get_arg_type (&dict) != DBUS_TYPE_INVALID) { - DBusMessageIter sv; - DBusMessageIter v; - DBusBasicValue name; - DBusBasicValue value; + // 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); - if(!pcmk_dbus_type_check(reply, &dict, DBUS_TYPE_DICT_ENTRY, __func__, __LINE__)) { - dbus_message_iter_next (&dict); - continue; - } + // 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; + } - dbus_message_iter_recurse(&dict, &sv); - while (dbus_message_iter_get_arg_type (&sv) != DBUS_TYPE_INVALID) { - int dtype = dbus_message_iter_get_arg_type(&sv); + crm_trace("DBus query result for %s: %s='%s'", + data->object, data->name, (value.str? value.str : "")); - switch(dtype) { - case DBUS_TYPE_STRING: - dbus_message_iter_get_basic(&sv, &name); + if (data->callback) { // Query was asynchronous + data->callback(data->name, (value.str? value.str : ""), data->userdata); - if(!pcmk__str_eq(data->name, name.str, pcmk__str_null_matches)) { - dbus_message_iter_next (&sv); /* Skip the value */ - } - break; - case DBUS_TYPE_VARIANT: - dbus_message_iter_recurse(&sv, &v); - if(pcmk_dbus_type_check(reply, &v, DBUS_TYPE_STRING, __func__, __LINE__)) { - dbus_message_iter_get_basic(&v, &value); - - crm_trace("Property %s[%s] is '%s'", data->object, name.str, value.str); - if(data->callback) { - data->callback(name.str, value.str, data->userdata); - - } else { - free(output); - output = strdup(value.str); - } - - if(data->name) { - goto cleanup; - } - } - break; - default: - pcmk_dbus_type_check(reply, &sv, DBUS_TYPE_STRING, - __func__, __LINE__); - } - dbus_message_iter_next (&sv); - } - - dbus_message_iter_next (&dict); - } - - if(data->name && data->callback) { - crm_trace("No value for property %s[%s]", data->object, data->name); - data->callback(data->name, NULL, data->userdata); + } else { // Query was synchronous + output = strdup(value.str? value.str : ""); } cleanup: - free_db_getall_data(data); + free_property_query(data); return output; } static void -pcmk_dbus_lookup_cb(DBusPendingCall *pending, void *user_data) +async_query_result_cb(DBusPendingCall *pending, void *user_data) { DBusMessage *reply = NULL; char *value = NULL; - if(pending) { + if (pending) { reply = dbus_pending_call_steal_reply(pending); } - value = pcmk_dbus_lookup_result(reply, user_data); + value = handle_query_result(reply, user_data); free(value); - if(reply) { + if (reply) { dbus_message_unref(reply); } } +/*! + * \internal + * \brief Query a property on a DBus object + * + * \param[in] 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] userdata Caller-provided data to provide to \p callback + * \param[out] pending If \p callback is not NULL, this will be set to the + * 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, - void (*callback)(const char *name, const char *value, void *userdata), - void *userdata, DBusPendingCall **pending, int timeout) + property_callback_func callback, void *userdata, + DBusPendingCall **pending, int timeout) { DBusMessage *msg; - const char *method = "GetAll"; char *output = NULL; - struct db_getall_data *query_data = NULL; - - crm_debug("Calling: %s on %s", method, target); - msg = dbus_message_new_method_call(target, // target for the method call - obj, // object to call on - BUS_PROPERTY_IFACE, // interface to call on - method); // method name - if (NULL == msg) { - crm_err("Call to %s failed: No message", method); + 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; } - CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_STRING, &iface, DBUS_TYPE_INVALID)); + // 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 db_getall_data)); - if(query_data == NULL) { - crm_err("Call to %s failed: malloc failed", method); + 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 = NULL; - - if(name) { - query_data->name = strdup(name); - } - - if (query_data->callback) { + 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, pcmk_dbus_lookup_cb, + local_pending = pcmk_dbus_send(msg, connection, async_query_result_cb, query_data, timeout); if (local_pending == NULL) { - // pcmk_dbus_lookup_cb() was not called in this case - free_db_getall_data(query_data); + // async_query_result_cb() was not called in this case + free_property_query(query_data); query_data = NULL; } if (pending) { *pending = local_pending; } - } else { - DBusMessage *reply = pcmk_dbus_send_recv(msg, connection, NULL, timeout); + } else { // Synchronous + DBusMessage *reply = pcmk_dbus_send_recv(msg, connection, NULL, + timeout); - output = pcmk_dbus_lookup_result(reply, query_data); + output = handle_query_result(reply, query_data); - if(reply) { + if (reply) { dbus_message_unref(reply); } } dbus_message_unref(msg); return output; } - -static void -pcmk_dbus_connection_dispatch_status(DBusConnection *connection, - DBusDispatchStatus new_status, void *data) -{ - crm_trace("New status %d for connection %p", new_status, connection); - if (new_status == DBUS_DISPATCH_DATA_REMAINS){ - conn_dispatches = g_list_prepend(conn_dispatches, connection); - } -} - -static void -pcmk_dbus_connections_dispatch(void) -{ - GList *gIter = NULL; - - for (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 for connection %p", connection); - dbus_connection_dispatch(connection); - } - } - - g_list_free(conn_dispatches); - conn_dispatches = NULL; -} - -/* 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 = "readwrite"; - } else if (flags & DBUS_WATCH_READABLE) { - watch_type = "read"; - } else if (flags & DBUS_WATCH_WRITABLE) { - watch_type = "write"; - } else { - watch_type = "not read or write"; - } - return watch_type; -} - -static int -pcmk_dbus_watch_dispatch(gpointer userdata) -{ - bool oom = FALSE; - DBusWatch *watch = userdata; - int flags = dbus_watch_get_flags(watch); - bool enabled = dbus_watch_get_enabled (watch); - mainloop_io_t *client = dbus_watch_get_data(watch); - - crm_trace("Dispatching client %p: %s", client, 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 client %p: %s (%d)", client, - dbus_watch_flags_to_string(flags), flags); - } - - if(oom) { - crm_err("DBus encountered OOM while attempting to dispatch %p (%s)", - client, dbus_watch_flags_to_string(flags)); - - } else { - pcmk_dbus_connections_dispatch(); - } - - return 0; -} - -static void -pcmk_dbus_watch_destroy(gpointer userdata) -{ - mainloop_io_t *client = dbus_watch_get_data(userdata); - crm_trace("Destroyed %p", client); -} - - -struct mainloop_fd_callbacks pcmk_dbus_cb = { - .dispatch = pcmk_dbus_watch_dispatch, - .destroy = pcmk_dbus_watch_destroy, -}; - -static dbus_bool_t -pcmk_dbus_watch_add(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 watch %p with fd=%d to client %p", watch, fd, client); - dbus_watch_set_data(watch, client, NULL); - return TRUE; -} - -static void -pcmk_dbus_watch_toggle(DBusWatch *watch, void *data) -{ - mainloop_io_t *client = dbus_watch_get_data(watch); - crm_notice("DBus client %p is now %s", - client, (dbus_watch_get_enabled(watch)? "enabled" : "disabled")); -} - - -static void -pcmk_dbus_watch_remove(DBusWatch *watch, void *data) -{ - mainloop_io_t *client = dbus_watch_get_data(watch); - - crm_trace("Removed client %p (%p)", client, data); - mainloop_del_fd(client); -} - -static gboolean -pcmk_dbus_timeout_dispatch(gpointer data) -{ - crm_info("Timeout %p expired", data); - dbus_timeout_handle(data); - return FALSE; -} - -static dbus_bool_t -pcmk_dbus_timeout_add(DBusTimeout *timeout, void *data) -{ - guint id = g_timeout_add(dbus_timeout_get_interval(timeout), - pcmk_dbus_timeout_dispatch, timeout); - - crm_trace("Adding timeout %p (%d)", timeout, dbus_timeout_get_interval(timeout)); - - if(id) { - dbus_timeout_set_data(timeout, GUINT_TO_POINTER(id), NULL); - } - return TRUE; -} - -static void -pcmk_dbus_timeout_remove(DBusTimeout *timeout, void *data) -{ - void *vid = dbus_timeout_get_data(timeout); - guint id = GPOINTER_TO_UINT(vid); - - crm_trace("Removing timeout %p (%p)", timeout, data); - - if(id) { - g_source_remove(id); - dbus_timeout_set_data(timeout, 0, NULL); - } -} - -static void -pcmk_dbus_timeout_toggle(DBusTimeout *timeout, void *data) -{ - bool enabled = dbus_timeout_get_enabled(timeout); - - crm_trace("Toggling timeout for %p to %s", timeout, enabled?"off":"on"); - - if(enabled) { - pcmk_dbus_timeout_add(timeout, data); - } else { - pcmk_dbus_timeout_remove(timeout, data); - } -} - -/* Inspired by http://www.kolej.mff.cuni.cz/~vesej3am/devel/dbus-select.c */ - -void -pcmk_dbus_connection_setup_with_select(DBusConnection *c) -{ - dbus_connection_set_exit_on_disconnect(c, FALSE); - dbus_connection_set_timeout_functions(c, pcmk_dbus_timeout_add, - pcmk_dbus_timeout_remove, - pcmk_dbus_timeout_toggle, NULL, NULL); - dbus_connection_set_watch_functions(c, pcmk_dbus_watch_add, - pcmk_dbus_watch_remove, - pcmk_dbus_watch_toggle, NULL, NULL); - dbus_connection_set_dispatch_status_function(c, pcmk_dbus_connection_dispatch_status, NULL, NULL); - pcmk_dbus_connection_dispatch_status(c, dbus_connection_get_dispatch_status(c), NULL); -} diff --git a/lib/services/pcmk-dbus.h b/lib/services/pcmk-dbus.h index 3f3547eb8a..1d026ea01e 100644 --- a/lib/services/pcmk-dbus.h +++ b/lib/services/pcmk-dbus.h @@ -1,46 +1,45 @@ /* - * Copyright (C) 2014-2016 Andrew Beekhof + * Copyright 2014-2020 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 # 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_connection_setup_with_select(DBusConnection *c); - 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(DBusPendingCall *pending, DBusMessage *reply, DBusError *error); #endif /* PCMK_DBUS__H */ diff --git a/lib/services/systemd.c b/lib/services/systemd.c index 0f2f5145cc..a2826ac3b7 100644 --- a/lib/services/systemd.c +++ b/lib/services/systemd.c @@ -1,861 +1,866 @@ /* * Copyright 2012-2020 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include gboolean systemd_unit_exec_with_unit(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" static inline DBusMessage * systemd_new_method(const char *method) { crm_trace("Calling: %s on " BUS_NAME_MANAGER, method); return dbus_message_new_method_call(BUS_NAME, BUS_PATH, BUS_NAME_MANAGER, method); } /* * Functions to manage a static DBus connection */ static DBusConnection* systemd_proxy = NULL; static inline DBusPendingCall * systemd_send(DBusMessage *msg, void(*done)(DBusPendingCall *pending, void *user_data), void *user_data, int timeout) { return pcmk_dbus_send(msg, systemd_proxy, done, user_data, timeout); } static inline DBusMessage * systemd_send_recv(DBusMessage *msg, DBusError *error, int timeout) { return pcmk_dbus_send_recv(msg, systemd_proxy, error, timeout); } /*! * \internal * \brief Send a method to systemd without arguments, and wait for reply * * \param[in] method Method to send * * \return Systemd reply on success, NULL (and error will be logged) otherwise * * \note The caller must call dbus_message_unref() on the reply after * handling it. */ static DBusMessage * systemd_call_simple_method(const char *method) { DBusMessage *msg = systemd_new_method(method); DBusMessage *reply = NULL; DBusError error; /* Don't call systemd_init() here, because that calls this */ CRM_CHECK(systemd_proxy, return NULL); if (msg == NULL) { crm_err("Could not create message to send %s to systemd", method); return NULL; } dbus_error_init(&error); reply = systemd_send_recv(msg, &error, DBUS_TIMEOUT_USE_DEFAULT); dbus_message_unref(msg); if (dbus_error_is_set(&error)) { crm_err("Could not send %s to systemd: %s (%s)", method, error.message, error.name); dbus_error_free(&error); return NULL; } else if (reply == NULL) { crm_err("Could not send %s to systemd: no reply received", method); return NULL; } return reply; } static gboolean systemd_init(void) { static int need_init = 1; // https://dbus.freedesktop.org/doc/api/html/group__DBusConnection.html if (systemd_proxy && dbus_connection_get_is_connected(systemd_proxy) == FALSE) { crm_warn("Connection to System DBus is closed. Reconnecting..."); pcmk_dbus_disconnect(systemd_proxy); systemd_proxy = NULL; need_init = 1; } if (need_init) { need_init = 0; systemd_proxy = pcmk_dbus_connect(); } if (systemd_proxy == NULL) { return FALSE; } return TRUE; } static inline char * systemd_get_property(const char *unit, const char *name, void (*callback)(const char *name, const char *value, void *userdata), void *userdata, DBusPendingCall **pending, int timeout) { return systemd_proxy? pcmk_dbus_get_property(systemd_proxy, BUS_NAME, unit, BUS_NAME_UNIT, name, callback, userdata, pending, timeout) : NULL; } void systemd_cleanup(void) { if (systemd_proxy) { pcmk_dbus_disconnect(systemd_proxy); systemd_proxy = NULL; } } /* * end of systemd_proxy functions */ /*! * \internal * \brief Check whether a file name represents a manageable systemd unit * * \param[in] name File name to check * * \return Pointer to "dot" before filename extension if so, NULL otherwise */ static const char * systemd_unit_extension(const char *name) { if (name) { const char *dot = strrchr(name, '.'); if (dot && (!strcmp(dot, ".service") || !strcmp(dot, ".socket") || !strcmp(dot, ".mount") || !strcmp(dot, ".timer") || !strcmp(dot, ".path"))) { return dot; } } return NULL; } static char * systemd_service_name(const char *name) { if (name == NULL) { return NULL; } if (systemd_unit_extension(name)) { return strdup(name); } return crm_strdup_printf("%s.service", name); } static void systemd_daemon_reload_complete(DBusPendingCall *pending, void *user_data) { DBusError error; DBusMessage *reply = NULL; unsigned int reload_count = GPOINTER_TO_UINT(user_data); dbus_error_init(&error); if(pending) { reply = dbus_pending_call_steal_reply(pending); } if (pcmk_dbus_find_error(pending, reply, &error)) { crm_err("Could not issue systemd reload %d: %s", reload_count, error.message); dbus_error_free(&error); } else { crm_trace("Reload %d complete", reload_count); } if(pending) { dbus_pending_call_unref(pending); } if(reply) { dbus_message_unref(reply); } } static bool systemd_daemon_reload(int timeout) { static unsigned int reload_count = 0; DBusMessage *msg = systemd_new_method("Reload"); reload_count++; CRM_ASSERT(msg != NULL); systemd_send(msg, systemd_daemon_reload_complete, GUINT_TO_POINTER(reload_count), timeout); dbus_message_unref(msg); return TRUE; } static bool systemd_mask_error(svc_action_t *op, const char *error) { crm_trace("Could not issue %s for %s: %s", op->action, op->rsc, error); if(strstr(error, "org.freedesktop.systemd1.InvalidName") || strstr(error, "org.freedesktop.systemd1.LoadFailed") || strstr(error, "org.freedesktop.systemd1.NoSuchUnit")) { if (pcmk__str_eq(op->action, "stop", pcmk__str_casei)) { crm_trace("Masking %s failure for %s: unknown services are stopped", op->action, op->rsc); op->rc = PCMK_OCF_OK; return TRUE; } else { crm_trace("Mapping %s failure for %s: unknown services are not installed", op->action, op->rsc); op->rc = PCMK_OCF_NOT_INSTALLED; op->status = PCMK_LRM_OP_NOT_INSTALLED; return FALSE; } } return FALSE; } static const char * systemd_loadunit_result(DBusMessage *reply, svc_action_t * op) { const char *path = NULL; DBusError error; if (pcmk_dbus_find_error((void*)&path, reply, &error)) { if(op && !systemd_mask_error(op, error.name)) { crm_err("Could not load systemd unit %s for %s: %s", op->agent, op->id, error.message); } dbus_error_free(&error); - } else if(pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, __func__, __LINE__)) { + } else if (!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, + __func__, __LINE__)) { + crm_err("Could not load systemd unit %s for %s: " + "systemd reply has unexpected type", op->agent, op->id); + + } else { dbus_message_get_args (reply, NULL, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID); } if(op) { if (path) { systemd_unit_exec_with_unit(op, path); } else if (op->synchronous == FALSE) { operation_finalize(op); } } return path; } static void systemd_loadunit_cb(DBusPendingCall *pending, void *user_data) { DBusMessage *reply = NULL; svc_action_t * op = user_data; if(pending) { reply = dbus_pending_call_steal_reply(pending); } crm_trace("Got result: %p for %p / %p for %s", reply, pending, op->opaque->pending, op->id); CRM_LOG_ASSERT(pending == op->opaque->pending); services_set_op_pending(op, NULL); systemd_loadunit_result(reply, user_data); if(reply) { dbus_message_unref(reply); } } static char * systemd_unit_by_name(const gchar * arg_name, svc_action_t *op) { DBusMessage *msg; DBusMessage *reply = NULL; DBusPendingCall* pending = NULL; char *name = NULL; /* Equivalent to GetUnit if it's already loaded */ if (systemd_init() == FALSE) { return FALSE; } msg = systemd_new_method("LoadUnit"); CRM_ASSERT(msg != NULL); name = systemd_service_name(arg_name); CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_STRING, &name, DBUS_TYPE_INVALID)); free(name); if(op == NULL || op->synchronous) { const char *unit = NULL; char *munit = NULL; reply = systemd_send_recv(msg, NULL, (op? op->timeout : DBUS_TIMEOUT_USE_DEFAULT)); dbus_message_unref(msg); unit = systemd_loadunit_result(reply, op); if(unit) { munit = strdup(unit); } if(reply) { dbus_message_unref(reply); } return munit; } pending = systemd_send(msg, systemd_loadunit_cb, op, op->timeout); if(pending) { services_set_op_pending(op, pending); } dbus_message_unref(msg); return NULL; } /*! * \internal * \brief Compare two strings alphabetically (case-insensitive) * * \param[in] a First string to compare * \param[in] b Second string to compare * * \return 0 if strings are equal, -1 if a < b, 1 if a > b * * \note Usable as a GCompareFunc with g_list_sort(). * NULL is considered less than non-NULL. */ static gint sort_str(gconstpointer a, gconstpointer b) { if (!a && !b) { return 0; } else if (!a) { return -1; } else if (!b) { return 1; } return strcasecmp(a, b); } GList * systemd_unit_listall(void) { int nfiles = 0; GList *units = NULL; DBusMessageIter args; DBusMessageIter unit; DBusMessageIter elem; DBusMessage *reply = NULL; if (systemd_init() == FALSE) { return NULL; } /* " \n" \ " \n" \ " \n" \ */ reply = systemd_call_simple_method("ListUnitFiles"); if (reply == NULL) { return NULL; } if (!dbus_message_iter_init(reply, &args)) { crm_err("Could not list systemd unit files: systemd reply has no arguments"); dbus_message_unref(reply); return NULL; } if (!pcmk_dbus_type_check(reply, &args, DBUS_TYPE_ARRAY, __func__, __LINE__)) { crm_err("Could not list systemd unit files: systemd reply has invalid arguments"); dbus_message_unref(reply); return NULL; } dbus_message_iter_recurse(&args, &unit); for (; dbus_message_iter_get_arg_type(&unit) != DBUS_TYPE_INVALID; dbus_message_iter_next(&unit)) { DBusBasicValue value; const char *match = NULL; char *unit_name = NULL; char *basename = NULL; if(!pcmk_dbus_type_check(reply, &unit, DBUS_TYPE_STRUCT, __func__, __LINE__)) { - crm_debug("ListUnitFiles reply has unexpected type"); + 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_debug("ListUnitFiles reply does not contain a string"); + 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 *unit = NULL; /* Note: Makes a blocking dbus calls * Used by resources_find_service_class() when resource class=service */ unit = systemd_unit_by_name(name, NULL); if(unit) { free(unit); return TRUE; } return FALSE; } static char * systemd_unit_metadata(const char *name, int timeout) { char *meta = NULL; char *desc = NULL; char *path = systemd_unit_by_name(name, NULL); if (path) { /* TODO: Worth a making blocking call for? Probably not. Possibly if cached. */ desc = systemd_get_property(path, "Description", NULL, NULL, NULL, timeout); } else { desc = crm_strdup_printf("Systemd unit file for %s", name); } meta = crm_strdup_printf("\n" "\n" "\n" " 1.0\n" " \n" " %s\n" " \n" " systemd unit file for %s\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n", name, desc, name); free(desc); free(path); return meta; } static void systemd_exec_result(DBusMessage *reply, svc_action_t *op) { DBusError error; if (pcmk_dbus_find_error((void*)&error, reply, &error)) { /* ignore "already started" or "not running" errors */ if (!systemd_mask_error(op, error.name)) { crm_err("Could not issue %s for %s: %s", op->action, op->rsc, error.message); } dbus_error_free(&error); } else { if(!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, __func__, __LINE__)) { crm_warn("Call to %s passed but return type was unexpected", op->action); op->rc = PCMK_OCF_OK; } else { const char *path = NULL; dbus_message_get_args (reply, NULL, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID); crm_info("Call to %s passed: %s", op->action, path); op->rc = PCMK_OCF_OK; } } operation_finalize(op); } static void systemd_async_dispatch(DBusPendingCall *pending, void *user_data) { DBusMessage *reply = NULL; svc_action_t *op = user_data; if(pending) { reply = dbus_pending_call_steal_reply(pending); } crm_trace("Got result: %p for %p for %s, %s", reply, pending, op->rsc, op->action); CRM_LOG_ASSERT(pending == op->opaque->pending); services_set_op_pending(op, NULL); systemd_exec_result(reply, op); if(reply) { 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); crm_build_path(override_dir, 0755); 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); } static void systemd_unit_check(const char *name, const char *state, void *userdata) { svc_action_t * op = userdata; crm_trace("Resource %s has %s='%s'", op->rsc, name, state); if(state == NULL) { op->rc = PCMK_OCF_NOT_RUNNING; } else if (g_strcmp0(state, "active") == 0) { op->rc = PCMK_OCF_OK; } else if (g_strcmp0(state, "reloading") == 0) { op->rc = PCMK_OCF_OK; } else if (g_strcmp0(state, "activating") == 0) { op->rc = PCMK_OCF_PENDING; } else if (g_strcmp0(state, "deactivating") == 0) { op->rc = PCMK_OCF_PENDING; } else { op->rc = PCMK_OCF_NOT_RUNNING; } if (op->synchronous == FALSE) { services_set_op_pending(op, NULL); operation_finalize(op); } } gboolean systemd_unit_exec_with_unit(svc_action_t * op, const char *unit) { const char *method = op->action; DBusMessage *msg = NULL; DBusMessage *reply = NULL; CRM_ASSERT(unit); if (pcmk__str_eq(op->action, "monitor", pcmk__str_casei) || pcmk__str_eq(method, "status", pcmk__str_casei)) { DBusPendingCall *pending = NULL; char *state; state = systemd_get_property(unit, "ActiveState", (op->synchronous? NULL : systemd_unit_check), op, (op->synchronous? NULL : &pending), op->timeout); if (op->synchronous) { systemd_unit_check("ActiveState", state, op); free(state); return op->rc == PCMK_OCF_OK; } else if (pending) { services_set_op_pending(op, pending); return TRUE; } else { return operation_finalize(op); } } else if (g_strcmp0(method, "start") == 0) { method = "StartUnit"; systemd_create_override(op->agent, op->timeout); } else if (g_strcmp0(method, "stop") == 0) { method = "StopUnit"; systemd_remove_override(op->agent, op->timeout); } else if (g_strcmp0(method, "restart") == 0) { method = "RestartUnit"; } else { op->rc = PCMK_OCF_UNIMPLEMENT_FEATURE; goto cleanup; } crm_debug("Calling %s for %s: %s", method, op->rsc, unit); msg = systemd_new_method(method); CRM_ASSERT(msg != NULL); /* (ss) */ { const char *replace_s = "replace"; char *name = systemd_service_name(op->agent); CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_STRING, &name, DBUS_TYPE_INVALID)); CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_STRING, &replace_s, DBUS_TYPE_INVALID)); free(name); } if (op->synchronous == FALSE) { DBusPendingCall *pending = systemd_send(msg, systemd_async_dispatch, op, op->timeout); dbus_message_unref(msg); if(pending) { services_set_op_pending(op, pending); return TRUE; } else { return operation_finalize(op); } } else { reply = systemd_send_recv(msg, NULL, op->timeout); dbus_message_unref(msg); systemd_exec_result(reply, op); if(reply) { dbus_message_unref(reply); } return FALSE; } cleanup: if (op->synchronous == FALSE) { return operation_finalize(op); } return op->rc == PCMK_OCF_OK; } static gboolean systemd_timeout_callback(gpointer p) { svc_action_t * op = p; op->opaque->timerid = 0; crm_warn("%s operation on systemd unit %s named '%s' timed out", op->action, op->agent, op->rsc); operation_finalize(op); return FALSE; } /* For an asynchronous 'op', returns FALSE if 'op' should be free'd by the caller */ /* For a synchronous 'op', returns FALSE if 'op' fails */ gboolean systemd_unit_exec(svc_action_t * op) { char *unit = NULL; CRM_ASSERT(op); CRM_ASSERT(systemd_init()); op->rc = PCMK_OCF_UNKNOWN_ERROR; crm_debug("Performing %ssynchronous %s op on systemd unit %s named '%s'", op->synchronous ? "" : "a", op->action, op->agent, op->rsc); if (pcmk__str_eq(op->action, "meta-data", pcmk__str_casei)) { // @TODO Implement an async meta-data call in executor API op->stdout_data = systemd_unit_metadata(op->agent, op->timeout); op->rc = PCMK_OCF_OK; if (op->synchronous == FALSE) { return operation_finalize(op); } return TRUE; } unit = systemd_unit_by_name(op->agent, op); free(unit); if (op->synchronous == FALSE) { if (op->opaque->pending) { op->opaque->timerid = g_timeout_add(op->timeout + 5000, systemd_timeout_callback, op); services_add_inflight_op(op); return TRUE; } else { return operation_finalize(op); } } return op->rc == PCMK_OCF_OK; } diff --git a/lib/services/upstart.c b/lib/services/upstart.c index 972f4f6d79..b1efa81c12 100644 --- a/lib/services/upstart.c +++ b/lib/services/upstart.c @@ -1,563 +1,564 @@ /* * Copyright (C) 2010 Senko Rasic * Copyright (c) 2010 Ante Karamatic * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #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; 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; } } static gboolean upstart_job_by_name(const gchar * arg_name, gchar ** out_unit, int timeout) { /* com.ubuntu.Upstart0_6.GetJobByName (in String name, out ObjectPath job) */ DBusError error; DBusMessage *msg; DBusMessage *reply = NULL; const char *method = "GetJobByName"; if(upstart_init() == FALSE) { 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 method); // 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 issue %s for %s: %s", method, arg_name, error.message); dbus_error_free(&error); } else if(!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, __func__, __LINE__)) { crm_err("Invalid return type for %s", method); } else { if(out_unit) { char *path = NULL; dbus_message_get_args (reply, NULL, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID); *out_unit = strdup(path); } dbus_message_unref(reply); return TRUE; } if(reply) { dbus_message_unref(reply); } return FALSE; } 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 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 upstart_job_by_name(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_err("Call to %s failed: %s", method, error.message); dbus_error_free(&error); goto done; } else if(reply == NULL) { crm_err("Call to %s failed: no reply", method); goto done; } else if (!dbus_message_iter_init(reply, &args)) { crm_err("Call to %s failed: Message has no arguments", method); goto done; } if(!pcmk_dbus_type_check(reply, &args, DBUS_TYPE_ARRAY, __func__, __LINE__)) { crm_err("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; } static void upstart_job_check(const char *name, const char *state, void *userdata) { svc_action_t * op = userdata; if (state && g_strcmp0(state, "running") == 0) { op->rc = PCMK_OCF_OK; /* } else if (g_strcmp0(state, "activating") == 0) { */ /* op->rc = PCMK_OCF_PENDING; */ } else { op->rc = PCMK_OCF_NOT_RUNNING; } if (op->synchronous == FALSE) { services_set_op_pending(op, NULL); operation_finalize(op); } } static char * upstart_job_metadata(const char *name) { return crm_strdup_printf("\n" "\n" "\n" " 1.0\n" " \n" " Upstart agent for controlling the system %s service\n" " \n" " %s upstart agent\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n", name, name, name); } static bool upstart_mask_error(svc_action_t *op, const char *error) { crm_trace("Could not issue %s for %s: %s", op->action, op->rsc, error); if(strstr(error, UPSTART_06_API ".Error.UnknownInstance")) { if(pcmk__str_eq(op->action, "stop", pcmk__str_casei)) { crm_trace("Masking %s failure for %s: unknown services are stopped", op->action, op->rsc); op->rc = PCMK_OCF_OK; } else if(pcmk__str_eq(op->action, "start", pcmk__str_casei)) { crm_trace("Mapping %s failure for %s: unknown services are not installed", op->action, op->rsc); op->rc = PCMK_OCF_NOT_INSTALLED; op->status = PCMK_LRM_OP_NOT_INSTALLED; } return TRUE; } else if (pcmk__str_eq(op->action, "start", pcmk__str_casei) && strstr(error, UPSTART_06_API ".Error.AlreadyStarted")) { crm_trace("Mapping %s failure for %s: starting a started resource is allowed", op->action, op->rsc); op->rc = PCMK_OCF_OK; return TRUE; } return FALSE; } static void upstart_async_dispatch(DBusPendingCall *pending, void *user_data) { DBusError error; DBusMessage *reply = NULL; svc_action_t *op = user_data; dbus_error_init(&error); if(pending) { reply = dbus_pending_call_steal_reply(pending); } if (pcmk_dbus_find_error(pending, reply, &error)) { /* ignore "already started" or "not running" errors */ if (!upstart_mask_error(op, error.name)) { crm_err("%s for %s: %s", op->action, op->rsc, error.message); } dbus_error_free(&error); } else if (!g_strcmp0(op->action, "stop")) { /* No return vaue */ op->rc = PCMK_OCF_OK; } else { if(!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, __func__, __LINE__)) { crm_warn("Call to %s passed but return type was unexpected", op->action); op->rc = PCMK_OCF_OK; } else { const char *path = NULL; dbus_message_get_args (reply, NULL, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID); crm_info("Call to %s passed: %s", op->action, path); op->rc = PCMK_OCF_OK; } } CRM_LOG_ASSERT(pending == op->opaque->pending); services_set_op_pending(op, NULL); operation_finalize(op); if(reply) { dbus_message_unref(reply); } } /* For an asynchronous 'op', returns FALSE if 'op' should be free'd by the caller */ /* For a synchronous 'op', returns FALSE if 'op' fails */ gboolean upstart_job_exec(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; op->rc = PCMK_OCF_UNKNOWN_ERROR; CRM_ASSERT(upstart_init()); if (pcmk__str_eq(op->action, "meta-data", pcmk__str_casei)) { op->stdout_data = upstart_job_metadata(op->agent); op->rc = PCMK_OCF_OK; goto cleanup; } if(!upstart_job_by_name(op->agent, &job, op->timeout)) { crm_debug("Could not obtain job named '%s' to %s", op->agent, action); if (!g_strcmp0(action, "stop")) { op->rc = PCMK_OCF_OK; } else { op->rc = PCMK_OCF_NOT_INSTALLED; op->status = PCMK_LRM_OP_NOT_INSTALLED; } goto cleanup; } if (pcmk__strcase_any_of(op->action, "monitor", "status", NULL)) { char *path = get_first_instance(job, op->timeout); op->rc = PCMK_OCF_NOT_RUNNING; if(path) { DBusPendingCall *pending = NULL; char *state = pcmk_dbus_get_property( upstart_proxy, BUS_NAME, path, UPSTART_06_API ".Instance", "state", op->synchronous?NULL:upstart_job_check, op, op->synchronous?NULL:&pending, op->timeout); free(job); free(path); if(op->synchronous) { upstart_job_check("state", state, op); free(state); return op->rc == PCMK_OCF_OK; } else if (pending) { services_set_op_pending(op, pending); services_add_inflight_op(op); return TRUE; } return FALSE; } goto cleanup; } else if (!g_strcmp0(action, "start")) { action = "Start"; } else if (!g_strcmp0(action, "stop")) { action = "Stop"; } else if (!g_strcmp0(action, "restart")) { action = "Restart"; } else { op->rc = PCMK_OCF_UNIMPLEMENT_FEATURE; goto cleanup; } crm_debug("Calling %s for %s on %s", action, op->rsc, 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 == FALSE) { DBusPendingCall* pending = pcmk_dbus_send(msg, upstart_proxy, upstart_async_dispatch, op, op->timeout); free(job); if(pending) { services_set_op_pending(op, pending); services_add_inflight_op(op); return TRUE; } return FALSE; } dbus_error_init(&error); reply = pcmk_dbus_send_recv(msg, upstart_proxy, &error, op->timeout); if (dbus_error_is_set(&error)) { if(!upstart_mask_error(op, error.name)) { crm_err("Could not issue %s for %s: %s (%s)", action, op->rsc, error.message, job); } dbus_error_free(&error); } else if (!g_strcmp0(op->action, "stop")) { /* No return vaue */ op->rc = PCMK_OCF_OK; } else if(!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, __func__, __LINE__)) { crm_warn("Call to %s passed but return type was unexpected", op->action); op->rc = PCMK_OCF_OK; } else { const char *path = NULL; dbus_message_get_args (reply, NULL, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID); crm_info("Call to %s passed: %s", op->action, path); op->rc = PCMK_OCF_OK; } cleanup: free(job); if(msg) { dbus_message_unref(msg); } if(reply) { dbus_message_unref(reply); } if (op->synchronous == FALSE) { return operation_finalize(op); } return op->rc == PCMK_OCF_OK; }