sysupdate: notify hook subscribers after a successful update

Define a new io.systemd.SysUpdate.Notify Varlink interface with a single
OnCompletedUpdate() method, and after sysupdate successfully installs an
update, invoke that method on every socket linked into
/run/systemd/sysupdate/notify/ via varlink_execute_directory(). This
gives other components a hook to react to applied updates (e.g. recompute
a TPM policy, link a freshly downloaded kernel, refresh extensions).

The notification carries the component name, the installed version and the
list of updated resources (transfer id + on-disk path). Subscribers are
free to ignore the parameters and just treat the call as a trigger.

Setting SYSTEMD_SYSUPDATE_FORCE_NOTIFY=1 forces the notification to be sent
even when no update was applied (in which case no resource list is included),
so follow-up work can be triggered unconditionally.

Fixes: #35988
This commit is contained in:
Lennart Poettering
2026-06-24 13:05:33 +02:00
parent 95a372a514
commit d36bdc9467
7 changed files with 132 additions and 0 deletions
+7
View File
@@ -870,3 +870,10 @@ Tools using the Varlink protocol (such as `varlinkctl`) or sd-bus (such as
'freshness' check via `BEST-BEFORE-YYYY-MM-DD` files in `SHA256SUMS` manifest
files is disabled, and updating from outdated manifests will not result in an
error.
* `$SYSTEMD_SYSUPDATE_FORCE_NOTIFY` – takes a boolean. If true the notification
callouts in `/run/systemd/sysupdate/notify/` are invoked even when no update
was applied (i.e. the system was already up-to-date). In this case the
notification carries no list of updated resources. This is useful to
unconditionally trigger follow-up work such as relinking a kernel or
recomputing a TPM policy.
+2
View File
@@ -68,6 +68,8 @@
#define VARLINK_PATH_MACHINED_RESOLVE_HOOK "/run/systemd/resolve.hook/io.systemd.Machine"
/* Path where to connect to send varlink prekill events */
#define VARLINK_DIR_OOMD_PREKILL_HOOK "/run/systemd/oomd.prekill.hook/"
/* Directory whose sockets receive io.systemd.SysUpdate.Notify() after a successful sysupdate run */
#define VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK "/run/systemd/sysupdate/notify/"
/* Recommended baseline - see README for details */
#define KERNEL_BASELINE_VERSION "5.14"
@@ -51,6 +51,7 @@
#include "varlink-io.systemd.Resolve.Monitor.h"
#include "varlink-io.systemd.Shutdown.h"
#include "varlink-io.systemd.StorageProvider.h"
#include "varlink-io.systemd.SysUpdate.Notify.h"
#include "varlink-io.systemd.Udev.h"
#include "varlink-io.systemd.Unit.h"
#include "varlink-io.systemd.UserDatabase.h"
@@ -227,6 +228,7 @@ TEST(parse_format) {
&vl_interface_io_systemd_Resolve_Monitor,
&vl_interface_io_systemd_Shutdown,
&vl_interface_io_systemd_StorageProvider,
&vl_interface_io_systemd_SysUpdate_Notify,
&vl_interface_io_systemd_Udev,
&vl_interface_io_systemd_Unit,
&vl_interface_io_systemd_UserDatabase,
+1
View File
@@ -250,6 +250,7 @@ shared_sources = files(
'varlink-io.systemd.Resolve.Monitor.c',
'varlink-io.systemd.Shutdown.c',
'varlink-io.systemd.StorageProvider.c',
'varlink-io.systemd.SysUpdate.Notify.c',
'varlink-io.systemd.Udev.c',
'varlink-io.systemd.Unit.c',
'varlink-io.systemd.UserDatabase.c',
@@ -0,0 +1,20 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "varlink-io.systemd.SysUpdate.Notify.h"
static SD_VARLINK_DEFINE_STRUCT_TYPE(
Resource,
SD_VARLINK_DEFINE_FIELD(transfer, SD_VARLINK_STRING, 0),
SD_VARLINK_DEFINE_FIELD(path, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_METHOD(
OnCompletedUpdate,
SD_VARLINK_DEFINE_INPUT(component, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_DEFINE_INPUT(version, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_DEFINE_INPUT_BY_TYPE(resources, Resource, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE));
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_SysUpdate_Notify,
"io.systemd.SysUpdate.Notify",
&vl_type_Resource,
&vl_method_OnCompletedUpdate);
@@ -0,0 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "sd-varlink-idl.h"
extern const sd_varlink_interface vl_interface_io_systemd_SysUpdate_Notify;
+94
View File
@@ -3,11 +3,14 @@
#include <unistd.h>
#include "sd-daemon.h"
#include "sd-json.h"
#include "sd-varlink.h"
#include "build.h"
#include "conf-files.h"
#include "constants.h"
#include "dissect-image.h"
#include "env-util.h"
#include "errno-util.h"
#include "fd-util.h"
#include "format-table.h"
@@ -36,6 +39,7 @@
#include "sysupdate-transfer.h"
#include "sysupdate-update-set.h"
#include "sysupdate-util.h"
#include "varlink-util.h"
#include "verbs.h"
static char *arg_definitions = NULL;
@@ -1192,6 +1196,82 @@ static int context_process_partial_and_pending(
return 1;
}
static int notify_subscribers_reply(
sd_varlink *link,
sd_json_variant *reply,
const char *error_id,
sd_varlink_reply_flags_t flags,
void *userdata) {
assert(link);
if (error_id)
log_warning("Notification subscriber '%s' returned error, ignoring: %s",
strna(sd_varlink_get_description(link)), error_id);
return 0;
}
static int context_notify_subscribers(Context *c, UpdateSet *us) {
int r;
assert(c);
/* 'us' is NULL when we are forced to notify even though no update was applied (via
* SYSTEMD_SYSUPDATE_FORCE_NOTIFY=1). In that case we send neither a version nor a resource list. */
_cleanup_(sd_json_variant_unrefp) sd_json_variant *resources = NULL;
if (us)
for (size_t i = 0; i < c->n_transfers; i++) {
Instance *inst = us->instances[i];
Transfer *t = c->transfers[i];
if (inst->resource == &t->target &&
!inst->is_pending)
continue;
/* Report where the resource was installed *to* (not the source it came from): the
* final on-disk path for filesystem targets, the partition device node for partition
* targets. */
const char *target_path =
RESOURCE_IS_FILESYSTEM(t->target.type) ? t->final_path :
t->target.type == RESOURCE_PARTITION ? t->partition_info.device :
NULL;
r = sd_json_variant_append_arraybo(
&resources,
SD_JSON_BUILD_PAIR_STRING("transfer", t->id),
SD_JSON_BUILD_PAIR_CONDITION(!!target_path, "path", SD_JSON_BUILD_STRING(target_path)));
if (r < 0)
return log_warning_errno(r, "Failed to build sysupdate notify resources list, skipping notification: %m");
}
_cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL;
r = sd_json_buildo(
&params,
SD_JSON_BUILD_PAIR_CONDITION(!!arg_component, "component", SD_JSON_BUILD_STRING(arg_component)),
SD_JSON_BUILD_PAIR_CONDITION(!!us, "version", SD_JSON_BUILD_STRING(us ? us->version : NULL)),
SD_JSON_BUILD_PAIR_CONDITION(!!resources, "resources", SD_JSON_BUILD_VARIANT(resources)));
if (r < 0)
return log_warning_errno(r, "Failed to build sysupdate notify parameters, skipping notification: %m");
ssize_t n = varlink_execute_directory(
VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK,
"io.systemd.SysUpdate.Notify.OnCompletedUpdate",
params,
/* more= */ false,
/* timeout_usec= */ 5 * USEC_PER_MINUTE,
notify_subscribers_reply,
/* userdata= */ NULL);
if (n < 0)
log_debug_errno(n, "Failed to dispatch sysupdate notification to %s, ignoring: %m",
VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK);
else if (n > 0)
log_debug("Dispatched sysupdate notification to %zi subscribers in %s.", n, VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK);
return 0;
}
static int context_install(
Context *c,
const char *version,
@@ -1236,6 +1316,9 @@ static int context_install(
log_info("%s Successfully installed update '%s'.", glyph(GLYPH_SPARKLES), us->version);
if (!arg_root)
(void) context_notify_subscribers(c, us);
(void) sd_notifyf(/* unset_environment= */ false,
"STATUS=Installed '%s'.", us->version);
@@ -1625,6 +1708,17 @@ static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flag
installed = r > 0;
}
/* context_install() returns > 0 (and emits a notification) only if it actually applied an update. If
* nothing was applied but SYSTEMD_SYSUPDATE_FORCE_NOTIFY=1 is set, still notify subscribers (without a
* resource list), so e.g. a kernel/policy refresh can be triggered unconditionally. */
if ((action_flags & UPDATE_ACTION_INSTALL) && !installed) {
int f = secure_getenv_bool("SYSTEMD_SYSUPDATE_FORCE_NOTIFY");
if (f < 0 && f != -ENXIO)
log_debug_errno(f, "Failed to parse $SYSTEMD_SYSUPDATE_FORCE_NOTIFY, ignoring: %m");
if (f > 0)
(void) context_notify_subscribers(context, /* us= */ NULL);
}
}
if (arg_cleanup > 0)