sysupdate: do a varlink callout to a ready when completing an update, and hook bootctl install, pcrlock and sysext refresh into it (#42365)
This commit is contained in:
@@ -280,12 +280,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
- firstboot/sysinstall: add simple interface for prompting users to enable
|
||||
"features" exposed by of sysupdate.
|
||||
|
||||
- bootctl link + sysupdate integration
|
||||
- make sysupdate call out to a special varlink dir on completion
|
||||
- bind bootctl link socket in there, which when invoked goes to new dir in
|
||||
/var/ where downloaded kernels+confext+sysext are dropped in (place in
|
||||
.v/) and then does "bootctl link" on them.
|
||||
|
||||
- a tool that can prep credentials, put them in the ESP, for provisioning
|
||||
systems for SBC or UEFI/HTTP boot. Should be doing what sysinstall does with
|
||||
the credentials, and maybe even *be* sysinstall.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -147,6 +147,34 @@
|
||||
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><option>link-auto</option></term>
|
||||
|
||||
<listitem><para>Like <command>link</command>, but instead of taking the kernel and sidecar resources
|
||||
as arguments, discovers them automatically. The following directories are searched, in decreasing
|
||||
order of priority: <filename>/etc/systemd/uki/</filename>, <filename>/run/systemd/uki/</filename>,
|
||||
<filename>/var/lib/systemd/uki/</filename> (where
|
||||
<citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>
|
||||
stages downloaded resources), <filename>/usr/local/lib/systemd/uki/</filename> and
|
||||
<filename>/usr/lib/systemd/uki/</filename>. In each directory the UKI is taken from
|
||||
<filename>kernel.efi</filename>, or — if absent — the newest version in the versioned directory
|
||||
<filename>kernel.efi.v/</filename> (see
|
||||
<citerefentry><refentrytitle>systemd.v</refentrytitle><manvolnum>7</manvolnum></citerefentry>); the
|
||||
UKI from the highest-priority directory that has one is used. Sidecar resources are picked up from
|
||||
the <filename>extras.d/</filename> subdirectory of each, matching the suffixes
|
||||
<literal>.sysext.raw</literal>, <literal>.confext.raw</literal> and <literal>.cred</literal>, each
|
||||
either as a plain file or as a versioned <literal>.v/</literal> directory; resources are combined
|
||||
across all directories, with the same resource in a higher-priority directory taking precedence. Any
|
||||
files specified with <option>--extra=</option> are linked in addition to the discovered ones.</para>
|
||||
|
||||
<para>This is used by
|
||||
<citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>
|
||||
when it completes an update, so a freshly downloaded kernel and its sidecar resources are linked into
|
||||
place without specifying any paths.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><option>cleanup</option></term>
|
||||
|
||||
@@ -845,6 +873,32 @@ Boot Loader Entries:
|
||||
</example>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>Files</title>
|
||||
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term><filename>/etc/systemd/uki/</filename></term>
|
||||
<term><filename>/run/systemd/uki/</filename></term>
|
||||
<term><filename>/var/lib/systemd/uki/</filename></term>
|
||||
<term><filename>/usr/local/lib/systemd/uki/</filename></term>
|
||||
<term><filename>/usr/lib/systemd/uki/</filename></term>
|
||||
|
||||
<listitem><para>The <command>link-auto</command> command discovers the UKI to install from these
|
||||
directories (listed in decreasing order of priority). It uses <filename>kernel.efi</filename> or the
|
||||
versioned <filename>kernel.efi.v/</filename> directory from the highest-priority directory that
|
||||
contains one. Additional sidecar resources (system extension images, configuration extension images,
|
||||
credential files) are picked up from the <filename>extras.d/</filename> subdirectory of each, combined
|
||||
across all directories. <filename>/var/lib/systemd/uki/</filename> is the directory where
|
||||
<citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>
|
||||
is supposed to place downloaded kernels, system extensions, configuration extensions and
|
||||
credentials.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>See Also</title>
|
||||
<para><simplelist type="inline">
|
||||
|
||||
@@ -40,6 +40,9 @@ enable systemd-report-sign-tsm.socket
|
||||
enable systemd-report.socket
|
||||
enable systemd-resolved.service
|
||||
enable systemd-sysext.service
|
||||
enable systemd-sysupdate-notify-bootctl.socket
|
||||
enable systemd-sysupdate-notify-pcrlock.socket
|
||||
enable systemd-sysupdate-notify-sysext.socket
|
||||
enable systemd-timesyncd.service
|
||||
enable systemd-tpm2-clear.service
|
||||
enable systemd-userdbd.socket
|
||||
|
||||
@@ -87,7 +87,7 @@ _bootctl() {
|
||||
fi
|
||||
|
||||
local -A VERBS=(
|
||||
[STANDALONE]='help status install update remove is-installed random-seed list set-timeout set-timeout-oneshot cleanup'
|
||||
[STANDALONE]='help status install update remove is-installed random-seed list set-timeout set-timeout-oneshot cleanup link-auto'
|
||||
[BOOTENTRY]='set-default set-oneshot set-sysfail set-preferred unlink'
|
||||
[BOOLEAN]='reboot-to-firmware'
|
||||
[FILE]='kernel-identify kernel-inspect link'
|
||||
|
||||
@@ -75,6 +75,7 @@ _bootctl_reboot-to-firmware() {
|
||||
"set-timeout-oneshot:Set the menu timeout for the next boot only"
|
||||
"unlink:Remove boot loader entry"
|
||||
"link:Add boot loader entry"
|
||||
"link-auto:Add boot loader entry for kernel and resources staged in /var/lib/systemd/uki/"
|
||||
"cleanup:Remove files in ESP not referenced in any boot entry"
|
||||
"kernel-identify:Identify kernel image type"
|
||||
"kernel-inspect:Print details about the kernel image"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
#include "tmpfile-util.h"
|
||||
#include "umask-util.h"
|
||||
#include "utf8.h"
|
||||
#include "varlink-util.h"
|
||||
|
||||
typedef enum InstallOperation {
|
||||
INSTALL_NEW,
|
||||
@@ -2121,6 +2122,10 @@ int vl_method_install(
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
r = varlink_check_privileged_peer(link);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (!IN_SET(p.context.operation, INSTALL_NEW, INSTALL_UPDATE))
|
||||
return sd_varlink_error_invalid_parameter_name(link, "operation");
|
||||
|
||||
|
||||
+620
-138
@@ -32,6 +32,7 @@
|
||||
#include "parse-argument.h"
|
||||
#include "path-util.h"
|
||||
#include "recurse-dir.h"
|
||||
#include "set.h"
|
||||
#include "stat-util.h"
|
||||
#include "stdio-util.h"
|
||||
#include "string-util.h"
|
||||
@@ -39,6 +40,8 @@
|
||||
#include "tmpfile-util.h"
|
||||
#include "uki.h"
|
||||
#include "utf8.h"
|
||||
#include "varlink-util.h"
|
||||
#include "vpick.h"
|
||||
|
||||
/* Keeps track of an "extra" file to associate with the type 1 entries to generate */
|
||||
typedef struct ExtraFile {
|
||||
@@ -205,77 +208,14 @@ static void link_context_done(LinkContext *c) {
|
||||
c->linked_ids = strv_free(c->linked_ids);
|
||||
}
|
||||
|
||||
static int link_context_from_cmdline(LinkContext *ret, const char *kernel) {
|
||||
static int link_context_acquire_dollar_boot(LinkContext *b) {
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
assert(kernel);
|
||||
|
||||
_cleanup_(link_context_done) LinkContext b = LINK_CONTEXT_NULL;
|
||||
b.entry_token_type = arg_entry_token_type;
|
||||
b.tries_left = arg_tries_left;
|
||||
b.entry_commit = arg_entry_commit;
|
||||
b.keep_free = arg_keep_free;
|
||||
|
||||
if (strdup_to(&b.entry_token, arg_entry_token) < 0 ||
|
||||
strdup_to(&b.entry_title, arg_entry_title) < 0 ||
|
||||
strdup_to(&b.entry_version, arg_entry_version) < 0)
|
||||
return log_oom();
|
||||
|
||||
if (arg_root) {
|
||||
b.root_fd = open(arg_root, O_CLOEXEC|O_DIRECTORY|O_PATH);
|
||||
if (b.root_fd < 0)
|
||||
return log_error_errno(errno, "Failed to open root directory '%s': %m", arg_root);
|
||||
|
||||
if (strdup_to(&b.root, arg_root) < 0)
|
||||
return log_oom();
|
||||
} else
|
||||
b.root_fd = XAT_FDROOT;
|
||||
|
||||
r = path_extract_filename(kernel, &b.kernel_filename);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to extract filename from kernel path '%s': %m", kernel);
|
||||
if (!efi_loader_entry_resource_filename_valid(b.kernel_filename))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Kernel '%s' is not suitable for reference in a boot menu entry.", kernel);
|
||||
b.kernel_fd = xopenat_full(AT_FDCWD, kernel, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
|
||||
if (b.kernel_fd < 0)
|
||||
return log_error_errno(b.kernel_fd, "Failed to open kernel path '%s': %m", kernel);
|
||||
|
||||
KernelImageType kit = _KERNEL_IMAGE_TYPE_INVALID;
|
||||
r = inspect_kernel(b.kernel_fd, /* filename= */ NULL, &kit);
|
||||
if (r == -EBADMSG)
|
||||
return log_error_errno(r, "Kernel image '%s' is not valid.", kernel);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to determine kernel image type of '%s': %m", kernel);
|
||||
if (kit != KERNEL_IMAGE_TYPE_UKI)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Kernel image '%s' is not a UKI.", kernel);
|
||||
|
||||
STRV_FOREACH(x, arg_extras) {
|
||||
_cleanup_free_ char *fn = NULL;
|
||||
r = path_extract_filename(*x, &fn);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to extract filename from path '%s': %m", *x);
|
||||
if (r == O_DIRECTORY)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Extra file path '%s' does not refer to regular file.", *x);
|
||||
|
||||
_cleanup_close_ int fd = -EBADF;
|
||||
fd = xopenat_full(AT_FDCWD, *x, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
|
||||
if (fd < 0)
|
||||
return log_error_errno(fd, "Failed to open '%s': %m", *x);
|
||||
|
||||
if (!GREEDY_REALLOC(b.extra, b.n_extra+1))
|
||||
return log_oom();
|
||||
|
||||
b.extra[b.n_extra++] = (ExtraFile) {
|
||||
.source_fd = TAKE_FD(fd),
|
||||
.filename = TAKE_PTR(fn),
|
||||
.temp_fd = -EBADF,
|
||||
};
|
||||
}
|
||||
assert(b);
|
||||
|
||||
r = acquire_xbootldr(
|
||||
/* unprivileged_mode= */ false,
|
||||
&b.dollar_boot_fd,
|
||||
&b->dollar_boot_fd,
|
||||
/* ret_uuid= */ NULL,
|
||||
/* ret_devid= */ NULL);
|
||||
if (r < 0)
|
||||
@@ -288,19 +228,19 @@ static int link_context_from_cmdline(LinkContext *ret, const char *kernel) {
|
||||
if (!e)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "XBOOTLDR path '%s' not below specified root '%s', refusing.", arg_xbootldr_path, arg_root);
|
||||
|
||||
r = strdup_to(&b.dollar_boot_path, e);
|
||||
r = strdup_to(&b->dollar_boot_path, e);
|
||||
} else
|
||||
r = strdup_to(&b.dollar_boot_path, arg_xbootldr_path);
|
||||
r = strdup_to(&b->dollar_boot_path, arg_xbootldr_path);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
|
||||
b.dollar_boot_source = BOOT_ENTRY_XBOOTLDR;
|
||||
b->dollar_boot_source = BOOT_ENTRY_XBOOTLDR;
|
||||
} else {
|
||||
/* No XBOOTLDR has been found, look for ESP */
|
||||
|
||||
r = acquire_esp(/* unprivileged_mode= */ false,
|
||||
/* graceful= */ false,
|
||||
&b.dollar_boot_fd,
|
||||
&b->dollar_boot_fd,
|
||||
/* ret_part= */ NULL,
|
||||
/* ret_pstart= */ NULL,
|
||||
/* ret_psize= */ NULL,
|
||||
@@ -316,15 +256,126 @@ static int link_context_from_cmdline(LinkContext *ret, const char *kernel) {
|
||||
if (!e)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "ESP path '%s' not below specified root '%s', refusing.", arg_esp_path, arg_root);
|
||||
|
||||
r = strdup_to(&b.dollar_boot_path, e);
|
||||
r = strdup_to(&b->dollar_boot_path, e);
|
||||
} else
|
||||
r = strdup_to(&b.dollar_boot_path, arg_esp_path);
|
||||
r = strdup_to(&b->dollar_boot_path, arg_esp_path);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
|
||||
b.dollar_boot_source = BOOT_ENTRY_ESP;
|
||||
b->dollar_boot_source = BOOT_ENTRY_ESP;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Fills the bits of a LinkContext that both "bootctl link" and "bootctl link-auto" derive from the global
|
||||
* command line options (everything except the kernel image and extra files themselves). */
|
||||
static int link_context_from_cmdline_common(LinkContext *b) {
|
||||
assert(b);
|
||||
|
||||
b->entry_token_type = arg_entry_token_type;
|
||||
b->tries_left = arg_tries_left;
|
||||
b->entry_commit = arg_entry_commit;
|
||||
b->keep_free = arg_keep_free;
|
||||
|
||||
if (strdup_to(&b->entry_token, arg_entry_token) < 0 ||
|
||||
strdup_to(&b->entry_title, arg_entry_title) < 0 ||
|
||||
strdup_to(&b->entry_version, arg_entry_version) < 0)
|
||||
return log_oom();
|
||||
|
||||
if (arg_root) {
|
||||
b->root_fd = open(arg_root, O_CLOEXEC|O_DIRECTORY|O_PATH);
|
||||
if (b->root_fd < 0)
|
||||
return log_error_errno(errno, "Failed to open root directory '%s': %m", arg_root);
|
||||
|
||||
if (strdup_to(&b->root, arg_root) < 0)
|
||||
return log_oom();
|
||||
} else
|
||||
b->root_fd = XAT_FDROOT;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Appends the files passed via --extra= on the command line as extra resources to the link context. */
|
||||
static int link_context_add_cmdline_extras(LinkContext *b) {
|
||||
int r;
|
||||
|
||||
assert(b);
|
||||
|
||||
STRV_FOREACH(x, arg_extras) {
|
||||
_cleanup_free_ char *fn = NULL;
|
||||
r = path_extract_filename(*x, &fn);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to extract filename from path '%s': %m", *x);
|
||||
if (r == O_DIRECTORY)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Extra file path '%s' does not refer to regular file.", *x);
|
||||
|
||||
_cleanup_close_ int fd = xopenat_full(AT_FDCWD, *x, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
|
||||
if (fd < 0)
|
||||
return log_error_errno(fd, "Failed to open '%s': %m", *x);
|
||||
|
||||
if (!GREEDY_REALLOC(b->extra, b->n_extra+1))
|
||||
return log_oom();
|
||||
|
||||
b->extra[b->n_extra++] = (ExtraFile) {
|
||||
.source_fd = TAKE_FD(fd),
|
||||
.filename = TAKE_PTR(fn),
|
||||
.temp_fd = -EBADF,
|
||||
};
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int validate_kernel(int kernel_fd, const char *filename) {
|
||||
int r;
|
||||
|
||||
assert(kernel_fd >= 0);
|
||||
|
||||
KernelImageType kit = _KERNEL_IMAGE_TYPE_INVALID;
|
||||
r = inspect_kernel(kernel_fd, /* filename= */ NULL, &kit);
|
||||
if (r == -EBADMSG)
|
||||
return log_error_errno(r, "UKI '%s' is not valid.", filename);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to determine kernel image type of '%s': %m", filename);
|
||||
if (kit != KERNEL_IMAGE_TYPE_UKI)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Image '%s' is not a UKI.", filename);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int link_context_from_cmdline(LinkContext *ret, const char *kernel) {
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
assert(kernel);
|
||||
|
||||
_cleanup_(link_context_done) LinkContext b = LINK_CONTEXT_NULL;
|
||||
r = link_context_from_cmdline_common(&b);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = path_extract_filename(kernel, &b.kernel_filename);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to extract filename from kernel path '%s': %m", kernel);
|
||||
if (!efi_loader_entry_resource_filename_valid(b.kernel_filename))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Kernel '%s' is not suitable for reference in a boot menu entry.", kernel);
|
||||
b.kernel_fd = xopenat_full(AT_FDCWD, kernel, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
|
||||
if (b.kernel_fd < 0)
|
||||
return log_error_errno(b.kernel_fd, "Failed to open kernel path '%s': %m", kernel);
|
||||
|
||||
r = validate_kernel(b.kernel_fd, kernel);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = link_context_add_cmdline_extras(&b);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = link_context_acquire_dollar_boot(&b);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret = TAKE_GENERIC(b, LinkContext, LINK_CONTEXT_NULL);
|
||||
return 0;
|
||||
}
|
||||
@@ -993,6 +1044,311 @@ int verb_link(int argc, char *argv[], uintptr_t data, void *userdata) {
|
||||
return run_link(&c);
|
||||
}
|
||||
|
||||
/* Directories (relative to the operative root) below which "bootctl link-auto" looks for a staged UKI and
|
||||
* extra resources, in decreasing priority order. /var/lib/systemd/uki/ is where systemd-sysupdate stages
|
||||
* downloaded resources; the others follow the usual configuration search path precedence, with
|
||||
* /var/lib/ slotted in just below the volatile locations and above the vendor trees. */
|
||||
static const char* const uki_auto_dirs[] = {
|
||||
"etc/systemd/uki",
|
||||
"run/systemd/uki",
|
||||
"var/lib/systemd/uki",
|
||||
"usr/local/lib/systemd/uki",
|
||||
"usr/lib/systemd/uki",
|
||||
};
|
||||
|
||||
static const PickFilter pick_filter_uki = {
|
||||
.type_mask = UINT32_C(1) << DT_REG,
|
||||
.architecture = _ARCHITECTURE_INVALID,
|
||||
.suffix = ".efi",
|
||||
};
|
||||
|
||||
/* The recognized suffixes of extra resources we pick up alongside the UKI. Each may appear as a plain file
|
||||
* (e.g. foo.sysext.raw) or as a versioned directory (e.g. foo.sysext.raw.v/) resolved via the vpick
|
||||
* mechanism. */
|
||||
static const char* const auto_link_extra_suffixes[] = {
|
||||
".sysext.raw",
|
||||
".confext.raw",
|
||||
".cred",
|
||||
};
|
||||
|
||||
static int link_context_add_extra(LinkContext *c, int dir_fd, const char *path, const char *filename) {
|
||||
assert(c);
|
||||
assert(dir_fd >= 0);
|
||||
assert(path);
|
||||
assert(filename);
|
||||
|
||||
if (!efi_loader_entry_resource_filename_valid(filename))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Extra file '%s' is not suitable for reference in a boot menu entry, refusing.", filename);
|
||||
|
||||
_cleanup_close_ int fd = xopenat_full(dir_fd, path, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
|
||||
if (fd < 0)
|
||||
return log_error_errno(fd, "Failed to open extra file '%s': %m", path);
|
||||
|
||||
_cleanup_free_ char *fn = strdup(filename);
|
||||
if (!fn)
|
||||
return log_oom();
|
||||
|
||||
if (!GREEDY_REALLOC(c->extra, c->n_extra + 1))
|
||||
return log_oom();
|
||||
|
||||
c->extra[c->n_extra++] = (ExtraFile) {
|
||||
.source_fd = TAKE_FD(fd),
|
||||
.filename = TAKE_PTR(fn),
|
||||
.temp_fd = -EBADF,
|
||||
};
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Adds the extra resources found in <uki_dir_fd>/extras.d/ to the link context. The 'seen' set tracks the
|
||||
* logical resource names already added by a higher-priority directory, so the same resource appearing in
|
||||
* multiple directories is taken from the highest-priority one only. */
|
||||
static int link_context_add_extras(LinkContext *c, int uki_dir_fd, Set **seen) {
|
||||
int r;
|
||||
|
||||
assert(c);
|
||||
assert(uki_dir_fd >= 0);
|
||||
assert(seen);
|
||||
|
||||
_cleanup_close_ int extras_dir_fd = chase_and_openat(
|
||||
c->root_fd,
|
||||
uki_dir_fd,
|
||||
"extras.d",
|
||||
/* chase_flags= */ 0,
|
||||
O_DIRECTORY|O_CLOEXEC,
|
||||
/* ret_path= */ NULL);
|
||||
if (extras_dir_fd == -ENOENT)
|
||||
return 0;
|
||||
if (extras_dir_fd < 0)
|
||||
return log_error_errno(extras_dir_fd, "Failed to open extras.d/ directory: %m");
|
||||
|
||||
_cleanup_free_ DirectoryEntries *des = NULL;
|
||||
r = readdir_all(extras_dir_fd, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE|RECURSE_DIR_SORT, &des);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to enumerate extras.d/ directory: %m");
|
||||
|
||||
FOREACH_ARRAY(i, des->entries, des->n_entries) {
|
||||
struct dirent *de = *i;
|
||||
|
||||
if (!IN_SET(de->d_type, DT_DIR, DT_REG))
|
||||
continue;
|
||||
|
||||
FOREACH_ELEMENT(suffix, auto_link_extra_suffixes) {
|
||||
_cleanup_free_ char *dot_v = strjoin(*suffix, ".v");
|
||||
if (!dot_v)
|
||||
return log_oom();
|
||||
|
||||
bool versioned;
|
||||
_cleanup_free_ char *key = NULL;
|
||||
if (endswith(de->d_name, *suffix)) {
|
||||
if (de->d_type != DT_REG)
|
||||
continue;
|
||||
|
||||
versioned = false;
|
||||
key = strdup(de->d_name);
|
||||
|
||||
} else if (endswith(de->d_name, dot_v)) {
|
||||
if (de->d_type != DT_DIR)
|
||||
continue;
|
||||
|
||||
/* The dedup key is the directory entry name with any trailing ".v" stripped, so that
|
||||
* a plain file and a versioned directory of the same resource collapse, and a
|
||||
* higher-priority directory wins. */
|
||||
versioned = true;
|
||||
key = strndup(de->d_name, strlen(de->d_name) - strlen(".v"));
|
||||
} else
|
||||
continue;
|
||||
if (!key)
|
||||
return log_oom();
|
||||
|
||||
if (set_contains(*seen, key))
|
||||
break;
|
||||
|
||||
if (versioned) {
|
||||
/* A versioned directory: resolve the best version via vpick, relative to the
|
||||
* extras directory fd. We deliberately do not honour boot-counting suffixes
|
||||
* here. */
|
||||
_cleanup_(pick_result_done) PickResult pick = PICK_RESULT_NULL;
|
||||
const PickFilter filter = {
|
||||
.type_mask = UINT32_C(1) << DT_REG,
|
||||
.architecture = _ARCHITECTURE_INVALID,
|
||||
.suffix = *suffix,
|
||||
};
|
||||
|
||||
r = path_pick(/* root_path= */ NULL,
|
||||
/* root_fd= */ c->root_fd,
|
||||
/* dir_fd= */ extras_dir_fd,
|
||||
de->d_name,
|
||||
&filter,
|
||||
/* n_filters= */ 1,
|
||||
/* flags= */ 0,
|
||||
&pick);
|
||||
if (r == -ENOENT || r == 0) {
|
||||
log_debug("No matching entry in versioned directory '%s', skipping.", de->d_name);
|
||||
break;
|
||||
}
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to pick version in '%s': %m", de->d_name);
|
||||
|
||||
_cleanup_free_ char *fn = NULL;
|
||||
r = path_extract_filename(pick.path, &fn);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to extract filename from '%s': %m", pick.path);
|
||||
|
||||
r = link_context_add_extra(c, extras_dir_fd, pick.path, fn);
|
||||
} else
|
||||
/* A plain file. */
|
||||
r = link_context_add_extra(c, extras_dir_fd, de->d_name, de->d_name);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = set_ensure_consume(seen, &string_hash_ops_free, TAKE_PTR(key));
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Looks for a UKI (kernel.efi, or the best version in kernel.efi.v/) in the directory referenced by
|
||||
* uki_dir_fd and, if found, fills c->kernel_fd and c->kernel_filename. Returns 1 if found, 0 if not, and a
|
||||
* negative errno on error. We deliberately do not honour boot-counting suffixes here. */
|
||||
static int link_context_find_kernel(LinkContext *c, int uki_dir_fd) {
|
||||
int r;
|
||||
|
||||
assert(c);
|
||||
assert(uki_dir_fd >= 0);
|
||||
|
||||
_cleanup_free_ char *filename = NULL;
|
||||
_cleanup_close_ int kernel_fd = xopenat_full(
|
||||
uki_dir_fd, "kernel.efi",
|
||||
O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY|O_NOFOLLOW,
|
||||
XO_REGULAR,
|
||||
/* mode= */ MODE_INVALID);
|
||||
if (kernel_fd == -ENOENT) {
|
||||
_cleanup_(pick_result_done) PickResult pick = PICK_RESULT_NULL;
|
||||
r = path_pick(/* root_path= */ NULL,
|
||||
/* root_fd= */ c->root_fd,
|
||||
/* dir_fd= */ uki_dir_fd,
|
||||
"kernel.efi.v",
|
||||
&pick_filter_uki,
|
||||
/* n_filters= */ 1,
|
||||
/* flags= */ 0,
|
||||
&pick);
|
||||
if (r == -ENOENT || r == 0)
|
||||
return 0;
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to pick UKI version in 'kernel.efi.v': %m");
|
||||
|
||||
r = path_extract_filename(pick.path, &filename);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to extract filename from '%s': %m", pick.path);
|
||||
|
||||
kernel_fd = fd_reopen(pick.fd, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
|
||||
if (kernel_fd < 0)
|
||||
return log_error_errno(kernel_fd, "Failed to open UKI '%s': %m", pick.path);
|
||||
|
||||
r = fd_verify_regular(kernel_fd);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "UKI '%s' is not a regular file: %m", pick.path);
|
||||
|
||||
} else if (kernel_fd < 0)
|
||||
return log_error_errno(kernel_fd, "Failed to open 'kernel.efi': %m");
|
||||
else {
|
||||
filename = strdup("kernel.efi");
|
||||
if (!filename)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
if (!efi_loader_entry_resource_filename_valid(filename))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UKI '%s' is not suitable for reference in a boot menu entry.", filename);
|
||||
|
||||
r = validate_kernel(kernel_fd, filename);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
c->kernel_fd = TAKE_FD(kernel_fd);
|
||||
c->kernel_filename = TAKE_PTR(filename);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Discovers the staged UKI and extra resources below the uki_auto_dirs[] (relative to the already-pinned
|
||||
* c->root_fd) and fills c->kernel_fd, c->kernel_filename and c->extra[] accordingly. The directories are
|
||||
* searched in priority order: the UKI is taken from the first directory that has one, and extra resources
|
||||
* are combined across all directories with higher-priority directories winning on conflicts. Returns 1 if a
|
||||
* UKI was found, 0 if there is nothing to link, and a negative errno on error. */
|
||||
static int link_context_discover_resources(LinkContext *c) {
|
||||
_cleanup_set_free_ Set *seen = NULL;
|
||||
int r;
|
||||
|
||||
assert(c);
|
||||
|
||||
FOREACH_ELEMENT(dir, uki_auto_dirs) {
|
||||
_cleanup_close_ int uki_dir_fd = chase_and_openat(
|
||||
/* root_fd= */ c->root_fd,
|
||||
/* dir_fd= */ c->root_fd,
|
||||
*dir,
|
||||
CHASE_MUST_BE_DIRECTORY,
|
||||
O_RDONLY|O_DIRECTORY|O_CLOEXEC,
|
||||
/* ret_path= */ NULL);
|
||||
if (uki_dir_fd == -ENOENT)
|
||||
continue;
|
||||
if (uki_dir_fd < 0)
|
||||
return log_error_errno(uki_dir_fd, "Failed to open '%s': %m", *dir);
|
||||
|
||||
/* Take the UKI from the first (highest-priority) directory that has one. */
|
||||
if (c->kernel_fd < 0) {
|
||||
r = link_context_find_kernel(c, uki_dir_fd);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
r = link_context_add_extras(c, uki_dir_fd, &seen);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (c->kernel_fd < 0) {
|
||||
log_debug("No UKI found in any of the search directories, nothing to link.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int verb_link_auto(int argc, char *argv[], uintptr_t data, void *userdata) {
|
||||
int r;
|
||||
|
||||
assert(argc == 1);
|
||||
|
||||
_cleanup_(link_context_done) LinkContext c = LINK_CONTEXT_NULL;
|
||||
r = link_context_from_cmdline_common(&c);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = link_context_discover_resources(&c);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0) {
|
||||
log_info("No UKI staged for linking, nothing to do.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* In addition to the auto-discovered resources, also honour any files passed via --extra=. */
|
||||
r = link_context_add_cmdline_extras(&c);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = link_context_acquire_dollar_boot(&c);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return run_link(&c);
|
||||
}
|
||||
|
||||
static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_boot_entry_token_type, BootEntryTokenType, boot_entry_token_type_from_string);
|
||||
|
||||
typedef struct LinkParameters {
|
||||
@@ -1089,6 +1445,107 @@ static int dispatch_extras(const char *name, sd_json_variant *v, sd_json_dispatc
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Resolves the root_fd of the LinkContext from the dispatched Link/LinkAuto parameters, fills in the
|
||||
* boot entry token type default, and validates the entry title/version/commit fields. Returns 0 on success,
|
||||
* non-zero (Varlink error or negative errno) otherwise. */
|
||||
static int vl_link_prepare(sd_varlink *link, LinkParameters *p) {
|
||||
int r;
|
||||
|
||||
assert(link);
|
||||
assert(p);
|
||||
|
||||
if (p->root_fd_index != UINT_MAX) {
|
||||
p->context.root_fd = sd_varlink_peek_dup_fd(link, p->root_fd_index);
|
||||
if (p->context.root_fd < 0)
|
||||
return log_debug_errno(p->context.root_fd, "Failed to acquire root fd from Varlink: %m");
|
||||
|
||||
r = fd_verify_safe_flags_full(p->context.root_fd, O_DIRECTORY);
|
||||
if (r < 0)
|
||||
return sd_varlink_error_invalid_parameter_name(link, "rootFileDescriptor");
|
||||
|
||||
r = fd_verify_directory(p->context.root_fd);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Specified file descriptor does not refer to a directory: %m");
|
||||
|
||||
if (!p->context.root) {
|
||||
r = fd_get_path(p->context.root_fd, &p->context.root);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to get path of file descriptor: %m");
|
||||
|
||||
if (empty_or_root(p->context.root))
|
||||
p->context.root = mfree(p->context.root);
|
||||
}
|
||||
} else if (p->context.root) {
|
||||
p->context.root_fd = open(p->context.root, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
|
||||
if (p->context.root_fd < 0)
|
||||
return log_debug_errno(errno, "Failed to open '%s': %m", p->context.root);
|
||||
} else
|
||||
p->context.root_fd = XAT_FDROOT;
|
||||
|
||||
if (p->context.entry_token_type < 0)
|
||||
p->context.entry_token_type = BOOT_ENTRY_TOKEN_AUTO;
|
||||
|
||||
if (p->context.entry_title && !efi_loader_entry_title_valid(p->context.entry_title))
|
||||
return sd_varlink_error_invalid_parameter_name(link, "entryTitle");
|
||||
|
||||
if (p->context.entry_version && !version_is_valid_versionspec(p->context.entry_version))
|
||||
return sd_varlink_error_invalid_parameter_name(link, "entryVersion");
|
||||
|
||||
if (p->context.entry_commit != 0 && !entry_commit_valid(p->context.entry_commit))
|
||||
return sd_varlink_error_invalid_parameter_name(link, "entryCommit");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Resolves $BOOT, runs the link operation and sends the reply. Expects the kernel and extra files to already
|
||||
* be set up in the context. If with_ids is true the reply carries the list of created entry IDs (as expected
|
||||
* by Link()/LinkAuto()), otherwise an empty reply is sent (as expected by
|
||||
* io.systemd.SysUpdate.Notify.OnCompletedUpdate(), which declares no output fields). */
|
||||
static int vl_link_finish(sd_varlink *link, LinkParameters *p, bool with_ids) {
|
||||
int r;
|
||||
|
||||
assert(link);
|
||||
assert(p);
|
||||
|
||||
r = find_xbootldr_and_warn_at(
|
||||
p->context.root_fd,
|
||||
/* path= */ NULL,
|
||||
/* unprivileged_mode= */ false,
|
||||
&p->context.dollar_boot_path,
|
||||
&p->context.dollar_boot_fd);
|
||||
if (r < 0) {
|
||||
if (r != -ENOKEY)
|
||||
return r;
|
||||
|
||||
/* No XBOOTLDR found, let's look for ESP then. */
|
||||
|
||||
r = find_esp_and_warn_at(
|
||||
p->context.root_fd,
|
||||
/* path= */ NULL,
|
||||
/* unprivileged_mode= */ false,
|
||||
&p->context.dollar_boot_path,
|
||||
&p->context.dollar_boot_fd);
|
||||
if (r == -ENOKEY)
|
||||
return sd_varlink_error(link, "io.systemd.BootControl.NoDollarBootFound", NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
p->context.dollar_boot_source = BOOT_ENTRY_ESP;
|
||||
} else
|
||||
p->context.dollar_boot_source = BOOT_ENTRY_XBOOTLDR;
|
||||
|
||||
r = run_link(&p->context);
|
||||
if (r == -EUNATCH) /* no boot entry token is set */
|
||||
return sd_varlink_error(link, "io.systemd.BootControl.BootEntryTokenUnavailable", NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (with_ids)
|
||||
return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRV("ids", p->context.linked_ids));
|
||||
|
||||
return sd_varlink_reply(link, NULL);
|
||||
}
|
||||
|
||||
int vl_method_link(
|
||||
sd_varlink *link,
|
||||
sd_json_variant *parameters,
|
||||
@@ -1125,45 +1582,13 @@ int vl_method_link(
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (p.root_fd_index != UINT_MAX) {
|
||||
p.context.root_fd = sd_varlink_peek_dup_fd(link, p.root_fd_index);
|
||||
if (p.context.root_fd < 0)
|
||||
return log_debug_errno(p.context.root_fd, "Failed to acquire root fd from Varlink: %m");
|
||||
r = varlink_check_privileged_peer(link);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = fd_verify_safe_flags_full(p.context.root_fd, O_DIRECTORY);
|
||||
if (r < 0)
|
||||
return sd_varlink_error_invalid_parameter_name(link, "rootFileDescriptor");
|
||||
|
||||
r = fd_verify_directory(p.context.root_fd);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Specified file descriptor does not refer to a directory: %m");
|
||||
|
||||
if (!p.context.root) {
|
||||
r = fd_get_path(p.context.root_fd, &p.context.root);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to get path of file descriptor: %m");
|
||||
|
||||
if (empty_or_root(p.context.root))
|
||||
p.context.root = mfree(p.context.root);
|
||||
}
|
||||
} else if (p.context.root) {
|
||||
p.context.root_fd = open(p.context.root, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
|
||||
if (p.context.root_fd < 0)
|
||||
return log_debug_errno(errno, "Failed to open '%s': %m", p.context.root);
|
||||
} else
|
||||
p.context.root_fd = XAT_FDROOT;
|
||||
|
||||
if (p.context.entry_token_type < 0)
|
||||
p.context.entry_token_type = BOOT_ENTRY_TOKEN_AUTO;
|
||||
|
||||
if (p.context.entry_title && !efi_loader_entry_title_valid(p.context.entry_title))
|
||||
return sd_varlink_error_invalid_parameter_name(link, "entryTitle");
|
||||
|
||||
if (p.context.entry_version && !version_is_valid_versionspec(p.context.entry_version))
|
||||
return sd_varlink_error_invalid_parameter_name(link, "entryVersion");
|
||||
|
||||
if (p.context.entry_commit != 0 && !entry_commit_valid(p.context.entry_commit))
|
||||
return sd_varlink_error_invalid_parameter_name(link, "entryCommit");
|
||||
r = vl_link_prepare(link, &p);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
p.context.kernel_fd = sd_varlink_peek_dup_fd(link, p.kernel_fd_index);
|
||||
if (p.context.kernel_fd < 0)
|
||||
@@ -1186,38 +1611,95 @@ int vl_method_link(
|
||||
if (kit != KERNEL_IMAGE_TYPE_UKI)
|
||||
return sd_varlink_error(link, "io.systemd.BootControl.InvalidKernelImage", NULL);
|
||||
|
||||
r = find_xbootldr_and_warn_at(
|
||||
p.context.root_fd,
|
||||
/* path= */ NULL,
|
||||
/* unprivileged_mode= */ false,
|
||||
&p.context.dollar_boot_path,
|
||||
&p.context.dollar_boot_fd);
|
||||
if (r < 0) {
|
||||
if (r != -ENOKEY)
|
||||
return r;
|
||||
return vl_link_finish(link, &p, /* with_ids= */ true);
|
||||
}
|
||||
|
||||
/* No XBOOTLDR found, let's look for ESP then. */
|
||||
int vl_method_link_auto(
|
||||
sd_varlink *link,
|
||||
sd_json_variant *parameters,
|
||||
sd_varlink_method_flags_t flags,
|
||||
void *userdata) {
|
||||
|
||||
r = find_esp_and_warn_at(
|
||||
p.context.root_fd,
|
||||
/* path= */ NULL,
|
||||
/* unprivileged_mode= */ false,
|
||||
&p.context.dollar_boot_path,
|
||||
&p.context.dollar_boot_fd);
|
||||
if (r == -ENOKEY)
|
||||
return sd_varlink_error(link, "io.systemd.BootControl.NoDollarBootFound", NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
int r;
|
||||
|
||||
p.context.dollar_boot_source = BOOT_ENTRY_ESP;
|
||||
} else
|
||||
p.context.dollar_boot_source = BOOT_ENTRY_XBOOTLDR;
|
||||
assert(link);
|
||||
|
||||
r = run_link(&p.context);
|
||||
if (r == -EUNATCH) /* no boot entry token is set */
|
||||
return sd_varlink_error(link, "io.systemd.BootControl.BootEntryTokenUnavailable", NULL);
|
||||
_cleanup_(link_parameters_done) LinkParameters p = {
|
||||
.context = LINK_CONTEXT_NULL,
|
||||
.root_fd_index = UINT_MAX,
|
||||
.kernel_fd_index = UINT_MAX,
|
||||
.link = link,
|
||||
};
|
||||
|
||||
static const sd_json_dispatch_field dispatch_table[] = {
|
||||
{ "rootFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, voffsetof(p, root_fd_index), 0 },
|
||||
{ "rootDirectory", SD_JSON_VARIANT_STRING, json_dispatch_path, voffsetof(p, context.root), 0 },
|
||||
{ "bootEntryTokenType", SD_JSON_VARIANT_STRING, json_dispatch_boot_entry_token_type, voffsetof(p, context.entry_token_type), 0 },
|
||||
{ "entryTitle", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, voffsetof(p, context.entry_title), 0 },
|
||||
{ "entryVersion", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, voffsetof(p, context.entry_version), 0 },
|
||||
{ "entryCommit", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_uint64, voffsetof(p, context.entry_commit), 0 },
|
||||
{ "triesLeft", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, voffsetof(p, context.tries_left), 0 },
|
||||
{ "keepFree", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, context.keep_free), 0 },
|
||||
{},
|
||||
};
|
||||
|
||||
r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
r = varlink_check_privileged_peer(link);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRV("ids", p.context.linked_ids));
|
||||
r = vl_link_prepare(link, &p);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
r = link_context_discover_resources(&p.context);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0) /* Nothing staged for linking. */
|
||||
return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRV("ids", STRV_EMPTY));
|
||||
|
||||
return vl_link_finish(link, &p, /* with_ids= */ true);
|
||||
}
|
||||
|
||||
int vl_method_on_completed_update(
|
||||
sd_varlink *link,
|
||||
sd_json_variant *parameters,
|
||||
sd_varlink_method_flags_t flags,
|
||||
void *userdata) {
|
||||
|
||||
int r;
|
||||
|
||||
assert(link);
|
||||
|
||||
/* Only honour update notifications if they come from root */
|
||||
r = varlink_check_privileged_peer(link);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* Triggered by systemd-sysupdate after an update completed. We deliberately ignore all parameters
|
||||
* (we don't even dispatch them) and behave like LinkAuto() with default parameters: discover the
|
||||
* staged UKI and extra resources and link them in. Contrary to LinkAuto() we reply without any
|
||||
* parameters, matching the io.systemd.SysUpdate.Notify.OnCompletedUpdate() signature. */
|
||||
|
||||
_cleanup_(link_parameters_done) LinkParameters p = {
|
||||
.context = LINK_CONTEXT_NULL,
|
||||
.root_fd_index = UINT_MAX,
|
||||
.kernel_fd_index = UINT_MAX,
|
||||
.link = link,
|
||||
};
|
||||
|
||||
r = vl_link_prepare(link, &p);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
r = link_context_discover_resources(&p.context);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0) /* Nothing staged for linking. */
|
||||
return sd_varlink_reply(link, NULL);
|
||||
|
||||
return vl_link_finish(link, &p, /* with_ids= */ false);
|
||||
}
|
||||
|
||||
@@ -4,5 +4,8 @@
|
||||
#include "shared-forward.h"
|
||||
|
||||
int verb_link(int argc, char *argv[], uintptr_t data, void *userdata);
|
||||
int verb_link_auto(int argc, char *argv[], uintptr_t data, void *userdata);
|
||||
|
||||
int vl_method_link(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
|
||||
int vl_method_link_auto(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
|
||||
int vl_method_on_completed_update(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "errno-util.h"
|
||||
#include "log.h"
|
||||
#include "parse-util.h"
|
||||
#include "varlink-util.h"
|
||||
|
||||
int verb_reboot_to_firmware(int argc, char *argv[], uintptr_t _data, void *userdata) {
|
||||
int r;
|
||||
@@ -61,6 +62,10 @@ int vl_method_set_reboot_to_firmware(sd_varlink *link, sd_json_variant *paramete
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
r = varlink_check_privileged_peer(link);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = efi_set_reboot_to_firmware(b);
|
||||
if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
|
||||
return sd_varlink_error(link, "io.systemd.BootControl.RebootToFirmwareNotSupported", NULL);
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "stat-util.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "varlink-util.h"
|
||||
|
||||
typedef struct UnlinkContext {
|
||||
char *root;
|
||||
@@ -593,6 +594,10 @@ int vl_method_unlink(
|
||||
if (p.id && !efi_loader_entry_name_valid(p.id))
|
||||
return sd_varlink_error_invalid_parameter_name(link, "id");
|
||||
|
||||
r = varlink_check_privileged_peer(link);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (p.root_fd_index != UINT_MAX) {
|
||||
p.context.root_fd = sd_varlink_peek_dup_fd(link, p.root_fd_index);
|
||||
if (p.context.root_fd < 0)
|
||||
|
||||
+17
-8
@@ -43,6 +43,7 @@
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "varlink-io.systemd.BootControl.h"
|
||||
#include "varlink-io.systemd.SysUpdate.Notify.h"
|
||||
#include "varlink-util.h"
|
||||
#include "verbs.h"
|
||||
#include "virt.h"
|
||||
@@ -380,6 +381,9 @@ VERB_SCOPE(, verb_unlink, "unlink", "ID", VERB_ANY, 2, 0,
|
||||
VERB_SCOPE(, verb_link, "link", "KERNEL", 2, 2, 0,
|
||||
"Create boot loader entry for specified kernel");
|
||||
|
||||
VERB_SCOPE_NOARG(, verb_link_auto, "link-auto",
|
||||
"Create boot loader entry for the kernel and extra resources staged in /var/lib/systemd/uki/");
|
||||
|
||||
VERB_SCOPE_NOARG(, verb_cleanup, "cleanup",
|
||||
"Remove files in ESP not referenced in any boot entry");
|
||||
|
||||
@@ -822,18 +826,23 @@ static int vl_server(void) {
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to allocate Varlink server: %m");
|
||||
|
||||
r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_BootControl);
|
||||
r = sd_varlink_server_add_interface_many(
|
||||
varlink_server,
|
||||
&vl_interface_io_systemd_BootControl,
|
||||
&vl_interface_io_systemd_SysUpdate_Notify);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to add Varlink interface: %m");
|
||||
return log_error_errno(r, "Failed to add Varlink interfaces: %m");
|
||||
|
||||
r = sd_varlink_server_bind_method_many(
|
||||
varlink_server,
|
||||
"io.systemd.BootControl.ListBootEntries", vl_method_list_boot_entries,
|
||||
"io.systemd.BootControl.SetRebootToFirmware", vl_method_set_reboot_to_firmware,
|
||||
"io.systemd.BootControl.GetRebootToFirmware", vl_method_get_reboot_to_firmware,
|
||||
"io.systemd.BootControl.Install", vl_method_install,
|
||||
"io.systemd.BootControl.Link", vl_method_link,
|
||||
"io.systemd.BootControl.Unlink", vl_method_unlink);
|
||||
"io.systemd.BootControl.ListBootEntries", vl_method_list_boot_entries,
|
||||
"io.systemd.BootControl.SetRebootToFirmware", vl_method_set_reboot_to_firmware,
|
||||
"io.systemd.BootControl.GetRebootToFirmware", vl_method_get_reboot_to_firmware,
|
||||
"io.systemd.BootControl.Install", vl_method_install,
|
||||
"io.systemd.BootControl.Link", vl_method_link,
|
||||
"io.systemd.BootControl.LinkAuto", vl_method_link_auto,
|
||||
"io.systemd.BootControl.Unlink", vl_method_unlink,
|
||||
"io.systemd.SysUpdate.Notify.OnCompletedUpdate", vl_method_on_completed_update);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to bind Varlink methods: %m");
|
||||
|
||||
|
||||
@@ -3719,8 +3719,9 @@ static int pin_rootfs(
|
||||
if (context->root_image) {
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
r = path_pick(/* root_path= */ NULL,
|
||||
/* root_fd= */ AT_FDCWD,
|
||||
/* dir_fd= */ AT_FDCWD,
|
||||
context->root_image,
|
||||
pick_filter_image_raw,
|
||||
ELEMENTSOF(pick_filter_image_raw),
|
||||
@@ -3760,8 +3761,9 @@ static int pin_rootfs(
|
||||
if (context->root_directory) {
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
r = path_pick(/* root_path= */ NULL,
|
||||
/* root_fd= */ AT_FDCWD,
|
||||
/* dir_fd= */ AT_FDCWD,
|
||||
context->root_directory,
|
||||
pick_filter_image_dir,
|
||||
ELEMENTSOF(pick_filter_image_dir),
|
||||
@@ -3789,8 +3791,9 @@ static int pin_rootfs(
|
||||
if (context->root_mstack) {
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
r = path_pick(/* root_path= */ NULL,
|
||||
/* root_fd= */ AT_FDCWD,
|
||||
/* dir_fd= */ AT_FDCWD,
|
||||
context->root_mstack,
|
||||
pick_filter_image_mstack,
|
||||
/* n_filters= */ 1,
|
||||
|
||||
@@ -564,8 +564,9 @@ static int append_extensions(
|
||||
_cleanup_free_ char *mount_point = NULL;
|
||||
const MountImage *m = mount_images + i;
|
||||
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
r = path_pick(/* root_path= */ NULL,
|
||||
/* root_fd= */ AT_FDCWD,
|
||||
/* dir_fd= */ AT_FDCWD,
|
||||
m->source,
|
||||
pick_filter_image_raw,
|
||||
ELEMENTSOF(pick_filter_image_raw),
|
||||
@@ -636,8 +637,9 @@ static int append_extensions(
|
||||
if (startswith(e, "+"))
|
||||
e++;
|
||||
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
r = path_pick(/* root_path= */ NULL,
|
||||
/* root_fd= */ AT_FDCWD,
|
||||
/* dir_fd= */ AT_FDCWD,
|
||||
e,
|
||||
pick_filter_image_dir,
|
||||
ELEMENTSOF(pick_filter_image_dir),
|
||||
|
||||
@@ -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,
|
||||
|
||||
+31
-5
@@ -61,6 +61,7 @@
|
||||
#include "unit-name.h"
|
||||
#include "utf8.h"
|
||||
#include "varlink-io.systemd.PCRLock.h"
|
||||
#include "varlink-io.systemd.SysUpdate.Notify.h"
|
||||
#include "varlink-util.h"
|
||||
#include "verbs.h"
|
||||
|
||||
@@ -5467,6 +5468,27 @@ static int vl_method_remove_policy(sd_varlink *link, sd_json_variant *parameters
|
||||
return sd_varlink_reply(link, NULL);
|
||||
}
|
||||
|
||||
static int vl_method_on_completed_update(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
|
||||
int r;
|
||||
|
||||
assert(link);
|
||||
|
||||
/* Triggered by systemd-sysupdate after an update completed. We deliberately ignore all parameters
|
||||
* (we don't even dispatch them) and simply recompute the PCR policy unconditionally, since the set
|
||||
* of measured components might have changed. */
|
||||
|
||||
/* Only honour update notifications if they come from root */
|
||||
r = varlink_check_privileged_peer(link);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = make_policy(/* force= */ false, /* recovery_pin_mode= */ RECOVERY_PIN_HIDE);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return sd_varlink_reply(link, NULL);
|
||||
}
|
||||
|
||||
static int run(int argc, char *argv[]) {
|
||||
int r;
|
||||
|
||||
@@ -5496,15 +5518,19 @@ static int run(int argc, char *argv[]) {
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to allocate Varlink server: %m");
|
||||
|
||||
r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_PCRLock);
|
||||
r = sd_varlink_server_add_interface_many(
|
||||
varlink_server,
|
||||
&vl_interface_io_systemd_PCRLock,
|
||||
&vl_interface_io_systemd_SysUpdate_Notify);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to add Varlink interface: %m");
|
||||
return log_error_errno(r, "Failed to add Varlink interfaces: %m");
|
||||
|
||||
r = sd_varlink_server_bind_method_many(
|
||||
varlink_server,
|
||||
"io.systemd.PCRLock.ReadEventLog", vl_method_read_event_log,
|
||||
"io.systemd.PCRLock.MakePolicy", vl_method_make_policy,
|
||||
"io.systemd.PCRLock.RemovePolicy", vl_method_remove_policy);
|
||||
"io.systemd.PCRLock.ReadEventLog", vl_method_read_event_log,
|
||||
"io.systemd.PCRLock.MakePolicy", vl_method_make_policy,
|
||||
"io.systemd.PCRLock.RemovePolicy", vl_method_remove_policy,
|
||||
"io.systemd.SysUpdate.Notify.OnCompletedUpdate", vl_method_on_completed_update);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to bind Varlink methods: %m");
|
||||
|
||||
|
||||
@@ -821,8 +821,9 @@ static int extract_image_and_extensions(
|
||||
/* If we get a path, then check if it can be resolved with vpick. We need this as we might just
|
||||
* get a simple image name, which would make vpick error out. */
|
||||
if (path_is_absolute(name_or_path)) {
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
r = path_pick(/* root_path= */ NULL,
|
||||
/* root_fd= */ AT_FDCWD,
|
||||
/* dir_fd= */ AT_FDCWD,
|
||||
name_or_path,
|
||||
pick_filter_image_any,
|
||||
ELEMENTSOF(pick_filter_image_any),
|
||||
@@ -860,8 +861,9 @@ static int extract_image_and_extensions(
|
||||
const char *path = *p;
|
||||
|
||||
if (path_is_absolute(*p)) {
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
r = path_pick(/* root_path= */ NULL,
|
||||
/* root_fd= */ AT_FDCWD,
|
||||
/* dir_fd= */ AT_FDCWD,
|
||||
*p,
|
||||
pick_filter_image_any,
|
||||
ELEMENTSOF(pick_filter_image_any),
|
||||
@@ -2201,8 +2203,9 @@ static bool marker_matches_images(const char *marker, const char *name_or_path,
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to extract image name from %s, ignoring: %m", image);
|
||||
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
r = path_pick(/* root_path= */ NULL,
|
||||
/* root_fd= */ AT_FDCWD,
|
||||
/* dir_fd= */ AT_FDCWD,
|
||||
*image_name_or_path,
|
||||
pick_filter_image_any,
|
||||
ELEMENTSOF(pick_filter_image_any),
|
||||
|
||||
@@ -956,6 +956,7 @@ int image_find(RuntimeScope scope,
|
||||
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
r = path_pick(root,
|
||||
rfd,
|
||||
rfd,
|
||||
fname_path, /* This has to be the unresolved entry with the .v suffix */
|
||||
&filter,
|
||||
@@ -1177,6 +1178,7 @@ int image_discover(
|
||||
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
r = path_pick(root,
|
||||
rfd,
|
||||
rfd,
|
||||
fname_path, /* This has to be the unresolved entry with the .v suffix */
|
||||
&filter,
|
||||
|
||||
@@ -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',
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ static int mstack_load_one(MStack *mstack, const char *dir, int dir_fd, const ch
|
||||
};
|
||||
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
r = path_pick(dir, dir_fd, fname, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE, &result);
|
||||
r = path_pick(dir, dir_fd, dir_fd, fname, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE, &result);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to resolve '%s' directory: %m", fname);
|
||||
if (r == 0)
|
||||
|
||||
@@ -185,6 +185,27 @@ static SD_VARLINK_DEFINE_METHOD(
|
||||
SD_VARLINK_FIELD_COMMENT("The IDs of the created boot loader entries."),
|
||||
SD_VARLINK_DEFINE_OUTPUT(ids, SD_VARLINK_STRING, SD_VARLINK_ARRAY));
|
||||
|
||||
static SD_VARLINK_DEFINE_METHOD(
|
||||
LinkAuto,
|
||||
SD_VARLINK_FIELD_COMMENT("Index into array of file descriptors passed along with this message, pointing to file descriptor to root file system to operate on"),
|
||||
SD_VARLINK_DEFINE_INPUT(rootFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_FIELD_COMMENT("Root directory to operate relative to. If both this and rootFileDescriptor is specified, this is purely informational. If only this is specified, it is what will be used."),
|
||||
SD_VARLINK_DEFINE_INPUT(rootDirectory, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_FIELD_COMMENT("Selects how to identify boot entries"),
|
||||
SD_VARLINK_DEFINE_INPUT_BY_TYPE(bootEntryTokenType, BootEntryTokenType, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_FIELD_COMMENT("The entry title for the newly created boot menu entry"),
|
||||
SD_VARLINK_DEFINE_INPUT(entryTitle, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_FIELD_COMMENT("The entry version for the newly created boot menu entry"),
|
||||
SD_VARLINK_DEFINE_INPUT(entryVersion, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_FIELD_COMMENT("The commit number for the newly created boot menu entry"),
|
||||
SD_VARLINK_DEFINE_INPUT(entryCommit, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_FIELD_COMMENT("What to set the triesLeft counter of the boot menu entry to initially."),
|
||||
SD_VARLINK_DEFINE_INPUT(triesLeft, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_FIELD_COMMENT("How much space to always keep free on ESP/XBOOTLDR. Defaults to 1 MiB"),
|
||||
SD_VARLINK_DEFINE_INPUT(keepFree, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_FIELD_COMMENT("The IDs of the created boot loader entries."),
|
||||
SD_VARLINK_DEFINE_OUTPUT(ids, SD_VARLINK_STRING, SD_VARLINK_ARRAY));
|
||||
|
||||
static SD_VARLINK_DEFINE_ERROR(
|
||||
RebootToFirmwareNotSupported);
|
||||
|
||||
@@ -233,6 +254,8 @@ SD_VARLINK_DEFINE_INTERFACE(
|
||||
&vl_method_Unlink,
|
||||
SD_VARLINK_SYMBOL_COMMENT("Install a kernel as boot menu item"),
|
||||
&vl_method_Link,
|
||||
SD_VARLINK_SYMBOL_COMMENT("Like Link(), but automatically discovers the UKI and extra resources staged below /var/lib/systemd/uki/"),
|
||||
&vl_method_LinkAuto,
|
||||
SD_VARLINK_SYMBOL_COMMENT("SetRebootToFirmware() and GetRebootToFirmware() return this if the firmware does not actually support the reboot-to-firmware-UI concept."),
|
||||
&vl_error_RebootToFirmwareNotSupported,
|
||||
SD_VARLINK_SYMBOL_COMMENT("No boot entry defined."),
|
||||
|
||||
@@ -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;
|
||||
+52
-38
@@ -176,8 +176,9 @@ static int errno_from_mode(uint32_t type_mask, mode_t found) {
|
||||
}
|
||||
|
||||
static int pin_choice(
|
||||
const char *toplevel_path,
|
||||
int toplevel_fd,
|
||||
const char *root_path,
|
||||
int root_fd,
|
||||
int dir_fd,
|
||||
const char *inode_path,
|
||||
int _inode_fd, /* we always take ownership of the fd, even on failure */
|
||||
unsigned tries_left,
|
||||
@@ -190,14 +191,15 @@ static int pin_choice(
|
||||
_cleanup_free_ char *resolved_path = NULL;
|
||||
int r;
|
||||
|
||||
assert(wildcard_fd_is_valid(toplevel_fd));
|
||||
assert(wildcard_fd_is_valid(root_fd));
|
||||
assert(wildcard_fd_is_valid(dir_fd));
|
||||
assert(inode_path);
|
||||
assert(filter);
|
||||
assert(ret);
|
||||
|
||||
if (inode_fd < 0 || FLAGS_SET(flags, PICK_RESOLVE)) {
|
||||
r = chaseat(toplevel_fd,
|
||||
toplevel_fd,
|
||||
r = chaseat(root_fd,
|
||||
dir_fd,
|
||||
inode_path,
|
||||
/* flags= */ 0,
|
||||
FLAGS_SET(flags, PICK_RESOLVE) ? &resolved_path : NULL,
|
||||
@@ -212,11 +214,11 @@ static int pin_choice(
|
||||
struct stat st;
|
||||
if (fstat(inode_fd, &st) < 0)
|
||||
return log_debug_errno(errno, "Failed to stat discovered inode '%s%s': %m",
|
||||
empty_to_root(toplevel_path), skip_leading_slash(inode_path));
|
||||
empty_to_root(root_path), skip_leading_slash(inode_path));
|
||||
|
||||
if (filter->type_mask != 0 && !BIT_SET(filter->type_mask, IFTODT(st.st_mode))) {
|
||||
log_debug("Inode '%s/%s' has wrong type, found '%s'.",
|
||||
empty_to_root(toplevel_path), skip_leading_slash(inode_path),
|
||||
empty_to_root(root_path), skip_leading_slash(inode_path),
|
||||
inode_type_to_string(st.st_mode));
|
||||
*ret = PICK_RESULT_NULL;
|
||||
return 0;
|
||||
@@ -311,8 +313,9 @@ static bool architecture_matches(const PickFilter *filter, Architecture a) {
|
||||
}
|
||||
|
||||
static int make_choice(
|
||||
const char *toplevel_path,
|
||||
int toplevel_fd,
|
||||
const char *root_path,
|
||||
int root_fd,
|
||||
int dir_fd,
|
||||
const char *inode_path,
|
||||
int _inode_fd, /* we always take ownership of the fd, even on failure */
|
||||
const PickFilter *filter,
|
||||
@@ -322,13 +325,14 @@ static int make_choice(
|
||||
_cleanup_close_ int inode_fd = TAKE_FD(_inode_fd);
|
||||
int r;
|
||||
|
||||
assert(wildcard_fd_is_valid(toplevel_fd));
|
||||
assert(wildcard_fd_is_valid(root_fd));
|
||||
assert(wildcard_fd_is_valid(dir_fd));
|
||||
assert(inode_path);
|
||||
assert(filter);
|
||||
assert(ret);
|
||||
|
||||
if (inode_fd < 0) {
|
||||
r = chaseat(toplevel_fd, toplevel_fd, inode_path, /* flags= */ 0, NULL, &inode_fd);
|
||||
r = chaseat(root_fd, dir_fd, inode_path, /* flags= */ 0, NULL, &inode_fd);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
@@ -345,18 +349,19 @@ static int make_choice(
|
||||
return log_oom_debug();
|
||||
|
||||
_cleanup_close_ int object_fd = -EBADF;
|
||||
r = chaseat(toplevel_fd, toplevel_fd, p, /* flags= */ 0, &object_path, &object_fd);
|
||||
r = chaseat(root_fd, dir_fd, p, /* flags= */ 0, &object_path, &object_fd);
|
||||
if (r == -ENOENT) {
|
||||
*ret = PICK_RESULT_NULL;
|
||||
return 0;
|
||||
}
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to open '%s/%s': %m",
|
||||
empty_to_root(toplevel_path), skip_leading_slash(p));
|
||||
empty_to_root(root_path), skip_leading_slash(p));
|
||||
|
||||
return pin_choice(
|
||||
toplevel_path,
|
||||
toplevel_fd,
|
||||
root_path,
|
||||
root_fd,
|
||||
dir_fd,
|
||||
FLAGS_SET(flags, PICK_RESOLVE) ? object_path : p,
|
||||
TAKE_FD(object_fd), /* unconditionally pass ownership of the fd */
|
||||
/* tries_left= */ UINT_MAX,
|
||||
@@ -371,16 +376,16 @@ static int make_choice(
|
||||
/* Underspecified, so we do our enumeration dance */
|
||||
|
||||
/* Convert O_PATH to a regular directory fd */
|
||||
_cleanup_close_ int dir_fd = fd_reopen(inode_fd, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
|
||||
if (dir_fd < 0)
|
||||
return log_debug_errno(dir_fd, "Failed to reopen '%s/%s' as directory: %m",
|
||||
empty_to_root(toplevel_path), skip_leading_slash(inode_path));
|
||||
_cleanup_close_ int enumerate_fd = fd_reopen(inode_fd, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
|
||||
if (enumerate_fd < 0)
|
||||
return log_debug_errno(enumerate_fd, "Failed to reopen '%s/%s' as directory: %m",
|
||||
empty_to_root(root_path), skip_leading_slash(inode_path));
|
||||
|
||||
_cleanup_free_ DirectoryEntries *de = NULL;
|
||||
r = readdir_all(dir_fd, 0, &de);
|
||||
r = readdir_all(enumerate_fd, 0, &de);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to read directory '%s/%s': %m",
|
||||
empty_to_root(toplevel_path), skip_leading_slash(inode_path));
|
||||
empty_to_root(root_path), skip_leading_slash(inode_path));
|
||||
|
||||
_cleanup_(pick_result_done) PickResult best = PICK_RESULT_NULL;
|
||||
|
||||
@@ -457,8 +462,9 @@ static int make_choice(
|
||||
return log_oom_debug();
|
||||
|
||||
_cleanup_(pick_result_done) PickResult found = PICK_RESULT_NULL;
|
||||
r = pin_choice(toplevel_path,
|
||||
toplevel_fd,
|
||||
r = pin_choice(root_path,
|
||||
root_fd,
|
||||
dir_fd,
|
||||
p,
|
||||
/* _inode_fd= */ -EBADF,
|
||||
found_tries_left,
|
||||
@@ -501,8 +507,9 @@ static int make_choice(
|
||||
}
|
||||
|
||||
static int path_pick_one(
|
||||
const char *toplevel_path,
|
||||
int toplevel_fd,
|
||||
const char *root_path,
|
||||
int root_fd,
|
||||
int dir_fd,
|
||||
const char *path,
|
||||
const PickFilter *filter,
|
||||
PickFlags flags,
|
||||
@@ -513,7 +520,8 @@ static int path_pick_one(
|
||||
uint32_t filter_type_mask;
|
||||
int r;
|
||||
|
||||
assert(wildcard_fd_is_valid(toplevel_fd));
|
||||
assert(wildcard_fd_is_valid(root_fd));
|
||||
assert(wildcard_fd_is_valid(dir_fd));
|
||||
assert(path);
|
||||
assert(filter);
|
||||
assert(ret);
|
||||
@@ -537,8 +545,9 @@ static int path_pick_one(
|
||||
/* Explicit basename specified, then shortcut things and do .v mode regardless of the path name. */
|
||||
if (filter->basename)
|
||||
return make_choice(
|
||||
toplevel_path,
|
||||
toplevel_fd,
|
||||
root_path,
|
||||
root_fd,
|
||||
dir_fd,
|
||||
path,
|
||||
/* inode_fd= */ -EBADF,
|
||||
filter,
|
||||
@@ -623,8 +632,9 @@ static int path_pick_one(
|
||||
}
|
||||
|
||||
return make_choice(
|
||||
toplevel_path,
|
||||
toplevel_fd,
|
||||
root_path,
|
||||
root_fd,
|
||||
dir_fd,
|
||||
enumeration_path,
|
||||
/* inode_fd= */ -EBADF,
|
||||
&(const PickFilter) {
|
||||
@@ -640,8 +650,9 @@ static int path_pick_one(
|
||||
bypass:
|
||||
/* Don't make any choice, but just use the passed path literally */
|
||||
return pin_choice(
|
||||
toplevel_path,
|
||||
toplevel_fd,
|
||||
root_path,
|
||||
root_fd,
|
||||
dir_fd,
|
||||
path,
|
||||
/* inode_fd= */ -EBADF,
|
||||
/* tries_left= */ UINT_MAX,
|
||||
@@ -651,8 +662,9 @@ bypass:
|
||||
ret);
|
||||
}
|
||||
|
||||
int path_pick(const char *toplevel_path,
|
||||
int toplevel_fd,
|
||||
int path_pick(const char *root_path,
|
||||
int root_fd,
|
||||
int dir_fd,
|
||||
const char *path,
|
||||
const PickFilter filters[],
|
||||
size_t n_filters,
|
||||
@@ -662,7 +674,8 @@ int path_pick(const char *toplevel_path,
|
||||
_cleanup_(pick_result_done) PickResult best = PICK_RESULT_NULL;
|
||||
int r;
|
||||
|
||||
assert(wildcard_fd_is_valid(toplevel_fd));
|
||||
assert(wildcard_fd_is_valid(root_fd));
|
||||
assert(wildcard_fd_is_valid(dir_fd));
|
||||
assert(path);
|
||||
assert(filters || n_filters == 0);
|
||||
assert(ret);
|
||||
@@ -671,7 +684,7 @@ int path_pick(const char *toplevel_path,
|
||||
for (size_t i = 0; i < n_filters; i++) {
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
|
||||
r = path_pick_one(toplevel_path, toplevel_fd, path, &filters[i], flags, &result);
|
||||
r = path_pick_one(root_path, root_fd, dir_fd, path, &filters[i], flags, &result);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0)
|
||||
@@ -717,8 +730,9 @@ int path_pick_update_warn(
|
||||
|
||||
/* This updates the first argument if needed! */
|
||||
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
r = path_pick(/* root_path= */ NULL,
|
||||
/* root_fd= */ AT_FDCWD,
|
||||
/* dir_fd= */ AT_FDCWD,
|
||||
*path,
|
||||
filters,
|
||||
n_filters,
|
||||
|
||||
+3
-2
@@ -45,8 +45,9 @@ void pick_result_done(PickResult *p);
|
||||
|
||||
int pick_result_compare(const PickResult *a, const PickResult *b, PickFlags flags);
|
||||
|
||||
int path_pick(const char *toplevel_path,
|
||||
int toplevel_fd,
|
||||
int path_pick(const char *root_path,
|
||||
int root_fd,
|
||||
int dir_fd,
|
||||
const char *path,
|
||||
const PickFilter filters[],
|
||||
size_t n_filters,
|
||||
|
||||
+44
-3
@@ -68,6 +68,7 @@
|
||||
#include "strv.h"
|
||||
#include "time-util.h"
|
||||
#include "unit-name.h"
|
||||
#include "varlink-io.systemd.SysUpdate.Notify.h"
|
||||
#include "varlink-io.systemd.sysext.h"
|
||||
#include "varlink-util.h"
|
||||
#include "verbs.h"
|
||||
@@ -2898,6 +2899,42 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v
|
||||
return sd_varlink_reply(link, NULL);
|
||||
}
|
||||
|
||||
static int refresh_class(ImageClass image_class) {
|
||||
_cleanup_strv_free_ char **hierarchies = NULL;
|
||||
int r;
|
||||
|
||||
r = parse_env_extension_hierarchies(&hierarchies, image_class_info[image_class].name_env);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to determine %s hierarchies: %m", image_class_info[image_class].short_identifier);
|
||||
|
||||
return refresh(image_class, hierarchies, arg_force, arg_no_reload, arg_always_refresh, arg_noexec);
|
||||
}
|
||||
|
||||
static int vl_method_on_completed_update(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
|
||||
int r;
|
||||
|
||||
assert(link);
|
||||
|
||||
/* Only honour update notifications if they come from root */
|
||||
r = varlink_check_privileged_peer(link);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* Triggered by systemd-sysupdate after an update completed. We deliberately ignore all parameters
|
||||
* (we don't even dispatch them). This method is reached through the sysext socket, but a single
|
||||
* notification refreshes both image classes, so freshly downloaded sysexts and confexts are both
|
||||
* picked up, equivalent to "systemd-sysext refresh" plus "systemd-confext refresh". We attempt both
|
||||
* classes and only fail afterwards, so a problem with one does not prevent refreshing the other. */
|
||||
|
||||
r = 0;
|
||||
RET_GATHER(r, refresh_class(IMAGE_SYSEXT));
|
||||
RET_GATHER(r, refresh_class(IMAGE_CONFEXT));
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return sd_varlink_reply(link, NULL);
|
||||
}
|
||||
|
||||
VERB_NOARG(verb_list, "list", "List installed extensions");
|
||||
static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) {
|
||||
_cleanup_hashmap_free_ Hashmap *images = NULL;
|
||||
@@ -3184,16 +3221,20 @@ static int run(int argc, char *argv[]) {
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to allocate Varlink server: %m");
|
||||
|
||||
r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_sysext);
|
||||
r = sd_varlink_server_add_interface_many(
|
||||
varlink_server,
|
||||
&vl_interface_io_systemd_sysext,
|
||||
&vl_interface_io_systemd_SysUpdate_Notify);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to add Varlink interface: %m");
|
||||
return log_error_errno(r, "Failed to add Varlink interfaces: %m");
|
||||
|
||||
r = sd_varlink_server_bind_method_many(
|
||||
varlink_server,
|
||||
"io.systemd.sysext.Merge", vl_method_merge,
|
||||
"io.systemd.sysext.Unmerge", vl_method_unmerge,
|
||||
"io.systemd.sysext.Refresh", vl_method_refresh,
|
||||
"io.systemd.sysext.List", vl_method_list);
|
||||
"io.systemd.sysext.List", vl_method_list,
|
||||
"io.systemd.SysUpdate.Notify.OnCompletedUpdate", vl_method_on_completed_update);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to bind Varlink methods: %m");
|
||||
|
||||
|
||||
@@ -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(
|
||||
¶ms,
|
||||
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)
|
||||
|
||||
+24
-24
@@ -48,7 +48,7 @@ TEST(path_pick) {
|
||||
};
|
||||
|
||||
if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_TRUE(S_ISREG(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "99");
|
||||
ASSERT_EQ(result.architecture, ARCHITECTURE_X86);
|
||||
@@ -58,7 +58,7 @@ TEST(path_pick) {
|
||||
}
|
||||
|
||||
filter.architecture = ARCHITECTURE_X86_64;
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_TRUE(S_ISREG(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "55");
|
||||
ASSERT_EQ(result.architecture, ARCHITECTURE_X86_64);
|
||||
@@ -66,7 +66,7 @@ TEST(path_pick) {
|
||||
pick_result_done(&result);
|
||||
|
||||
filter.architecture = ARCHITECTURE_IA64;
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_TRUE(S_ISREG(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "5");
|
||||
ASSERT_EQ(result.architecture, ARCHITECTURE_IA64);
|
||||
@@ -75,7 +75,7 @@ TEST(path_pick) {
|
||||
|
||||
filter.architecture = _ARCHITECTURE_INVALID;
|
||||
filter.version = "5";
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_TRUE(S_ISREG(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "5");
|
||||
if (native_architecture() != ARCHITECTURE_IA64) {
|
||||
@@ -85,7 +85,7 @@ TEST(path_pick) {
|
||||
pick_result_done(&result);
|
||||
|
||||
filter.architecture = ARCHITECTURE_IA64;
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_TRUE(S_ISREG(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "5");
|
||||
ASSERT_EQ(result.architecture, ARCHITECTURE_IA64);
|
||||
@@ -93,7 +93,7 @@ TEST(path_pick) {
|
||||
pick_result_done(&result);
|
||||
|
||||
filter.architecture = ARCHITECTURE_CRIS;
|
||||
ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_EQ(result.st.st_mode, MODE_INVALID);
|
||||
ASSERT_NULL(result.version);
|
||||
ASSERT_LT(result.architecture, 0);
|
||||
@@ -104,7 +104,7 @@ TEST(path_pick) {
|
||||
filter.architecture = _ARCHITECTURE_INVALID;
|
||||
filter.version = NULL;
|
||||
if (IN_SET(native_architecture(), ARCHITECTURE_X86_64, ARCHITECTURE_X86)) {
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_TRUE(S_ISREG(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "55");
|
||||
|
||||
@@ -123,7 +123,7 @@ TEST(path_pick) {
|
||||
pp = ASSERT_NOT_NULL(path_join(p, "foo.v/foo___.raw"));
|
||||
|
||||
if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_TRUE(S_ISREG(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "55");
|
||||
ASSERT_EQ(result.architecture, native_architecture());
|
||||
@@ -137,10 +137,10 @@ TEST(path_pick) {
|
||||
pp = ASSERT_NOT_NULL(path_join(p, "foo.v/foo_5.raw"));
|
||||
|
||||
filter.type_mask = UINT32_C(1) << DT_DIR;
|
||||
ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
|
||||
filter.type_mask = UINT32_C(1) << DT_REG;
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_TRUE(S_ISREG(result.st.st_mode));
|
||||
ASSERT_NULL(result.version);
|
||||
ASSERT_EQ(result.architecture, _ARCHITECTURE_INVALID);
|
||||
@@ -153,7 +153,7 @@ TEST(path_pick) {
|
||||
filter.architecture = ARCHITECTURE_S390;
|
||||
filter.basename = "quux";
|
||||
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
|
||||
ASSERT_TRUE(S_ISREG(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "2");
|
||||
ASSERT_EQ(result.tries_left, 4U);
|
||||
@@ -209,7 +209,7 @@ TEST(pick_filter_image_any) {
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
|
||||
/* Test pick_filter_image_any: should pick the highest version, which is the directory test_5 */
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "5");
|
||||
ASSERT_TRUE(endswith(result.path, "/test_5"));
|
||||
@@ -219,14 +219,14 @@ TEST(pick_filter_image_any) {
|
||||
ASSERT_OK(unlinkat(sub_dfd, "test_4", AT_REMOVEDIR));
|
||||
ASSERT_OK(unlinkat(sub_dfd, "test_5", AT_REMOVEDIR));
|
||||
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_TRUE(S_ISREG(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "3");
|
||||
ASSERT_TRUE(endswith(result.path, "/test_3.raw"));
|
||||
pick_result_done(&result);
|
||||
|
||||
/* Verify that pick_filter_image_raw only matches .raw files */
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_TRUE(S_ISREG(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "3");
|
||||
ASSERT_TRUE(endswith(result.path, "/test_3.raw"));
|
||||
@@ -239,12 +239,12 @@ TEST(pick_filter_image_any) {
|
||||
ASSERT_OK(unlinkat(sub_dfd, "test_3.raw", 0));
|
||||
|
||||
/* Now only test_10.txt, test_11.img, and test_12 remain - none should match */
|
||||
ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
|
||||
|
||||
/* But if we add a directory, it should be picked */
|
||||
ASSERT_OK(mkdirat(sub_dfd, "test_6", 0755));
|
||||
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "6");
|
||||
ASSERT_TRUE(endswith(result.path, "/test_6"));
|
||||
@@ -263,7 +263,7 @@ TEST(pick_filter_image_any) {
|
||||
|
||||
pick_result_done(&result);
|
||||
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "2");
|
||||
ASSERT_TRUE(endswith(result.path, "/myimage_2"));
|
||||
@@ -273,7 +273,7 @@ TEST(pick_filter_image_any) {
|
||||
ASSERT_OK(unlinkat(sub_dfd, "myimage_1", AT_REMOVEDIR));
|
||||
ASSERT_OK(unlinkat(sub_dfd, "myimage_2", AT_REMOVEDIR));
|
||||
|
||||
ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
|
||||
}
|
||||
|
||||
TEST(path_pick_resolve) {
|
||||
@@ -294,26 +294,26 @@ TEST(path_pick_resolve) {
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
|
||||
/* Test without PICK_RESOLVE - should return the symlink path */
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_STREQ(result.version, "2");
|
||||
ASSERT_TRUE(endswith(result.path, "/resolve_2.raw"));
|
||||
pick_result_done(&result);
|
||||
|
||||
/* Test with PICK_RESOLVE - should return the resolved (target) path */
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
|
||||
ASSERT_STREQ(result.version, "2");
|
||||
ASSERT_TRUE(endswith(result.path, "/target_file.raw"));
|
||||
pick_result_done(&result);
|
||||
|
||||
/* Test pick_filter_image_dir without PICK_RESOLVE */
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "1");
|
||||
ASSERT_TRUE(endswith(result.path, "/resolve_1"));
|
||||
pick_result_done(&result);
|
||||
|
||||
/* Test pick_filter_image_dir with PICK_RESOLVE */
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
|
||||
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
|
||||
ASSERT_STREQ(result.version, "1");
|
||||
ASSERT_TRUE(endswith(result.path, "/target_dir"));
|
||||
@@ -323,12 +323,12 @@ TEST(path_pick_resolve) {
|
||||
ASSERT_OK(symlinkat("target_file.raw", dfd, "intermediate_link.raw"));
|
||||
ASSERT_OK(symlinkat("../intermediate_link.raw", sub_dfd, "resolve_3.raw"));
|
||||
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE, &result));
|
||||
ASSERT_STREQ(result.version, "3");
|
||||
ASSERT_TRUE(endswith(result.path, "/resolve_3.raw"));
|
||||
pick_result_done(&result);
|
||||
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
|
||||
ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
|
||||
ASSERT_STREQ(result.version, "3");
|
||||
/* The chain should be fully resolved to target_file.raw */
|
||||
ASSERT_TRUE(endswith(result.path, "/target_file.raw"));
|
||||
|
||||
@@ -237,8 +237,9 @@ static int run(int argc, char *argv[]) {
|
||||
return log_error_errno(r, "Failed to make path '%s' absolute: %m", *i);
|
||||
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
r = path_pick(/* root_path= */ NULL,
|
||||
/* root_fd= */ AT_FDCWD,
|
||||
/* dir_fd= */ AT_FDCWD,
|
||||
p,
|
||||
&(PickFilter) {
|
||||
.basename = arg_filter_basename,
|
||||
|
||||
@@ -49,6 +49,10 @@ systemctl daemon-reload
|
||||
at_exit() {
|
||||
set +e
|
||||
|
||||
systemctl stop test-sysupdate-notify-recorder.socket
|
||||
rm -f /run/systemd/system/test-sysupdate-notify-recorder.socket \
|
||||
/run/systemd/system/test-sysupdate-notify-recorder@.service
|
||||
|
||||
losetup -n --output NAME --associated "$BACKING_FILE" | while read -r loop_dev; do
|
||||
losetup --detach "$loop_dev"
|
||||
done
|
||||
@@ -947,4 +951,85 @@ test ! -f "$CLEANUP/target/alpha-v2.bin"
|
||||
|
||||
rm -rf "$CONFIGDIR" "$INSTALLDB" "$CLEANUP"
|
||||
|
||||
# Verify the notification callout: after a successful update, sysupdate must connect to every socket in
|
||||
# /run/systemd/sysupdate/notify/ and invoke io.systemd.SysUpdate.Notify.OnCompletedUpdate(). We hook a tiny
|
||||
# recorder socket into that directory that captures the request and replies with success.
|
||||
NOTIFY_LOG="$WORKDIR/notify.log"
|
||||
rm -f "$NOTIFY_LOG"
|
||||
|
||||
cat >"$WORKDIR/notify-recorder.py" <<EOF
|
||||
#!/usr/bin/env python3
|
||||
# Minimal Varlink server: read one NUL-terminated request, record it, reply with empty parameters.
|
||||
import sys
|
||||
buf = b""
|
||||
while True:
|
||||
c = sys.stdin.buffer.read(1)
|
||||
if not c or c == b"\x00":
|
||||
break
|
||||
buf += c
|
||||
with open("$NOTIFY_LOG", "ab") as f:
|
||||
f.write(buf + b"\n")
|
||||
sys.stdout.buffer.write(b'{"parameters":{}}\x00')
|
||||
sys.stdout.buffer.flush()
|
||||
EOF
|
||||
chmod +x "$WORKDIR/notify-recorder.py"
|
||||
|
||||
cat >/run/systemd/system/test-sysupdate-notify-recorder.socket <<EOF
|
||||
[Socket]
|
||||
ListenStream=/run/systemd/sysupdate/notify/io.test.SysUpdateRecorder
|
||||
Accept=yes
|
||||
EOF
|
||||
|
||||
cat >"/run/systemd/system/test-sysupdate-notify-recorder@.service" <<EOF
|
||||
[Service]
|
||||
ExecStart=$WORKDIR/notify-recorder.py
|
||||
StandardInput=socket
|
||||
StandardOutput=socket
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl start test-sysupdate-notify-recorder.socket
|
||||
|
||||
rm -rf "$CONFIGDIR" "$WORKDIR/blobs"
|
||||
mkdir -p "$CONFIGDIR" "$WORKDIR/blobs"
|
||||
echo "hello" >"$WORKDIR/source/notifytest-v1.bin"
|
||||
(cd "$WORKDIR/source" && sha256sum notifytest-v1.bin >SHA256SUMS)
|
||||
cat >"$CONFIGDIR/01-notifytest.transfer" <<EOF
|
||||
[Source]
|
||||
Type=url-file
|
||||
Path=file://$WORKDIR/source
|
||||
MatchPattern=notifytest-@v.bin
|
||||
|
||||
[Target]
|
||||
Type=regular-file
|
||||
Path=$WORKDIR/blobs
|
||||
MatchPattern=notifytest-@v.bin
|
||||
InstancesMax=1
|
||||
EOF
|
||||
|
||||
# A real update must trigger exactly one notification carrying the version and the updated resources.
|
||||
# The callout is synchronous (sysupdate blocks until the subscriber replied, which happens after the
|
||||
# request was recorded), so the log is fully written by the time the update returns.
|
||||
"$SYSUPDATE" --verify=no update
|
||||
test -s "$NOTIFY_LOG" # the notification must have been recorded
|
||||
notify_line="$(tail -n1 "$NOTIFY_LOG")"
|
||||
echo "Recorded notification: $notify_line"
|
||||
jq -e '.method == "io.systemd.SysUpdate.Notify.OnCompletedUpdate"' <<<"$notify_line" >/dev/null
|
||||
jq -e '.parameters.version == "v1"' <<<"$notify_line" >/dev/null
|
||||
jq -e '.parameters.resources | length >= 1' <<<"$notify_line" >/dev/null
|
||||
jq -e '.parameters.resources | all(has("transfer"))' <<<"$notify_line" >/dev/null
|
||||
|
||||
# A no-op update ("No update needed") must NOT emit a notification.
|
||||
rm -f "$NOTIFY_LOG"
|
||||
"$SYSUPDATE" --verify=no update
|
||||
test ! -s "$NOTIFY_LOG"
|
||||
|
||||
systemctl stop test-sysupdate-notify-recorder.socket
|
||||
rm -f /run/systemd/system/test-sysupdate-notify-recorder.socket \
|
||||
/run/systemd/system/test-sysupdate-notify-recorder@.service
|
||||
systemctl daemon-reload
|
||||
rm -rf "$CONFIGDIR" "$WORKDIR/blobs"
|
||||
rm -f "$WORKDIR/source/notifytest-v1.bin" "$WORKDIR/source/SHA256SUMS" \
|
||||
"$WORKDIR/notify-recorder.py" "$NOTIFY_LOG"
|
||||
|
||||
touch /testok
|
||||
|
||||
@@ -710,4 +710,162 @@ EOF
|
||||
'{"id":"foo*.conf"}')
|
||||
}
|
||||
|
||||
cleanup_link_auto() {
|
||||
rm -rf /run/systemd/uki /etc/systemd/uki
|
||||
if [[ -n "${LINK_WORKDIR:-}" ]]; then
|
||||
rm -rf "$LINK_WORKDIR"
|
||||
unset LINK_WORKDIR
|
||||
fi
|
||||
restore_esp
|
||||
}
|
||||
|
||||
testcase_bootctl_link_auto() {
|
||||
if ! command -v ukify >/dev/null; then
|
||||
echo "ukify not found, skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
backup_esp
|
||||
LINK_WORKDIR="$(mktemp --directory /tmp/test-bootctl-link-auto.XXXXXXXXXX)"
|
||||
trap cleanup_link_auto RETURN ERR
|
||||
|
||||
# Ensure loader/entries directory is present
|
||||
bootctl install --make-entry-directory=yes
|
||||
|
||||
local ESP
|
||||
ESP="$(bootctl --print-esp-path)"
|
||||
|
||||
cat >"$LINK_WORKDIR/os-release" <<'EOF'
|
||||
ID=testos
|
||||
NAME="Test OS"
|
||||
PRETTY_NAME="Test OS"
|
||||
EOF
|
||||
echo "fake-kernel" >"$LINK_WORKDIR/vmlinuz"
|
||||
echo "fake-initrd" >"$LINK_WORKDIR/initrd"
|
||||
|
||||
# Two distinct UKIs, so we can tell which one was picked up.
|
||||
ukify build \
|
||||
--linux "$LINK_WORKDIR/vmlinuz" \
|
||||
--initrd "$LINK_WORKDIR/initrd" \
|
||||
--os-release "@$LINK_WORKDIR/os-release" \
|
||||
--uname "1.2.3-testkernel" \
|
||||
--cmdline "quiet uki=a" \
|
||||
--output "$LINK_WORKDIR/uki_a.efi"
|
||||
ukify build \
|
||||
--linux "$LINK_WORKDIR/vmlinuz" \
|
||||
--initrd "$LINK_WORKDIR/initrd" \
|
||||
--os-release "@$LINK_WORKDIR/os-release" \
|
||||
--uname "1.2.3-testkernel" \
|
||||
--cmdline "quiet uki=b" \
|
||||
--output "$LINK_WORKDIR/uki_b.efi"
|
||||
|
||||
local TOKEN="systemdtest"
|
||||
local BOOTCTL=(bootctl "--entry-token=literal:$TOKEN")
|
||||
local ENTRY="$ESP/loader/entries/${TOKEN}-commit_1.conf"
|
||||
|
||||
# --- Test 1: link-auto picks up kernel.efi + extras.d/ from /run/systemd/uki/ ---
|
||||
rm -rf /run/systemd/uki
|
||||
mkdir -p /run/systemd/uki/extras.d
|
||||
cp "$LINK_WORKDIR/uki_a.efi" /run/systemd/uki/kernel.efi
|
||||
echo "sysext-data" >/run/systemd/uki/extras.d/hello.sysext.raw
|
||||
echo "confext-data" >/run/systemd/uki/extras.d/hello.confext.raw
|
||||
echo "cred-data" >/run/systemd/uki/extras.d/hello.cred
|
||||
|
||||
"${BOOTCTL[@]}" link-auto
|
||||
|
||||
test -f "$ENTRY"
|
||||
test -f "$ESP/$TOKEN/kernel.efi"
|
||||
cmp "$LINK_WORKDIR/uki_a.efi" "$ESP/$TOKEN/kernel.efi"
|
||||
test -f "$ESP/$TOKEN/hello.sysext.raw"
|
||||
test -f "$ESP/$TOKEN/hello.confext.raw"
|
||||
test -f "$ESP/$TOKEN/hello.cred"
|
||||
grep "^uki /${TOKEN}/kernel.efi\$" "$ENTRY" >/dev/null
|
||||
grep "^extra /${TOKEN}/hello.sysext.raw\$" "$ENTRY" >/dev/null
|
||||
grep "^extra /${TOKEN}/hello.confext.raw\$" "$ENTRY" >/dev/null
|
||||
grep "^extra /${TOKEN}/hello.cred\$" "$ENTRY" >/dev/null
|
||||
|
||||
"${BOOTCTL[@]}" unlink "${TOKEN}-commit_1.conf"
|
||||
test ! -e "$ENTRY"
|
||||
test ! -e "$ESP/$TOKEN/kernel.efi"
|
||||
|
||||
# --- Test 2: versioned kernel.efi.v/ and extras .v/ are resolved via vpick ---
|
||||
rm -rf /run/systemd/uki
|
||||
mkdir -p /run/systemd/uki/kernel.efi.v /run/systemd/uki/extras.d/hello.sysext.raw.v
|
||||
cp "$LINK_WORKDIR/uki_a.efi" /run/systemd/uki/kernel.efi.v/kernel_1.0.efi
|
||||
cp "$LINK_WORKDIR/uki_a.efi" /run/systemd/uki/kernel.efi.v/kernel_2.0.efi
|
||||
echo "sysext-1" >/run/systemd/uki/extras.d/hello.sysext.raw.v/hello_1.0.sysext.raw
|
||||
echo "sysext-2" >/run/systemd/uki/extras.d/hello.sysext.raw.v/hello_2.0.sysext.raw
|
||||
|
||||
"${BOOTCTL[@]}" link-auto
|
||||
|
||||
test -f "$ENTRY"
|
||||
# vpick must select the newest version
|
||||
test -f "$ESP/$TOKEN/kernel_2.0.efi"
|
||||
test ! -e "$ESP/$TOKEN/kernel_1.0.efi"
|
||||
test -f "$ESP/$TOKEN/hello_2.0.sysext.raw"
|
||||
test ! -e "$ESP/$TOKEN/hello_1.0.sysext.raw"
|
||||
grep "^uki /${TOKEN}/kernel_2.0.efi\$" "$ENTRY" >/dev/null
|
||||
grep "^extra /${TOKEN}/hello_2.0.sysext.raw\$" "$ENTRY" >/dev/null
|
||||
|
||||
"${BOOTCTL[@]}" unlink "${TOKEN}-commit_1.conf"
|
||||
|
||||
# --- Test 3: priority — /etc/systemd/uki/ wins over /run/systemd/uki/ ---
|
||||
rm -rf /run/systemd/uki /etc/systemd/uki
|
||||
mkdir -p /run/systemd/uki/extras.d /etc/systemd/uki/extras.d
|
||||
cp "$LINK_WORKDIR/uki_b.efi" /run/systemd/uki/kernel.efi
|
||||
cp "$LINK_WORKDIR/uki_a.efi" /etc/systemd/uki/kernel.efi
|
||||
echo "run-cred" >/run/systemd/uki/extras.d/hello.cred
|
||||
echo "etc-cred" >/etc/systemd/uki/extras.d/hello.cred
|
||||
|
||||
"${BOOTCTL[@]}" link-auto
|
||||
|
||||
test -f "$ESP/$TOKEN/kernel.efi"
|
||||
# The /etc copy (uki_a) must win over the /run copy (uki_b)
|
||||
cmp "$LINK_WORKDIR/uki_a.efi" "$ESP/$TOKEN/kernel.efi"
|
||||
cmp <(echo "etc-cred") "$ESP/$TOKEN/hello.cred"
|
||||
|
||||
"${BOOTCTL[@]}" unlink "${TOKEN}-commit_1.conf"
|
||||
rm -rf /etc/systemd/uki
|
||||
|
||||
# --- Test 4: with nothing staged, link-auto is a successful no-op ---
|
||||
rm -rf /run/systemd/uki
|
||||
"${BOOTCTL[@]}" link-auto
|
||||
# No entries referencing our token should remain.
|
||||
local leftover
|
||||
leftover="$(find "$ESP/loader/entries/" -name "*$TOKEN*" -print -quit 2>/dev/null)"
|
||||
test -z "$leftover"
|
||||
|
||||
# === Varlink coverage: io.systemd.BootControl.LinkAuto ===
|
||||
local BOOTCTL_BIN vreply vid vtoken
|
||||
BOOTCTL_BIN="$(type -p bootctl)"
|
||||
|
||||
# --- Test 5: LinkAuto discovers kernel.efi + extras ---
|
||||
rm -rf /run/systemd/uki
|
||||
mkdir -p /run/systemd/uki/extras.d
|
||||
cp "$LINK_WORKDIR/uki_a.efi" /run/systemd/uki/kernel.efi
|
||||
echo "cred-data" >/run/systemd/uki/extras.d/hello.cred
|
||||
|
||||
vreply="$(varlinkctl call --json=short "$BOOTCTL_BIN" io.systemd.BootControl.LinkAuto '{}')"
|
||||
vid="$(echo "$vreply" | jq -r '.ids[0]')"
|
||||
test -n "$vid"
|
||||
test "$vid" != "null"
|
||||
vtoken="${vid%%-commit_*}"
|
||||
test -n "$vtoken"
|
||||
|
||||
test -f "$ESP/loader/entries/$vid"
|
||||
test -f "$ESP/$vtoken/kernel.efi"
|
||||
test -f "$ESP/$vtoken/hello.cred"
|
||||
grep "^uki /$vtoken/kernel.efi\$" "$ESP/loader/entries/$vid" >/dev/null
|
||||
|
||||
varlinkctl call --quiet "$BOOTCTL_BIN" io.systemd.BootControl.Unlink "{\"id\":\"$vid\"}"
|
||||
test ! -e "$ESP/loader/entries/$vid"
|
||||
test ! -e "$ESP/$vtoken/kernel.efi"
|
||||
test ! -e "$ESP/$vtoken/hello.cred"
|
||||
|
||||
# --- Test 6: LinkAuto with nothing staged returns an empty id list ---
|
||||
rm -rf /run/systemd/uki
|
||||
vreply="$(varlinkctl call --json=short "$BOOTCTL_BIN" io.systemd.BootControl.LinkAuto '{}')"
|
||||
assert_eq "$(echo "$vreply" | jq -r '.ids | length')" "0"
|
||||
}
|
||||
|
||||
run_testcases
|
||||
|
||||
@@ -303,6 +303,14 @@ units = [
|
||||
'conditions' : ['ENABLE_BOOTLOADER'],
|
||||
'symlinks' : ['sockets.target.wants/'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-sysupdate-notify-bootctl@.service',
|
||||
'conditions' : ['ENABLE_BOOTLOADER'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-sysupdate-notify-bootctl.socket',
|
||||
'conditions' : ['ENABLE_BOOTLOADER'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-confext.service',
|
||||
'conditions' : ['ENABLE_SYSEXT'],
|
||||
@@ -706,6 +714,14 @@ units = [
|
||||
'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'],
|
||||
'symlinks' : ['sockets.target.wants/'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-sysupdate-notify-pcrlock@.service.in',
|
||||
'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-sysupdate-notify-pcrlock.socket',
|
||||
'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-portabled.service.in',
|
||||
'conditions' : ['ENABLE_PORTABLED'],
|
||||
@@ -807,6 +823,14 @@ units = [
|
||||
'conditions' : ['ENABLE_SYSEXT'],
|
||||
'symlinks' : ['sockets.target.wants/'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-sysupdate-notify-sysext@.service',
|
||||
'conditions' : ['ENABLE_SYSEXT'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-sysupdate-notify-sysext.socket',
|
||||
'conditions' : ['ENABLE_SYSEXT'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-sysext@.service',
|
||||
'conditions' : ['ENABLE_SYSEXT'],
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
[Unit]
|
||||
Description=Automatic Boot Loader Update
|
||||
Documentation=man:bootctl(1)
|
||||
ConditionPathExists=!/etc/initrd-release
|
||||
|
||||
DefaultDependencies=no
|
||||
ConditionPathExists=!/etc/initrd-release
|
||||
ConditionFirmware=uefi
|
||||
After=local-fs.target
|
||||
Before=sysinit.target systemd-update-done.service
|
||||
Conflicts=shutdown.target
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
[Unit]
|
||||
Description=Boot Loader UKI Auto-Link on Completed System Update
|
||||
Documentation=man:bootctl(1)
|
||||
DefaultDependencies=no
|
||||
ConditionFirmware=uefi
|
||||
After=local-fs.target
|
||||
Before=sockets.target
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/systemd/sysupdate/notify/io.systemd.BootControl
|
||||
FileDescriptorName=varlink
|
||||
SocketMode=0644
|
||||
Accept=yes
|
||||
MaxConnectionsPerSource=16
|
||||
XAttrEntryPoint=user.varlink=entrypoint
|
||||
XAttrListen=user.varlink=listen
|
||||
XAttrAccept=user.varlink=server
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -0,0 +1,19 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
[Unit]
|
||||
Description=Boot Loader UKI Auto-Link on Completed System Update
|
||||
Documentation=man:bootctl(1)
|
||||
DefaultDependencies=no
|
||||
Conflicts=shutdown.target
|
||||
After=local-fs.target
|
||||
Before=shutdown.target
|
||||
|
||||
[Service]
|
||||
ExecStart=bootctl
|
||||
@@ -0,0 +1,29 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
[Unit]
|
||||
Description=Recompute TPM PCR Policy on Completed System Update
|
||||
Documentation=man:systemd-pcrlock(8)
|
||||
DefaultDependencies=no
|
||||
After=tpm2.target
|
||||
Before=sockets.target
|
||||
ConditionSecurity=measured-uki
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/systemd/sysupdate/notify/io.systemd.PCRLock
|
||||
FileDescriptorName=varlink
|
||||
SocketMode=0644
|
||||
Accept=yes
|
||||
MaxConnectionsPerSource=16
|
||||
XAttrEntryPoint=user.varlink=entrypoint
|
||||
XAttrListen=user.varlink=listen
|
||||
XAttrAccept=user.varlink=server
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -0,0 +1,20 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
[Unit]
|
||||
Description=Recompute TPM PCR Policy on Completed System Update
|
||||
Documentation=man:systemd-pcrlock(8)
|
||||
DefaultDependencies=no
|
||||
Conflicts=shutdown.target
|
||||
After=systemd-tpm2-setup.service
|
||||
After=systemd-remount-fs.service var.mount
|
||||
Before=shutdown.target
|
||||
|
||||
[Service]
|
||||
ExecStart={{LIBEXECDIR}}/systemd-pcrlock --location=770
|
||||
@@ -0,0 +1,29 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
[Unit]
|
||||
Description=System Extension and Configuration Refresh on Completed System Update
|
||||
Documentation=man:systemd-sysext(8)
|
||||
DefaultDependencies=no
|
||||
After=local-fs.target
|
||||
Before=sockets.target
|
||||
ConditionCapability=CAP_SYS_ADMIN
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/systemd/sysupdate/notify/io.systemd.sysext
|
||||
FileDescriptorName=varlink
|
||||
SocketMode=0644
|
||||
Accept=yes
|
||||
MaxConnectionsPerSource=16
|
||||
XAttrEntryPoint=user.varlink=entrypoint
|
||||
XAttrListen=user.varlink=listen
|
||||
XAttrAccept=user.varlink=server
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -0,0 +1,19 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
[Unit]
|
||||
Description=System Extension and Configuration Refresh on Completed System Update
|
||||
Documentation=man:systemd-sysext(8)
|
||||
DefaultDependencies=no
|
||||
Conflicts=shutdown.target initrd-switch-root.target
|
||||
After=local-fs.target
|
||||
Before=shutdown.target initrd-switch-root.target
|
||||
|
||||
[Service]
|
||||
ExecStart=-systemd-sysext
|
||||
Reference in New Issue
Block a user