From 79ec39958d70e3eeb141f7ca1f57ea52533727b6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Feb 2024 17:30:31 +0100 Subject: [PATCH] bootctl: add a Varlink interface For now, just super basic functionality: return the list of boot menu entries, and read/write the reboot to firmware flag --- src/boot/bootctl-reboot-to-firmware.c | 39 +++++++++++- src/boot/bootctl-reboot-to-firmware.h | 5 ++ src/boot/bootctl-status.c | 68 ++++++++++++++++++++- src/boot/bootctl-status.h | 4 ++ src/boot/bootctl.c | 39 ++++++++++++ src/boot/bootctl.h | 1 + src/shared/meson.build | 1 + src/shared/varlink-io.systemd.BootControl.c | 59 ++++++++++++++++++ src/shared/varlink-io.systemd.BootControl.h | 6 ++ src/test/test-varlink-idl.c | 3 + test/units/testsuite-74.bootctl.sh | 9 +++ units/meson.build | 9 +++ units/systemd-bootctl.socket | 21 +++++++ units/systemd-bootctl@.service.in | 20 ++++++ 14 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 src/shared/varlink-io.systemd.BootControl.c create mode 100644 src/shared/varlink-io.systemd.BootControl.h create mode 100644 units/systemd-bootctl.socket create mode 100644 units/systemd-bootctl@.service.in diff --git a/src/boot/bootctl-reboot-to-firmware.c b/src/boot/bootctl-reboot-to-firmware.c index 91f259768c0..cdb04f80450 100644 --- a/src/boot/bootctl-reboot-to-firmware.c +++ b/src/boot/bootctl-reboot-to-firmware.c @@ -2,6 +2,7 @@ #include "bootctl-reboot-to-firmware.h" #include "efi-api.h" +#include "errno-util.h" #include "parse-util.h" int verb_reboot_to_firmware(int argc, char *argv[], void *userdata) { @@ -17,7 +18,7 @@ int verb_reboot_to_firmware(int argc, char *argv[], void *userdata) { puts("supported"); return 1; /* recognizable error #1 */ } - if (r == -EOPNOTSUPP) { + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) { puts("not supported"); return 2; /* recognizable error #2 */ } @@ -36,3 +37,39 @@ int verb_reboot_to_firmware(int argc, char *argv[], void *userdata) { return 0; } } + +int vl_method_set_reboot_to_firmware(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + static const JsonDispatch dispatch_table[] = { + { "state", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, 0, 0 }, + {} + }; + bool b; + int r; + + r = varlink_dispatch(link, parameters, dispatch_table, &b); + if (r != 0) + return r; + + r = efi_set_reboot_to_firmware(b); + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) + return varlink_error(link, "io.systemd.BootControl.RebootToFirmwareNotSupported", NULL); + if (r < 0) + return r; + + return varlink_reply(link, NULL); +} + +int vl_method_get_reboot_to_firmware(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + int r; + + if (json_variant_elements(parameters) > 0) + return varlink_error_invalid_parameter(link, parameters); + + r = efi_get_reboot_to_firmware(); + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) + return varlink_error(link, "io.systemd.BootControl.RebootToFirmwareNotSupported", NULL); + if (r < 0) + return r; + + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_BOOLEAN("state", r))); +} diff --git a/src/boot/bootctl-reboot-to-firmware.h b/src/boot/bootctl-reboot-to-firmware.h index 0ca4b2c3a30..fb8a2485b33 100644 --- a/src/boot/bootctl-reboot-to-firmware.h +++ b/src/boot/bootctl-reboot-to-firmware.h @@ -1,3 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "varlink.h" + int verb_reboot_to_firmware(int argc, char *argv[], void *userdata); + +int vl_method_set_reboot_to_firmware(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata); +int vl_method_get_reboot_to_firmware(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata); diff --git a/src/boot/bootctl-status.c b/src/boot/bootctl-status.c index f8b57c1f9d4..58b6276dd91 100644 --- a/src/boot/bootctl-status.c +++ b/src/boot/bootctl-status.c @@ -318,7 +318,13 @@ int verb_status(int argc, char *argv[], void *userdata) { dev_t esp_devid = 0, xbootldr_devid = 0; int r, k; - r = acquire_esp(/* unprivileged_mode= */ -1, /* graceful= */ false, NULL, NULL, NULL, &esp_uuid, &esp_devid); + r = acquire_esp(/* unprivileged_mode= */ -1, + /* graceful= */ false, + /* ret_part= */ NULL, + /* ret_pstart= */ NULL, + /* ret_psize= */ NULL, + &esp_uuid, + &esp_devid); if (arg_print_esp_path) { if (r == -EACCES) /* If we couldn't acquire the ESP path, log about access errors (which is the only * error the find_esp_and_warn() won't log on its own) */ @@ -330,7 +336,10 @@ int verb_status(int argc, char *argv[], void *userdata) { return 0; } - r = acquire_xbootldr(/* unprivileged_mode= */ -1, &xbootldr_uuid, &xbootldr_devid); + r = acquire_xbootldr( + /* unprivileged_mode= */ -1, + &xbootldr_uuid, + &xbootldr_devid); if (arg_print_dollar_boot_path) { if (r == -EACCES) return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m"); @@ -825,3 +834,58 @@ int verb_list(int argc, char *argv[], void *userdata) { int verb_unlink(int argc, char *argv[], void *userdata) { return verb_list(argc, argv, userdata); } + +int vl_method_list_boot_entries(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL; + dev_t esp_devid = 0, xbootldr_devid = 0; + int r; + + assert(link); + + if (json_variant_elements(parameters) > 0) + return varlink_error_invalid_parameter(link, parameters); + + r = acquire_esp(/* unprivileged_mode= */ false, + /* graceful= */ false, + /* ret_part= */ NULL, + /* ret_pstart= */ NULL, + /* ret_psize= */ NULL, + /* ret_uuid=*/ NULL, + &esp_devid); + if (r == -EACCES) /* We really need the ESP path for this call, hence also log about access errors */ + return log_error_errno(r, "Failed to determine ESP location: %m"); + if (r < 0) + return r; + + r = acquire_xbootldr( + /* unprivileged_mode= */ false, + /* ret_uuid= */ NULL, + &xbootldr_devid); + if (r == -EACCES) + return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m"); + if (r < 0) + return r; + + r = boot_config_load_and_select(&config, arg_esp_path, esp_devid, arg_xbootldr_path, xbootldr_devid); + if (r < 0) + return r; + + _cleanup_(json_variant_unrefp) JsonVariant *previous = NULL; + for (size_t i = 0; i < config.n_entries; i++) { + if (previous) { + r = varlink_notifyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_VARIANT("entry", previous))); + if (r < 0) + return r; + + previous = json_variant_unref(previous); + } + + r = boot_entry_to_json(&config, i, &previous); + if (r < 0) + return r; + } + + return varlink_replyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_CONDITION(previous, "entry", JSON_BUILD_VARIANT(previous)))); +} diff --git a/src/boot/bootctl-status.h b/src/boot/bootctl-status.h index f7998a3303e..6fd436513ba 100644 --- a/src/boot/bootctl-status.h +++ b/src/boot/bootctl-status.h @@ -1,5 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "varlink.h" + int verb_status(int argc, char *argv[], void *userdata); int verb_list(int argc, char *argv[], void *userdata); int verb_unlink(int argc, char *argv[], void *userdata); + +int vl_method_list_boot_entries(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata); diff --git a/src/boot/bootctl.c b/src/boot/bootctl.c index f608e8cc8e0..bd10c08b82e 100644 --- a/src/boot/bootctl.c +++ b/src/boot/bootctl.c @@ -22,6 +22,8 @@ #include "parse-argument.h" #include "pretty-print.h" #include "utf8.h" +#include "varlink.h" +#include "varlink-io.systemd.BootControl.h" #include "verbs.h" #include "virt.h" @@ -53,6 +55,7 @@ InstallSource arg_install_source = ARG_INSTALL_SOURCE_AUTO; char *arg_efi_boot_option_description = NULL; bool arg_dry_run = false; ImagePolicy *arg_image_policy = NULL; +bool arg_varlink = false; STATIC_DESTRUCTOR_REGISTER(arg_esp_path, freep); STATIC_DESTRUCTOR_REGISTER(arg_xbootldr_path, freep); @@ -418,6 +421,14 @@ static int parse_argv(int argc, char *argv[]) { if (arg_dry_run && argv[optind] && !STR_IN_SET(argv[optind], "unlink", "cleanup")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--dry is only supported with --unlink or --cleanup"); + r = varlink_invocation(VARLINK_ALLOW_ACCEPT); + if (r < 0) + return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); + if (r > 0) { + arg_varlink = true; + arg_pager_flags |= PAGER_DISABLE; + } + return 1; } @@ -462,6 +473,34 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; + if (arg_varlink) { + _cleanup_(varlink_server_unrefp) VarlinkServer *varlink_server = NULL; + + /* Invocation as Varlink service */ + + r = varlink_server_new(&varlink_server, VARLINK_SERVER_ROOT_ONLY); + if (r < 0) + return log_error_errno(r, "Failed to allocate Varlink server: %m"); + + r = varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_BootControl); + if (r < 0) + return log_error_errno(r, "Failed to add Varlink interface: %m"); + + r = 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); + if (r < 0) + return log_error_errno(r, "Failed to bind Varlink methods: %m"); + + r = varlink_server_loop_auto(varlink_server); + if (r < 0) + return log_error_errno(r, "Failed to run Varlink event loop: %m"); + + return EXIT_SUCCESS; + } + if (arg_print_root_device > 0) { _cleanup_free_ char *path = NULL; dev_t devno; diff --git a/src/boot/bootctl.h b/src/boot/bootctl.h index e395b3324ad..25cb5166ce7 100644 --- a/src/boot/bootctl.h +++ b/src/boot/bootctl.h @@ -36,6 +36,7 @@ extern InstallSource arg_install_source; extern char *arg_efi_boot_option_description; extern bool arg_dry_run; extern ImagePolicy *arg_image_policy; +extern bool arg_varlink; static inline const char *arg_dollar_boot_path(void) { /* $BOOT shall be the XBOOTLDR partition if it exists, and otherwise the ESP */ diff --git a/src/shared/meson.build b/src/shared/meson.build index 81de6708f01..fe0c9c1f2fa 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -174,6 +174,7 @@ shared_sources = files( 'varlink.c', 'varlink-idl.c', 'varlink-io.systemd.c', + 'varlink-io.systemd.BootControl.c', 'varlink-io.systemd.Credentials.c', 'varlink-io.systemd.Hostname.c', 'varlink-io.systemd.Journal.c', diff --git a/src/shared/varlink-io.systemd.BootControl.c b/src/shared/varlink-io.systemd.BootControl.c new file mode 100644 index 00000000000..500e07243c7 --- /dev/null +++ b/src/shared/varlink-io.systemd.BootControl.c @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.BootControl.h" + +static VARLINK_DEFINE_ENUM_TYPE( + BootEntryType, + VARLINK_DEFINE_ENUM_VALUE(type1), + VARLINK_DEFINE_ENUM_VALUE(type2), + VARLINK_DEFINE_ENUM_VALUE(loader), + VARLINK_DEFINE_ENUM_VALUE(auto)); + +static VARLINK_DEFINE_STRUCT_TYPE( + BootEntry, + VARLINK_DEFINE_FIELD_BY_TYPE(type, BootEntryType, 0), + VARLINK_DEFINE_FIELD(id, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(path, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(root, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(title, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(showTitle, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(sortKey, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(version, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(machineId, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(architecture, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(options, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(linux, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(efi, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(initrd, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(devicetree, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(devicetreeOverlay, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(isReported, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(triesLeft, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(triesDone, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(isDefault, VARLINK_BOOL, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(isSelected, VARLINK_BOOL, VARLINK_NULLABLE)); + +static VARLINK_DEFINE_METHOD( + ListBootEntries, + VARLINK_DEFINE_OUTPUT_BY_TYPE(entry, BootEntry, VARLINK_NULLABLE)); + +static VARLINK_DEFINE_METHOD( + SetRebootToFirmware, + VARLINK_DEFINE_INPUT(state, VARLINK_BOOL, 0)); + +static VARLINK_DEFINE_METHOD( + GetRebootToFirmware, + VARLINK_DEFINE_OUTPUT(state, VARLINK_BOOL, 0)); + +static VARLINK_DEFINE_ERROR( + RebootToFirmwareNotSupported); + +VARLINK_DEFINE_INTERFACE( + io_systemd_BootControl, + "io.systemd.BootControl", + &vl_type_BootEntryType, + &vl_type_BootEntry, + &vl_method_ListBootEntries, + &vl_method_SetRebootToFirmware, + &vl_method_GetRebootToFirmware, + &vl_error_RebootToFirmwareNotSupported); diff --git a/src/shared/varlink-io.systemd.BootControl.h b/src/shared/varlink-io.systemd.BootControl.h new file mode 100644 index 00000000000..fa72b703d18 --- /dev/null +++ b/src/shared/varlink-io.systemd.BootControl.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_BootControl; diff --git a/src/test/test-varlink-idl.c b/src/test/test-varlink-idl.c index e5708b73b54..d80fd705293 100644 --- a/src/test/test-varlink-idl.c +++ b/src/test/test-varlink-idl.c @@ -8,6 +8,7 @@ #include "varlink.h" #include "varlink-idl.h" #include "varlink-io.systemd.h" +#include "varlink-io.systemd.BootControl.h" #include "varlink-io.systemd.Credentials.h" #include "varlink-io.systemd.Journal.h" #include "varlink-io.systemd.ManagedOOM.h" @@ -152,6 +153,8 @@ TEST(parse_format) { print_separator(); test_parse_format_one(&vl_interface_io_systemd_Credentials); print_separator(); + test_parse_format_one(&vl_interface_io_systemd_BootControl); + print_separator(); test_parse_format_one(&vl_interface_xyz_test); } diff --git a/test/units/testsuite-74.bootctl.sh b/test/units/testsuite-74.bootctl.sh index 61373b506e4..0c52b19203e 100755 --- a/test/units/testsuite-74.bootctl.sh +++ b/test/units/testsuite-74.bootctl.sh @@ -258,4 +258,13 @@ EOF SYSTEMD_RELAX_ESP_CHECKS=yes SYSTEMD_RELAX_XBOOTLDR_CHECKS=yes basic_tests --root "${IMAGE_DIR}/root" } +testcase_bootctl_varlink() { + varlinkctl call --collect /run/systemd/io.systemd.BootControl io.systemd.BootControl.ListBootEntries '{}' + + # We have no UEFI in the test environment, hence just check that this fails cleanly + ( SYSTEMD_LOG_TARGET=console varlinkctl call --json=short /run/systemd/io.systemd.BootControl io.systemd.BootControl.GetRebootToFirmware '{}' 2>&1 || true ) | grep -q io.systemd.BootControl.RebootToFirmwareNotSupported + ( SYSTEMD_LOG_TARGET=console varlinkctl call --json=short /run/systemd/io.systemd.BootControl io.systemd.BootControl.SetRebootToFirmware '{"state":true}' 2>&1 || true ) | grep -q io.systemd.BootControl.RebootToFirmwareNotSupported + ( SYSTEMD_LOG_TARGET=console varlinkctl call --json=short /run/systemd/io.systemd.BootControl io.systemd.BootControl.SetRebootToFirmware '{"state":false}' 2>&1 || true ) | grep -q io.systemd.BootControl.RebootToFirmwareNotSupported +} + run_testcases diff --git a/units/meson.build b/units/meson.build index 0c971ef0bc4..936ebf78374 100644 --- a/units/meson.build +++ b/units/meson.build @@ -267,6 +267,15 @@ units = [ 'file' : 'systemd-boot-update.service', 'conditions' : ['ENABLE_BOOTLOADER'], }, + { + 'file' : 'systemd-bootctl@.service.in', + 'conditions' : ['ENABLE_BOOTLOADER'], + }, + { + 'file' : 'systemd-bootctl.socket', + 'conditions' : ['ENABLE_BOOTLOADER'], + 'symlinks' : ['sockets.target.wants/'], + }, { 'file' : 'systemd-confext.service', 'conditions' : ['ENABLE_SYSEXT'], diff --git a/units/systemd-bootctl.socket b/units/systemd-bootctl.socket new file mode 100644 index 00000000000..2b26d7edac0 --- /dev/null +++ b/units/systemd-bootctl.socket @@ -0,0 +1,21 @@ +# 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 Control (Varlink) +Documentation=man:bootctl(1) +DefaultDependencies=no +After=local-fs.target +Before=sockets.target + +[Socket] +ListenStream=/run/systemd/io.systemd.BootControl +FileDescriptorName=varlink +SocketMode=0600 +Accept=yes diff --git a/units/systemd-bootctl@.service.in b/units/systemd-bootctl@.service.in new file mode 100644 index 00000000000..d1c3deddfd7 --- /dev/null +++ b/units/systemd-bootctl@.service.in @@ -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=Boot Control (Varlink) +Documentation=man:bootctl(1) +DefaultDependencies=no +Conflicts=shutdown.target +After=local-fs.target +Before=shutdown.target + +[Service] +Environment=LISTEN_FDNAMES=varlink +ExecStart={{BINDIR}}/bootctl