summaryrefslogtreecommitdiffstats
path: root/lib/services/systemd.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/services/systemd.c')
-rw-r--r--lib/services/systemd.c1100
1 files changed, 1100 insertions, 0 deletions
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);
+ }
+}