dispatcher: support device-handler actions

"device-add" and "device-delete" actions are called for
device-handlers of generic devices. They differ from other actions in
the following aspects:

 - only one script is invoked, the one with name specified by the
   device-handler property;
 - the script is searched in the "device" subdirectory;
 - since there is only one script executed, the result and error
   string from that script are returned by NM in the callback function.

(cherry picked from commit ee5845063d)
This commit is contained in:
Beniamino Galvani 2023-09-29 14:07:53 +02:00 committed by Íñigo Huguet
parent 220d42c6f8
commit 5c33b14fe0
4 changed files with 263 additions and 77 deletions

View file

@ -52,18 +52,22 @@
/*****************************************************************************/
/* Type for generic callback function; must be cast to either
* NMDispatcherFunc or NMDispatcherFuncDH before using. */
typedef void (*NMDispatcherCallback)(void);
struct NMDispatcherCallId {
NMDispatcherFunc callback;
gpointer user_data;
const char *log_ifname;
const char *log_con_uuid;
GVariant *action_params;
gint64 start_at_msec;
NMDispatcherAction action;
guint idle_id;
guint32 request_id;
bool is_action2 : 1;
char extra_strings[];
NMDispatcherCallback callback;
gpointer user_data;
const char *log_ifname;
const char *log_con_uuid;
GVariant *action_params;
gint64 start_at_msec;
NMDispatcherAction action;
guint idle_id;
guint32 request_id;
bool is_action2 : 1;
char extra_strings[];
};
/*****************************************************************************/
@ -98,14 +102,20 @@ action_need_device(NMDispatcherAction action)
return TRUE;
}
static gboolean
action_is_device_handler(NMDispatcherAction action)
{
return NM_IN_SET(action, NM_DISPATCHER_ACTION_DEVICE_ADD, NM_DISPATCHER_ACTION_DEVICE_DELETE);
}
static NMDispatcherCallId *
dispatcher_call_id_new(guint32 request_id,
gint64 start_at_msec,
NMDispatcherAction action,
NMDispatcherFunc callback,
gpointer user_data,
const char *log_ifname,
const char *log_con_uuid)
dispatcher_call_id_new(guint32 request_id,
gint64 start_at_msec,
NMDispatcherAction action,
NMDispatcherCallback callback,
gpointer user_data,
const char *log_ifname,
const char *log_con_uuid)
{
NMDispatcherCallId *call_id;
gsize l_log_ifname;
@ -388,19 +398,42 @@ dispatch_result_to_string(DispatchResult result)
g_assert_not_reached();
}
/*
* dispatcher_results_process:
* @action: the dispatcher action
* @request_id: request id
* @start_at_msec: the timestamp at which the dispatcher call was started
* @now_msec: the current timestamp in milliseconds
* @log_ifname: the interface name for logging
* @log_con_uuid: the connection UUID for logging
* @out_success: (out): for device-handler actions, the result of the script
* @out_error_msg: (out)(transfer full): for device-handler actions, the
* error message in case of failure
* @v_results: the GVariant containing the results to parse
* @is_action2: whether the D-Bus method is "Action2()" (or "Action()")
*
* Process the results of the dispatcher call.
*
*/
static void
dispatcher_results_process(guint32 request_id,
gint64 start_at_msec,
gint64 now_msec,
const char *log_ifname,
const char *log_con_uuid,
GVariant *v_results,
gboolean is_action2)
dispatcher_results_process(NMDispatcherAction action,
guint32 request_id,
gint64 start_at_msec,
gint64 now_msec,
const char *log_ifname,
const char *log_con_uuid,
gboolean *out_success,
char **out_error_msg,
GVariant *v_results,
gboolean is_action2)
{
nm_auto_free_variant_iter GVariantIter *results = NULL;
const char *script, *err;
guint32 result;
gsize n_children;
gboolean action_is_dh = action_is_device_handler(action);
nm_assert(!action_is_dh || is_action2);
if (is_action2)
g_variant_get(v_results, "(a(susa{sv}))", &results);
@ -417,8 +450,13 @@ dispatcher_results_process(guint32 request_id,
(int) ((now_msec - start_at_msec) % 1000),
n_children);
if (n_children == 0)
if (n_children == 0) {
if (action_is_dh) {
NM_SET_OUT(out_success, FALSE);
NM_SET_OUT(out_error_msg, g_strdup("no result returned from dispatcher service"));
}
return;
}
while (TRUE) {
gs_unref_variant GVariant *options = NULL;
@ -442,6 +480,11 @@ dispatcher_results_process(guint32 request_id,
dispatch_result_to_string(result),
err);
}
if (action_is_dh) {
NM_SET_OUT(out_success, result == DISPATCH_RESULT_SUCCESS);
NM_SET_OUT(out_error_msg, g_strdup(err));
break;
}
}
}
@ -452,6 +495,9 @@ dispatcher_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
gs_free_error GError *error = NULL;
NMDispatcherCallId *call_id = user_data;
gint64 now_msec;
gboolean action_is_dh;
gboolean success = TRUE;
gs_free char *error_msg = NULL;
nm_assert((gpointer) source == gl.dbus_connection);
@ -459,7 +505,7 @@ dispatcher_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
if (!ret && call_id->is_action2
if (!ret && call_id->is_action2 && !action_is_device_handler(call_id->action)
&& g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) {
_LOG3D(call_id,
"dispatcher service does not implement Action2() method, falling back to Action()");
@ -493,38 +539,54 @@ dispatcher_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
(int) ((now_msec - call_id->start_at_msec) % 1000),
error->message);
} else {
dispatcher_results_process(call_id->request_id,
dispatcher_results_process(call_id->action,
call_id->request_id,
call_id->start_at_msec,
now_msec,
call_id->log_ifname,
call_id->log_con_uuid,
&success,
&error_msg,
ret,
call_id->is_action2);
}
g_hash_table_remove(gl.requests, call_id);
action_is_dh = action_is_device_handler(call_id->action);
if (call_id->callback)
call_id->callback(call_id, call_id->user_data);
if (call_id->callback) {
if (action_is_dh) {
NMDispatcherFuncDH cb = (NMDispatcherFuncDH) call_id->callback;
cb(call_id, call_id->user_data, success, error_msg);
} else {
NMDispatcherFunc cb = (NMDispatcherFunc) call_id->callback;
cb(call_id, call_id->user_data);
}
}
dispatcher_call_id_free(call_id);
}
static const char *action_table[] = {[NM_DISPATCHER_ACTION_HOSTNAME] = NMD_ACTION_HOSTNAME,
[NM_DISPATCHER_ACTION_PRE_UP] = NMD_ACTION_PRE_UP,
[NM_DISPATCHER_ACTION_UP] = NMD_ACTION_UP,
[NM_DISPATCHER_ACTION_PRE_DOWN] = NMD_ACTION_PRE_DOWN,
[NM_DISPATCHER_ACTION_DOWN] = NMD_ACTION_DOWN,
[NM_DISPATCHER_ACTION_VPN_PRE_UP] = NMD_ACTION_VPN_PRE_UP,
[NM_DISPATCHER_ACTION_VPN_UP] = NMD_ACTION_VPN_UP,
[NM_DISPATCHER_ACTION_VPN_PRE_DOWN] = NMD_ACTION_VPN_PRE_DOWN,
[NM_DISPATCHER_ACTION_VPN_DOWN] = NMD_ACTION_VPN_DOWN,
[NM_DISPATCHER_ACTION_DHCP_CHANGE_4] = NMD_ACTION_DHCP4_CHANGE,
[NM_DISPATCHER_ACTION_DHCP_CHANGE_6] = NMD_ACTION_DHCP6_CHANGE,
[NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE] =
NMD_ACTION_CONNECTIVITY_CHANGE,
[NM_DISPATCHER_ACTION_REAPPLY] = NMD_ACTION_REAPPLY,
[NM_DISPATCHER_ACTION_DNS_CHANGE] = NMD_ACTION_DNS_CHANGE};
static const char *action_table[] = {
[NM_DISPATCHER_ACTION_HOSTNAME] = NMD_ACTION_HOSTNAME,
[NM_DISPATCHER_ACTION_PRE_UP] = NMD_ACTION_PRE_UP,
[NM_DISPATCHER_ACTION_UP] = NMD_ACTION_UP,
[NM_DISPATCHER_ACTION_PRE_DOWN] = NMD_ACTION_PRE_DOWN,
[NM_DISPATCHER_ACTION_DOWN] = NMD_ACTION_DOWN,
[NM_DISPATCHER_ACTION_VPN_PRE_UP] = NMD_ACTION_VPN_PRE_UP,
[NM_DISPATCHER_ACTION_VPN_UP] = NMD_ACTION_VPN_UP,
[NM_DISPATCHER_ACTION_VPN_PRE_DOWN] = NMD_ACTION_VPN_PRE_DOWN,
[NM_DISPATCHER_ACTION_VPN_DOWN] = NMD_ACTION_VPN_DOWN,
[NM_DISPATCHER_ACTION_DHCP_CHANGE_4] = NMD_ACTION_DHCP4_CHANGE,
[NM_DISPATCHER_ACTION_DHCP_CHANGE_6] = NMD_ACTION_DHCP6_CHANGE,
[NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE] = NMD_ACTION_CONNECTIVITY_CHANGE,
[NM_DISPATCHER_ACTION_REAPPLY] = NMD_ACTION_REAPPLY,
[NM_DISPATCHER_ACTION_DNS_CHANGE] = NMD_ACTION_DNS_CHANGE,
[NM_DISPATCHER_ACTION_DEVICE_ADD] = NMD_ACTION_DEVICE_ADD,
[NM_DISPATCHER_ACTION_DEVICE_DELETE] = NMD_ACTION_DEVICE_DELETE,
};
static const char *
action_to_string(NMDispatcherAction action)
@ -664,7 +726,7 @@ _dispatcher_call(NMDispatcherAction action,
NMConnectivityState connectivity_state,
const char *vpn_iface,
const NML3ConfigData *l3cd,
NMDispatcherFunc callback,
NMDispatcherCallback callback,
gpointer user_data,
NMDispatcherCallId **out_call_id)
{
@ -784,11 +846,14 @@ _dispatcher_call(NMDispatcherAction action,
error->message);
return FALSE;
}
dispatcher_results_process(request_id,
dispatcher_results_process(action,
request_id,
start_at_msec,
now_msec,
log_ifname,
log_con_uuid,
NULL,
NULL,
ret,
is_action2);
return TRUE;
@ -856,7 +921,7 @@ nm_dispatcher_call_hostname(NMDispatcherFunc callback,
NM_CONNECTIVITY_UNKNOWN,
NULL,
NULL,
callback,
(NMDispatcherCallback) callback,
user_data,
out_call_id);
}
@ -866,7 +931,7 @@ _dispatcher_call_device(NMDispatcherAction action,
NMDevice *device,
gboolean blocking,
NMActRequest *act_request,
NMDispatcherFunc callback,
NMDispatcherCallback callback,
gpointer user_data,
NMDispatcherCallId **out_call_id)
{
@ -919,11 +984,48 @@ nm_dispatcher_call_device(NMDispatcherAction action,
gpointer user_data,
NMDispatcherCallId **out_call_id)
{
g_return_val_if_fail(!action_is_device_handler(action), FALSE);
return _dispatcher_call_device(action,
device,
FALSE,
act_request,
callback,
(NMDispatcherCallback) callback,
user_data,
out_call_id);
}
/**
* nm_dispatcher_call_device_handler:
* @action: the %NMDispatcherAction, must be device-add or device-remove
* @device: the #NMDevice the action applies to
* @act_request: the #NMActRequest for the action. If %NULL, use the
* current request of the device.
* @callback: a caller-supplied device-handler callback to execute when done
* @user_data: caller-supplied pointer passed to @callback
* @out_call_id: on success, a call identifier which can be passed to
* nm_dispatcher_call_cancel()
*
* This method always invokes the device dispatcher action asynchronously. To ignore
* the result, pass %NULL to @callback.
*
* Returns: %TRUE if the action was dispatched, %FALSE on failure
*/
gboolean
nm_dispatcher_call_device_handler(NMDispatcherAction action,
NMDevice *device,
NMActRequest *act_request,
NMDispatcherFuncDH callback,
gpointer user_data,
NMDispatcherCallId **out_call_id)
{
g_return_val_if_fail(action_is_device_handler(action), FALSE);
return _dispatcher_call_device(action,
device,
FALSE,
act_request,
(NMDispatcherCallback) callback,
user_data,
out_call_id);
}
@ -945,6 +1047,8 @@ nm_dispatcher_call_device_sync(NMDispatcherAction action,
NMDevice *device,
NMActRequest *act_request)
{
g_return_val_if_fail(!action_is_device_handler(action), FALSE);
return _dispatcher_call_device(action, device, TRUE, act_request, NULL, NULL, NULL);
}
@ -986,7 +1090,7 @@ nm_dispatcher_call_vpn(NMDispatcherAction action,
NM_CONNECTIVITY_UNKNOWN,
vpn_iface,
l3cd,
callback,
(NMDispatcherCallback) callback,
user_data,
out_call_id);
}
@ -1013,6 +1117,8 @@ nm_dispatcher_call_vpn_sync(NMDispatcherAction action,
const char *vpn_iface,
const NML3ConfigData *l3cd)
{
g_return_val_if_fail(!action_is_device_handler(action), FALSE);
return _dispatcher_call(action,
TRUE,
parent_device,
@ -1054,7 +1160,7 @@ nm_dispatcher_call_connectivity(NMConnectivityState connectivity_state,
connectivity_state,
NULL,
NULL,
callback,
(NMDispatcherCallback) callback,
user_data,
out_call_id);
}
@ -1086,7 +1192,10 @@ nm_dispatcher_call_dns_change(void)
void
nm_dispatcher_call_cancel(NMDispatcherCallId *call_id)
{
if (!call_id || g_hash_table_lookup(gl.requests, call_id) != call_id || !call_id->callback)
if (!call_id || g_hash_table_lookup(gl.requests, call_id) != call_id)
g_return_if_reached();
if (!call_id->callback)
g_return_if_reached();
/* Canceling just means the callback doesn't get called, so set the

View file

@ -24,6 +24,8 @@ typedef enum {
NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE,
NM_DISPATCHER_ACTION_REAPPLY,
NM_DISPATCHER_ACTION_DNS_CHANGE,
NM_DISPATCHER_ACTION_DEVICE_ADD,
NM_DISPATCHER_ACTION_DEVICE_DELETE,
} NMDispatcherAction;
#define NM_DISPATCHER_ACTION_DHCP_CHANGE_X(IS_IPv4) \
@ -31,7 +33,13 @@ typedef enum {
typedef struct NMDispatcherCallId NMDispatcherCallId;
/* Callback function for regular dispatcher calls */
typedef void (*NMDispatcherFunc)(NMDispatcherCallId *call_id, gpointer user_data);
/* Callback function for device-handler dispatcher calls */
typedef void (*NMDispatcherFuncDH)(NMDispatcherCallId *call_id,
gpointer user_data,
gboolean success,
const char *error_msg);
gboolean nm_dispatcher_call_hostname(NMDispatcherFunc callback,
gpointer user_data,
@ -44,6 +52,13 @@ gboolean nm_dispatcher_call_device(NMDispatcherAction action,
gpointer user_data,
NMDispatcherCallId **out_call_id);
gboolean nm_dispatcher_call_device_handler(NMDispatcherAction action,
NMDevice *device,
NMActRequest *act_request,
NMDispatcherFuncDH callback_dh,
gpointer user_data,
NMDispatcherCallId **out_call_id);
gboolean nm_dispatcher_call_device_sync(NMDispatcherAction action,
NMDevice *device,
NMActRequest *act_request);

View file

@ -35,6 +35,8 @@
#define NMD_ACTION_CONNECTIVITY_CHANGE "connectivity-change"
#define NMD_ACTION_REAPPLY "reapply"
#define NMD_ACTION_DNS_CHANGE "dns-change"
#define NMD_ACTION_DEVICE_ADD "device-add"
#define NMD_ACTION_DEVICE_DELETE "device-delete"
typedef enum {
DISPATCH_RESULT_UNKNOWN = 0,

View file

@ -86,6 +86,7 @@ struct Request {
char **envp;
gboolean debug;
gboolean is_action2;
gboolean is_device_handler;
GPtrArray *scripts; /* list of ScriptInfo */
guint idx;
@ -615,6 +616,31 @@ _compare_basenames(gconstpointer a, gconstpointer b)
return 0;
}
static gboolean
check_file(Request *request, const char *path)
{
gs_free char *link_target = NULL;
const char *err_msg = NULL;
struct stat st;
int err;
link_target = g_file_read_link(path, NULL);
if (nm_streq0(link_target, "/dev/null"))
return FALSE;
err = stat(path, &st);
if (err) {
return FALSE;
} else if (!S_ISREG(st.st_mode) || st.st_size == 0) {
/* silently skip. */
return FALSE;
} else if (!check_permissions(&st, &err_msg)) {
_LOG_R_W(request, "find-scripts: Cannot execute '%s': %s", path, err_msg);
return FALSE;
}
return TRUE;
}
static void
_find_scripts(Request *request, GHashTable *scripts, const char *base, const char *subdir)
{
@ -647,7 +673,7 @@ _find_scripts(Request *request, GHashTable *scripts, const char *base, const cha
}
static GSList *
find_scripts(Request *request)
find_scripts(Request *request, const char *device_handler)
{
gs_unref_hashtable GHashTable *scripts = NULL;
GSList *script_list = NULL;
@ -656,6 +682,33 @@ find_scripts(Request *request)
char *path;
char *filename;
if (request->is_device_handler) {
const char *const dirs[] = {NMCONFDIR, NMLIBDIR};
guint i;
nm_assert(device_handler);
for (i = 0; i < G_N_ELEMENTS(dirs); i++) {
gs_free char *full_name = NULL;
full_name = g_build_filename(dirs[i], "dispatcher.d", "device", device_handler, NULL);
if (check_file(request, full_name)) {
script_list = g_slist_prepend(script_list, g_steal_pointer(&full_name));
return script_list;
}
}
_LOG_R_W(request,
"find-scripts: no device-handler script found with name \"%s\"",
device_handler);
return NULL;
}
nm_assert(!device_handler);
/* Use a hash-table to deduplicate scripts with same name from /etc and /usr */
scripts = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free);
if (NM_IN_STRSET(request->action, NMD_ACTION_PRE_UP, NMD_ACTION_VPN_PRE_UP))
subdir = "pre-up.d";
else if (NM_IN_STRSET(request->action, NMD_ACTION_PRE_DOWN, NMD_ACTION_VPN_PRE_DOWN))
@ -663,33 +716,13 @@ find_scripts(Request *request)
else
subdir = NULL;
scripts = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free);
_find_scripts(request, scripts, NMLIBDIR, subdir);
_find_scripts(request, scripts, NMCONFDIR, subdir);
g_hash_table_iter_init(&iter, scripts);
while (g_hash_table_iter_next(&iter, (gpointer *) &filename, (gpointer *) &path)) {
gs_free char *link_target = NULL;
const char *err_msg = NULL;
struct stat st;
int err;
link_target = g_file_read_link(path, NULL);
if (nm_streq0(link_target, "/dev/null"))
continue;
err = stat(path, &st);
if (err)
_LOG_R_W(request, "find-scripts: Failed to stat '%s': %d", path, err);
else if (!S_ISREG(st.st_mode) || st.st_size == 0) {
/* silently skip. */
} else if (!check_permissions(&st, &err_msg))
_LOG_R_W(request, "find-scripts: Cannot execute '%s': %s", path, err_msg);
else {
/* success */
if (check_file(request, path)) {
script_list = g_slist_prepend(script_list, g_strdup(path));
continue;
}
}
@ -725,6 +758,27 @@ script_must_wait(const char *path)
return TRUE;
}
static char *
get_device_handler(GVariant *connection)
{
gs_unref_variant GVariant *generic_setting = NULL;
const char *device_handler = NULL;
generic_setting = g_variant_lookup_value(connection,
NM_SETTING_GENERIC_SETTING_NAME,
NM_VARIANT_TYPE_SETTING);
if (generic_setting) {
if (g_variant_lookup(generic_setting,
NM_SETTING_GENERIC_DEVICE_HANDLER,
"&s",
&device_handler)) {
return g_strdup(device_handler);
}
}
return NULL;
}
static void
_handle_action(GDBusMethodInvocation *invocation, GVariant *parameters, gboolean is_action2)
{
@ -739,6 +793,7 @@ _handle_action(GDBusMethodInvocation *invocation, GVariant *parameters, gboolean
gs_unref_variant GVariant *device_dhcp6_config = NULL;
const char *connectivity_state;
const char *vpn_ip_iface;
gs_free char *device_handler = NULL;
gs_unref_variant GVariant *vpn_proxy_properties = NULL;
gs_unref_variant GVariant *vpn_ip4_config = NULL;
gs_unref_variant GVariant *vpn_ip6_config = NULL;
@ -829,6 +884,8 @@ _handle_action(GDBusMethodInvocation *invocation, GVariant *parameters, gboolean
request->context = invocation;
request->action = g_strdup(action);
request->is_action2 = is_action2;
request->is_device_handler =
NM_IN_STRSET(action, NMD_ACTION_DEVICE_ADD, NMD_ACTION_DEVICE_DELETE);
request->envp = nm_dispatcher_utils_construct_envp(action,
connection,
@ -846,11 +903,14 @@ _handle_action(GDBusMethodInvocation *invocation, GVariant *parameters, gboolean
vpn_ip6_config,
&request->iface,
&error_message);
if (!error_message) {
if (request->is_device_handler) {
device_handler = get_device_handler(connection);
}
request->scripts = g_ptr_array_new_full(5, script_info_free);
sorted_scripts = find_scripts(request);
sorted_scripts = find_scripts(request, device_handler);
for (iter = sorted_scripts; iter; iter = g_slist_next(iter)) {
ScriptInfo *s;