page for a simple mechanism for shipping system services in disk images, in a similar fashion to OS
extensions. Note the differences in isolation between these two mechanisms: while system extensions directly extend
the underlying OS image with additional files that appear as if they were shipped in the OS image itself
- and thus imply no security isolation, portable services imply service-level sandboxing in one way or another.</para>
+ and thus imply no security isolation, portable services imply service-level sandboxing in one way or another.
+ Binaries shipped in system extensions can only link against libraries from the host if the extension is
+ bound to the version of the host OS (see further below) or otherwise have to use static linking.
+ Portable services, however, are fully decoupled from the host as they ship their own library dependencies.</para>
<para>The <filename>systemd-sysext.service</filename> and <filename>systemd-confext.service</filename>
services are guaranteed to finish start-up before <filename>basic.target</filename> is reached; i.e., by the time
but the used architecture identifiers are the same as for <varname>ConditionArchitecture=</varname>
described in <citerefentry><refentrytitle>systemd.unit</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
<varname>EXTENSION_RELOAD_MANAGER=</varname> can be set to 1 if the extension requires a service manager reload after application
- of the extension. Note that for the reasons mentioned earlier,
- <ulink url="https://systemd.io/PORTABLE_SERVICES">Portable Services</ulink> remain
- the recommended way to ship system services.
+ of the extension. <varname>EXTENSION_RESTART_UNITS=</varname> and
+ <varname>EXTENSION_RELOAD_OR_RESTART_UNITS=</varname> can be set to a whitespace-separated list of unit
+ names (e.g. <literal>EXTENSION_RESTART_UNITS="my.service other.service"</literal>) that
+ <command>systemd-sysext</command>/<command>systemd-confext</command> should restart resp. reload (or
+ restart, if the unit does not support reloading) after merging or refreshing the extension, and after
+ unmerging it. Use <varname>EXTENSION_RESTART_UNITS=</varname> for units whose binary the extension
+ replaces (a reload would not pick up the new binary), and <varname>EXTENSION_RELOAD_OR_RESTART_UNITS=</varname>
+ for units the extension extends with configuration drop-ins only (so reload-capable services are not
+ needlessly interrupted). If a unit has disappeared, e.g., on unmerge because it was part of the
+ extension, it is stopped instead. If the same unit is listed in both fields,
+ <varname>EXTENSION_RESTART_UNITS=</varname> takes precedence. Listing any unit in either field implies
+ <varname>EXTENSION_RELOAD_MANAGER=1</varname>, as the service manager would otherwise not yet (or no
+ longer) see the unit files shipped by the extension. Note that these settings do not take effect when
+ the extension is merged for the system already from the initrd and extensions should still ship a
+ <literal>.wants/</literal> or <literal>.upholds/</literal> symlink from the unit's install target to
+ ensure the unit starts at boot.
System extensions should not ship a <filename>/usr/lib/os-release</filename> file (as that would be merged
into the host <filename>/usr/</filename> tree, overriding the host OS version data, which is not desirable).
<para>When used with <command>merge</command>,
<command>unmerge</command> or <command>refresh</command>, do not reload daemon
after executing the changes even if an extension that is applied requires a reload via the
- <varname>EXTENSION_RELOAD_MANAGER=</varname> set to 1.</para>
+ <varname>EXTENSION_RELOAD_MANAGER=</varname> set to 1. This also skips processing any units
+ listed via <varname>EXTENSION_RESTART_UNITS=</varname> or
+ <varname>EXTENSION_RELOAD_OR_RESTART_UNITS=</varname>.</para>
<xi:include href="version-info.xml" xpointer="v255"/>
</listitem>
#include "blkid-util.h"
#include "blockdev-util.h"
#include "build.h"
+#include "bus-common-errors.h"
+#include "bus-locator.h"
#include "bus-polkit.h"
#include "bus-unit-util.h"
#include "bus-util.h"
+#include "bus-wait-for-jobs.h"
#include "capability-util.h"
#include "chase.h"
#include "conf-parser.h"
#include "rm-rf.h"
#include "runtime-scope.h"
#include "selinux-util.h"
+#include "set.h"
#include "sort-util.h"
#include "stat-util.h"
#include "string-table.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
+#include "unit-name.h"
#include "varlink-io.systemd.sysext.h"
#include "varlink-util.h"
#include "verbs.h"
return true;
}
-static int need_reload(
+static int split_unit_string(const char *s, const char *field, const char *extension, Set **units) {
+ _cleanup_strv_free_ char **split = NULL;
+ int r;
+
+ assert(field);
+ assert(extension);
+ assert(units);
+
+ if (isempty(s))
+ return 0;
+
+ split = strv_split(s, /* separators= whitespace */ NULL);
+ if (!split)
+ return log_oom();
+
+ STRV_FOREACH(u, split) {
+ if (!unit_name_is_valid(*u, UNIT_NAME_PLAIN|UNIT_NAME_INSTANCE)) {
+ log_warning("Invalid unit name '%s' in %s= of %s, ignoring.", *u, field, extension);
+ continue;
+ }
+
+ r = set_put_strdup(units, *u);
+ if (r < 0)
+ return log_oom();
+ }
+
+ return 0;
+}
+
+static int get_extension_release_metadata(
ImageClass image_class,
char **hierarchies,
- bool no_reload) {
-
- /* Parse the mounted images to find out if we need to reload the daemon. */
+ bool no_reload,
+ bool *ret_need_reload,
+ Set **ret_restart_units,
+ Set **ret_reload_or_restart_units) {
+
+ /* Parse the mounted images to find out if we need to reload the daemon, and which units to
+ * restart or reload-or-restart afterwards. */
+ _cleanup_set_free_ Set *restart_units = NULL, *reload_or_restart_units = NULL;
+ bool need_to_reload = false;
int r;
- if (no_reload)
- return false;
+ if (no_reload || !isempty(arg_root)) {
+ /* With --root= we'd be talking to the host service manager about extensions merged
+ * into a foreign root, which is never what we want. */
+ if (ret_need_reload)
+ *ret_need_reload = false;
+ if (ret_restart_units)
+ *ret_restart_units = NULL;
+ if (ret_reload_or_restart_units)
+ *ret_reload_or_restart_units = NULL;
+ return 0;
+ }
STRV_FOREACH(p, hierarchies) {
_cleanup_free_ char *f = NULL, *buf = NULL, *resolved = NULL;
STRV_FOREACH(extension, mounted_extensions) {
_cleanup_strv_free_ char **extension_release = NULL;
- const char *extension_reload_manager = NULL;
- int b;
+ const char *value;
r = load_extension_release_pairs(arg_root, image_class, *extension, /* relax_extension_release_check= */ true, &extension_release);
if (r < 0) {
continue;
}
- extension_reload_manager = strv_env_pairs_get(extension_release, "EXTENSION_RELOAD_MANAGER");
- if (isempty(extension_reload_manager))
- continue;
+ value = strv_env_pairs_get(extension_release, "EXTENSION_RELOAD_MANAGER");
+ if (!isempty(value)) {
+ int b = parse_boolean(value);
+ if (b < 0)
+ log_warning_errno(b, "Failed to parse EXTENSION_RELOAD_MANAGER= of %s, ignoring: %m", *extension);
+ else if (b)
+ need_to_reload = true;
+ }
- b = parse_boolean(extension_reload_manager);
- if (b < 0) {
- log_warning_errno(b, "Failed to parse the extension metadata to know if the manager needs to be reloaded, ignoring: %m");
- continue;
+ if (ret_restart_units) {
+ value = strv_env_pairs_get(extension_release, "EXTENSION_RESTART_UNITS");
+ r = split_unit_string(value, "EXTENSION_RESTART_UNITS", *extension, &restart_units);
+ if (r < 0)
+ return r;
+ if (!isempty(value))
+ /* Listing units to restart implies a manager reload because a unit
+ * shipped (and later removed) by the extension would otherwise not
+ * be visible to the manager when RestartUnit/StopUnit is issued. */
+ need_to_reload = true;
}
- if (b)
- /* If at least one extension wants a reload, we reload. */
- return true;
+ if (ret_reload_or_restart_units) {
+ value = strv_env_pairs_get(extension_release, "EXTENSION_RELOAD_OR_RESTART_UNITS");
+ r = split_unit_string(value, "EXTENSION_RELOAD_OR_RESTART_UNITS", *extension, &reload_or_restart_units);
+ if (r < 0)
+ return r;
+ if (!isempty(value))
+ need_to_reload = true;
+ }
}
}
- return false;
+ /* If a unit is listed in both, restart takes precedence over reload-or-restart. */
+ const char *u;
+ SET_FOREACH(u, restart_units)
+ free(set_remove(reload_or_restart_units, u));
+
+ if (ret_need_reload)
+ *ret_need_reload = need_to_reload;
+ if (ret_restart_units)
+ *ret_restart_units = TAKE_PTR(restart_units);
+ if (ret_reload_or_restart_units)
+ *ret_reload_or_restart_units = TAKE_PTR(reload_or_restart_units);
+ return 0;
}
static int move_submounts(const char *src, const char *dst) {
return bus_service_manager_reload(bus);
}
+static int dispatch_unit_method(sd_bus *bus, BusWaitForJobs *w, Set *units, const char *method) {
+ const char *unit;
+ int r;
+
+ assert(bus);
+ assert(w);
+ assert(method);
+
+ /* Issue the given method for each unit and wait for the jobs to complete. Fall back to StopUnit
+ * when the call reports that the unit's file is gone, e.g., on unmerge when the unit was part of
+ * the extension, because otherwise the running service is leaked. */
+
+ SET_FOREACH(unit, units) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ const char *m = method, *job;
+
+ r = bus_call_method(bus, bus_systemd_mgr, m, &error, &reply, "ss", unit, "replace");
+ if (r < 0 && sd_bus_error_has_names(&error, BUS_ERROR_NO_SUCH_UNIT, BUS_ERROR_LOAD_FAILED)) {
+ sd_bus_error_free(&error);
+ m = "StopUnit";
+ r = bus_call_method(bus, bus_systemd_mgr, m, &error, &reply, "ss", unit, "replace");
+ if (r < 0 && sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_UNIT)) {
+ log_debug("Unit '%s' is already gone, nothing to stop.", unit);
+ continue;
+ }
+ }
+ if (r < 0) {
+ log_warning("Failed to %s unit '%s': %s", m, unit, bus_error_message(&error, r));
+ continue;
+ }
+
+ r = sd_bus_message_read(reply, "o", &job);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = bus_wait_for_jobs_one(w, job, BUS_WAIT_JOBS_LOG_ERROR, /* extra_args= */ NULL);
+ if (r < 0)
+ log_warning_errno(r, "Failed to wait for %s job of unit '%s', ignoring: %m", m, unit);
+ }
+
+ return 0;
+}
+
+static int restart_units(Set *restart_units, Set *reload_or_restart_units) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL;
+ int r;
+
+ if (set_isempty(restart_units) && set_isempty(reload_or_restart_units))
+ return 0;
+
+ r = bus_connect_system_systemd(&bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get D-Bus connection: %m");
+
+ r = bus_wait_for_jobs_new(bus, &w);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up job watcher: %m");
+
+ r = dispatch_unit_method(bus, w, restart_units, "RestartUnit");
+ if (r < 0)
+ return r;
+
+ r = dispatch_unit_method(bus, w, reload_or_restart_units, "ReloadOrRestartUnit");
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
static int append_overlayfs_path_option(
char **options,
const char *separator,
char **hierarchies,
bool no_reload) {
+ _cleanup_set_free_ Set *units_to_restart = NULL, *units_to_reload_or_restart = NULL;
bool need_to_reload;
int r;
(void) DLOPEN_LIBMOUNT(LOG_DEBUG, SD_ELF_NOTE_DLOPEN_PRIORITY_REQUIRED);
- r = need_reload(image_class, hierarchies, no_reload);
+ r = get_extension_release_metadata(image_class, hierarchies, no_reload, &need_to_reload, &units_to_restart, &units_to_reload_or_restart);
if (r < 0)
return r;
- need_to_reload = r > 0;
r = pidref_safe_fork(
"(sd-unmerge)",
return r;
}
+ /* The sets will be empty when opted out */
+ r = restart_units(units_to_restart, units_to_reload_or_restart);
+ if (r < 0)
+ return r;
+
return 0;
}
if (r > 0)
return log_error_errno(SYNTHETIC_ERRNO(EPROTO), "Failed to merge hierarchies");
- r = need_reload(image_class, hierarchies, no_reload);
+ _cleanup_set_free_ Set *units_to_restart = NULL, *units_to_reload_or_restart = NULL;
+ bool need_to_reload;
+ r = get_extension_release_metadata(image_class, hierarchies, no_reload, &need_to_reload, &units_to_restart, &units_to_reload_or_restart);
if (r < 0)
return r;
- if (r > 0) {
+ if (need_to_reload) {
r = daemon_reload();
if (r < 0)
return r;
}
+ /* The sets will be empty when opted out */
+ r = restart_units(units_to_restart, units_to_reload_or_restart);
+ if (r < 0)
+ return r;
+
return 1;
}
extension_verify_after_unmerge "$fake_root" "$hierarchy" -h
)
+( init_trap
+: "Check EXTENSION_RESTART_UNITS= (re)starts units after merge and stops vanished ones on unmerge"
+# Talks to the real service manager, skip the --root= variant (it is a no-op there)
+if [ "$roots_dir" != "" ]; then
+ exit 0
+fi
+
+ext_name="test-restart-extension"
+ext_dir="/var/lib/extensions/$ext_name"
+host_unit="test-restart-host.service"
+host_unit_file="/run/systemd/system/$host_unit"
+ext_unit="test-restart-ext.service"
+
+cat >"$host_unit_file" <<EOF
+[Service]
+Type=simple
+ExecStart=sleep 9999
+EOF
+prepend_trap "rm -f ${host_unit_file@Q}; systemctl stop ${host_unit@Q} 2>/dev/null || :"
+systemctl daemon-reload
+systemctl start "$host_unit"
+host_pid_before=$(systemctl show -P MainPID "$host_unit")
+
+mkdir -p "$ext_dir/usr/lib/extension-release.d" "$ext_dir/usr/lib/systemd/system"
+prepend_trap "rm -rf ${ext_dir@Q}"
+cat >"$ext_dir/usr/lib/extension-release.d/extension-release.$ext_name" <<EOF
+ID=_any
+ARCHITECTURE=_any
+EXTENSION_RELOAD_MANAGER=1
+EXTENSION_RESTART_UNITS="$host_unit $ext_unit"
+EOF
+cat >"$ext_dir/usr/lib/systemd/system/$ext_unit" <<EOF
+[Service]
+Type=simple
+ExecStart=sleep 9999
+EOF
+
+# --no-reload skips both the daemon-reload and the restarts
+systemd-sysext merge --no-reload
+systemd-sysext refresh --always-refresh=yes --no-reload
+if [ "$(systemctl show -P MainPID "$host_unit")" != "$host_pid_before" ]; then
+ echo >&2 "Unexpected restart of host unit"
+ exit 1
+fi
+if systemctl --quiet is-active "$ext_unit"; then
+ echo >&2 "Unexpected start of extension unit"
+ exit 1
+fi
+systemd-sysext unmerge --no-reload
+
+# merge: host unit is restarted (new PID), extension unit is started
+systemd-sysext merge
+host_pid_merged=$(systemctl show -P MainPID "$host_unit")
+if [ "$host_pid_before" = "$host_pid_merged" ]; then
+ echo >&2 "Missing restart of host unit"
+ exit 1
+fi
+if ! systemctl --quiet is-active "$ext_unit"; then
+ echo >&2 "Missing start of extension unit"
+ exit 1
+fi
+host_pid_before_refresh="$host_pid_merged"
+systemd-sysext refresh --always-refresh=yes
+host_pid_merged=$(systemctl show -P MainPID "$host_unit")
+if [ "$host_pid_before_refresh" = "$host_pid_merged" ]; then
+ echo >&2 "Missing restart of host unit after refresh"
+ exit 1
+fi
+if ! systemctl --quiet is-active "$ext_unit"; then
+ echo >&2 "Missing start of extension unit"
+ exit 1
+fi
+
+# unmerge: host unit file is still in /run -> restart again (new PID)
+# but extension unit file is gone -> fallback to StopUnit
+systemd-sysext unmerge
+host_pid_unmerged=$(systemctl show -P MainPID "$host_unit")
+if [ "$host_pid_merged" = "$host_pid_unmerged" ]; then
+ echo >&2 "Missing restart of host unit"
+ exit 1
+fi
+if systemctl --quiet is-active "$ext_unit"; then
+ echo >&2 "Missing stop of extension unit"
+ exit 1
+fi
+)
+
+( init_trap
+: "Check EXTENSION_RELOAD_OR_RESTART_UNITS= reloads units after merge and stops vanished ones on unmerge"
+# Talks to the real service manager, skip the --root= variant (it is a no-op there)
+if [ "$roots_dir" != "" ]; then
+ exit 0
+fi
+
+ext_name="test-reload-or-restart-extension"
+ext_dir="/var/lib/extensions/$ext_name"
+host_unit="test-reload-or-restart-host.service"
+host_unit_file="/run/systemd/system/$host_unit"
+ext_unit="test-reload-or-restart-ext.service"
+host_stamp="/run/test-reload-or-restart-host.stamp"
+ext_stamp="/run/test-reload-or-restart-ext.stamp"
+
+cat >"$host_unit_file" <<EOF
+[Service]
+Type=simple
+ExecStart=sleep 9999
+ExecReload=touch $host_stamp
+EOF
+prepend_trap "rm -f ${host_unit_file@Q} ${host_stamp@Q}; systemctl stop ${host_unit@Q} 2>/dev/null || :"
+systemctl daemon-reload
+systemctl start "$host_unit"
+host_pid_before=$(systemctl show -P MainPID "$host_unit")
+
+mkdir -p "$ext_dir/usr/lib/extension-release.d" "$ext_dir/usr/lib/systemd/system"
+prepend_trap "rm -rf ${ext_dir@Q} ${ext_stamp@Q}"
+cat >"$ext_dir/usr/lib/extension-release.d/extension-release.$ext_name" <<EOF
+ID=_any
+ARCHITECTURE=_any
+EXTENSION_RELOAD_OR_RESTART_UNITS="$host_unit $ext_unit"
+EOF
+cat >"$ext_dir/usr/lib/systemd/system/$ext_unit" <<EOF
+[Service]
+Type=simple
+ExecStart=sleep 9999
+ExecReload=touch $ext_stamp
+EOF
+
+# merge: host unit supports reload -> reloaded (same PID, stamp file created),
+# extension unit is started (no reload because it was not running yet)
+rm -f "$host_stamp" "$ext_stamp"
+systemd-sysext merge
+host_pid_merged=$(systemctl show -P MainPID "$host_unit")
+if [ "$host_pid_before" != "$host_pid_merged" ]; then
+ echo >&2 "Unexpected restart of host unit (should have reloaded)"
+ exit 1
+fi
+if [ ! -e "$host_stamp" ]; then
+ echo >&2 "Host unit was not reloaded on merge (stamp missing)"
+ exit 1
+fi
+if ! systemctl --quiet is-active "$ext_unit"; then
+ echo >&2 "Missing start of extension unit"
+ exit 1
+fi
+ext_pid_merged=$(systemctl show -P MainPID "$ext_unit")
+
+# refresh: both units already running and reload-capable -> reloaded, MainPIDs unchanged,
+# both stamp files re-created
+rm -f "$host_stamp" "$ext_stamp"
+systemd-sysext refresh --always-refresh=yes
+host_pid_refreshed=$(systemctl show -P MainPID "$host_unit")
+if [ "$host_pid_merged" != "$host_pid_refreshed" ]; then
+ echo >&2 "Unexpected restart of host unit on refresh (should have reloaded)"
+ exit 1
+fi
+if [ ! -e "$host_stamp" ]; then
+ echo >&2 "Host unit was not reloaded on refresh (stamp missing)"
+ exit 1
+fi
+ext_pid_refreshed=$(systemctl show -P MainPID "$ext_unit")
+if [ "$ext_pid_merged" != "$ext_pid_refreshed" ]; then
+ echo >&2 "Unexpected restart of extension unit on refresh (should have reloaded)"
+ exit 1
+fi
+if [ ! -e "$ext_stamp" ]; then
+ echo >&2 "Extension unit was not reloaded on refresh (stamp missing)"
+ exit 1
+fi
+
+# unmerge: host unit file still in /run -> reloaded again (same PID, stamp re-created)
+# but extension unit file is gone -> fallback to StopUnit
+rm -f "$host_stamp" "$ext_stamp"
+systemd-sysext unmerge
+host_pid_unmerged=$(systemctl show -P MainPID "$host_unit")
+if [ "$host_pid_refreshed" != "$host_pid_unmerged" ]; then
+ echo >&2 "Unexpected restart of host unit on unmerge (should have reloaded)"
+ exit 1
+fi
+if [ ! -e "$host_stamp" ]; then
+ echo >&2 "Host unit was not reloaded on unmerge (stamp missing)"
+ exit 1
+fi
+if systemctl --quiet is-active "$ext_unit"; then
+ echo >&2 "Missing stop of extension unit"
+ exit 1
+fi
+)
+
} # End of run_sysext_tests