1
0
mirror of https://github.com/systemd/systemd synced 2024-07-09 04:26:06 +00:00

bootctl: unlink and cleanup functions

The unlink command removes an entry from the ESP including
referenced files that are not referenced in other entries. That is
useful eg to have multiple entries that use the same kernel with
different options.

The cleanup command removes all files that are not referenced by any
entry.
This commit is contained in:
Ludwig Nussel 2022-12-08 16:27:31 +01:00
parent 1132fd73b3
commit 8702496bfb
11 changed files with 351 additions and 7 deletions

View File

@ -153,6 +153,21 @@
disables the timeout while always showing the menu. When an empty string ("") is specified the
bootloader will revert to its default menu timeout.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>unlink</option> <replaceable>ID</replaceable></term>
<listitem><para>Removes a boot loader entry including the files it refers to. Takes a single boot
loader entry ID string or a glob pattern as argument. Referenced files such as kernel or initrd are
only removed if no other entry refers to them.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>cleanup</option></term>
<listitem><para>Removes files from the ESP and XBOOTLDR partitions that belong to the entry token but
are not referenced in any boot loader entries.</para></listitem>
</varlistentry>
</variablelist>
</refsect1>
@ -409,6 +424,14 @@
the firmware's boot option menu.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--dry-run</option></term>
<listitem><para>Dry run for <option>--unlink</option> and <option>--cleanup</option>.</para>
<para>In dry run mode, the unlink and cleanup operations only print the files that would get deleted
without actually deleting them.</para></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="no-pager"/>
<xi:include href="standard-options.xml" xpointer="json" />
<xi:include href="standard-options.xml" xpointer="help"/>

View File

@ -4051,6 +4051,7 @@ public_programs += exe
if want_tests != 'false' and want_kernel_install
test('test-kernel-install',
test_kernel_install_sh,
env : test_env,
args : [exe.full_path(), loaderentry_install])
endif

View File

@ -31,7 +31,7 @@ _bootctl() {
local i verb comps
local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]}
local -A OPTS=(
[STANDALONE]='-h --help -p --print-esp-path -x --print-boot-path --version --no-variables --no-pager --graceful'
[STANDALONE]='-h --help -p --print-esp-path -x --print-boot-path --version --no-variables --no-pager --graceful --dry-run'
[ARG]='--esp-path --boot-path --make-machine-id-directory --root --image --install-source'
)
@ -67,8 +67,8 @@ _bootctl() {
local -A VERBS=(
# systemd-efi-options takes an argument, but it is free-form, so we cannot complete it
[STANDALONE]='help status install update remove is-installed random-seed systemd-efi-options list set-timeout set-timeout-oneshot'
[BOOTENTRY]='set-default set-oneshot'
[STANDALONE]='help status install update remove is-installed random-seed systemd-efi-options list set-timeout set-timeout-oneshot cleanup'
[BOOTENTRY]='set-default set-oneshot unlink'
[BOOLEAN]='reboot-to-firmware'
)

View File

@ -24,6 +24,10 @@ _bootctl_set-oneshot() {
_bootctl_comp_ids
}
_bootctl_unlink() {
_bootctl_comp_ids
}
_bootctl_reboot-to-firmware() {
local -a _completions
_completions=( yes no )
@ -48,6 +52,8 @@ _bootctl_reboot-to-firmware() {
"set-oneshot:Set the default boot loader entry only for the next boot"
"set-timeout:Set the menu timeout"
"set-timeout-oneshot:Set the menu timeout for the next boot only"
"unlink:Remove boot loader entry"
"cleanup:Remove files in ESP not referenced in any boot entry"
)
if (( CURRENT == 1 )); then
_describe -t commands 'bootctl command' _bootctl_cmds || compadd "$@"
@ -73,6 +79,7 @@ _arguments \
'--no-variables[Do not touch EFI variables]' \
'--no-pager[Do not pipe output into a pager]' \
'--graceful[Do not fail when locating ESP or writing fails]' \
'--dry-run[Dry run (unlink and cleanup)]' \
'--root=[Operate under the specified directory]:PATH' \
'--image=[Operate on the specified image]:PATH' \
'--install-source[Where to pick files when using --root=/--image=]:options:(image host auto)' \

View File

@ -18,6 +18,7 @@
#include "find-esp.h"
#include "path-util.h"
#include "pretty-print.h"
#include "recurse-dir.h"
#include "terminal-util.h"
#include "tpm2-util.h"
@ -503,6 +504,250 @@ int verb_status(int argc, char *argv[], void *userdata) {
return r;
}
static int ref_file(Hashmap *known_files, const char *fn, int increment) {
char *k = NULL;
int n, r;
assert(known_files);
/* just gracefully ignore this. This way the caller doesn't
have to verify whether the bootloader entry is relevant */
if (!fn)
return 0;
n = PTR_TO_INT(hashmap_get2(known_files, fn, (void**)&k));
n += increment;
assert(n >= 0);
if (n == 0) {
(void) hashmap_remove(known_files, fn);
free(k);
} else if (!k) {
_cleanup_free_ char *t = NULL;
t = strdup(fn);
if (!t)
return -ENOMEM;
r = hashmap_put(known_files, t, INT_TO_PTR(n));
if (r < 0)
return r;
TAKE_PTR(t);
} else {
r = hashmap_update(known_files, fn, INT_TO_PTR(n));
if (r < 0)
return r;
}
return n;
}
static void deref_unlink_file(Hashmap *known_files, const char *fn, const char *root) {
_cleanup_free_ char *path = NULL;
int r;
/* just gracefully ignore this. This way the caller doesn't
have to verify whether the bootloader entry is relevant */
if (!fn || !root)
return;
r = ref_file(known_files, fn, -1);
if (r < 0)
return (void) log_warning_errno(r, "Failed to deref \"%s\", ignoring: %m", fn);
if (r > 0)
return;
if (arg_dry_run) {
r = chase_symlinks_and_access(fn, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, F_OK, &path, NULL);
if (r < 0)
log_info("Unable to determine whether \"%s\" exists, ignoring: %m", fn);
else
log_info("Would remove %s", path);
return;
}
r = chase_symlinks_and_unlink(fn, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, 0, &path);
if (r >= 0)
log_info("Removed %s", path);
else if (r != -ENOENT)
return (void) log_warning_errno(r, "Failed to remove \"%s\", ignoring: %m", path ?: fn);
_cleanup_free_ char *d = NULL;
if (path_extract_directory(fn, &d) >= 0 && !path_equal(d, "/")) {
r = chase_symlinks_and_unlink(d, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, AT_REMOVEDIR, NULL);
if (r < 0 && !IN_SET(r, -ENOTEMPTY, -ENOENT))
log_warning_errno(r, "Failed to remove directoy \"%s\", ignoring: %m", d);
}
}
static int count_known_files(const BootConfig *config, const char* root, Hashmap **ret_known_files) {
_cleanup_(hashmap_free_free_keyp) Hashmap *known_files = NULL;
int r = 0;
assert(config);
assert(ret_known_files);
known_files = hashmap_new(&path_hash_ops);
if (!known_files)
return -ENOMEM;
for (size_t i = 0; i < config->n_entries; i++) {
const BootEntry *e = config->entries + i;
if (!path_equal(e->root, root))
continue;
r = ref_file(known_files, e->kernel, +1);
if (r < 0)
return r;
r = ref_file(known_files, e->efi, +1);
if (r < 0)
return r;
STRV_FOREACH(s, e->initrd) {
r = ref_file(known_files, *s, +1);
if (r < 0)
return r;
}
r = ref_file(known_files, e->device_tree, +1);
if (r < 0)
return r;
STRV_FOREACH(s, e->device_tree_overlay) {
r = ref_file(known_files, *s, +1);
if (r < 0)
return r;
}
}
*ret_known_files = TAKE_PTR(known_files);
return 0;
}
static int boot_config_find_in(const BootConfig *config, const char *root, const char *id) {
assert(config);
if (!root || !id)
return -1;
for (size_t i = 0; i < config->n_entries; i++)
if (path_equal(config->entries[i].root, root)
&& fnmatch(id, config->entries[i].id, FNM_CASEFOLD) == 0)
return i;
return -1;
}
static int unlink_entry(const BootConfig *config, const char *root, const char *id) {
_cleanup_(hashmap_free_free_keyp) Hashmap *known_files = NULL;
const BootEntry *e = NULL;
int r;
assert(config);
r = count_known_files(config, root, &known_files);
if (r < 0)
return log_error_errno(r, "Failed to count files in %s: %m", root);
r = boot_config_find_in(config, root, id);
if (r < 0)
return -ENOENT;
if (r == config->default_entry)
log_warning("%s is the default boot entry", id);
if (r == config->selected_entry)
log_warning("%s is the selected boot entry", id);
e = &config->entries[r];
deref_unlink_file(known_files, e->kernel, e->root);
deref_unlink_file(known_files, e->efi, e->root);
STRV_FOREACH(s, e->initrd)
deref_unlink_file(known_files, *s, e->root);
deref_unlink_file(known_files, e->device_tree, e->root);
STRV_FOREACH(s, e->device_tree_overlay)
deref_unlink_file(known_files, *s, e->root);
if (arg_dry_run)
log_info("Would remove %s", e->path);
else {
r = chase_symlinks_and_unlink(e->path, root, CHASE_PROHIBIT_SYMLINKS, 0, NULL);
if (r < 0)
return log_error_errno(r, "Failed to remove \"%s\": %m", e->path);
log_info("Removed %s", e->path);
}
return 0;
}
static int list_remove_orphaned_file(
RecurseDirEvent event,
const char *path,
int dir_fd,
int inode_fd,
const struct dirent *de,
const struct statx *sx,
void *userdata) {
Hashmap *known_files = userdata;
assert(path);
assert(known_files);
if (event != RECURSE_DIR_ENTRY)
return RECURSE_DIR_CONTINUE;
if (hashmap_get(known_files, path))
return RECURSE_DIR_CONTINUE; /* keep! */
if (arg_dry_run)
log_info("Would remove %s", path);
else if (unlinkat(dir_fd, de->d_name, 0) < 0)
log_warning_errno(errno, "Failed to remove \"%s\", ignoring: %m", path);
else
log_info("Removed %s", path);
return RECURSE_DIR_CONTINUE;
}
static int cleanup_orphaned_files(
const BootConfig *config,
const char *root) {
_cleanup_(hashmap_free_free_keyp) Hashmap *known_files = NULL;
_cleanup_free_ char *full = NULL, *p = NULL;
_cleanup_close_ int dir_fd = -1;
int r = -1;
assert(config);
assert(root);
log_info("Cleaning %s", root);
r = settle_entry_token();
if (r < 0)
return r;
r = count_known_files(config, root, &known_files);
if (r < 0)
return log_error_errno(r, "Failed to count files in %s: %m", root);
dir_fd = chase_symlinks_and_open(arg_entry_token, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS,
O_DIRECTORY|O_CLOEXEC, &full);
if (dir_fd == -ENOENT)
return 0;
if (dir_fd < 0)
return log_error_errno(dir_fd, "Failed to open '%s/%s': %m", root, arg_entry_token);
p = path_join("/", arg_entry_token);
if (!p)
return log_oom();
r = recurse_dir(dir_fd, p, 0, UINT_MAX, RECURSE_DIR_SORT, list_remove_orphaned_file, known_files);
if (r < 0)
return log_error_errno(r, "Failed to cleanup %s: %m", full);
return r;
}
int verb_list(int argc, char *argv[], void *userdata) {
_cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
dev_t esp_devid = 0, xbootldr_devid = 0;
@ -534,6 +779,24 @@ int verb_list(int argc, char *argv[], void *userdata) {
return 0;
}
pager_open(arg_pager_flags);
return show_boot_entries(&config, arg_json_format_flags);
if (streq(argv[0], "list")) {
pager_open(arg_pager_flags);
return show_boot_entries(&config, arg_json_format_flags);
} else if (streq(argv[0], "cleanup")) {
if (arg_xbootldr_path && xbootldr_devid != esp_devid)
cleanup_orphaned_files(&config, arg_xbootldr_path);
return cleanup_orphaned_files(&config, arg_esp_path);
} else {
assert(streq(argv[0], "unlink"));
if (arg_xbootldr_path && xbootldr_devid != esp_devid) {
r = unlink_entry(&config, arg_xbootldr_path, argv[1]);
if (r == 0 || r != -ENOENT)
return r;
}
return unlink_entry(&config, arg_esp_path, argv[1]);
}
}
int verb_unlink(int argc, char *argv[], void *userdata) {
return verb_list(argc, argv, userdata);
}

View File

@ -2,3 +2,4 @@
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);

View File

@ -48,6 +48,7 @@ char *arg_root = NULL;
char *arg_image = NULL;
InstallSource arg_install_source = ARG_INSTALL_SOURCE_AUTO;
char *arg_efi_boot_option_description = NULL;
bool arg_dry_run = false;
STATIC_DESTRUCTOR_REGISTER(arg_esp_path, freep);
STATIC_DESTRUCTOR_REGISTER(arg_xbootldr_path, freep);
@ -145,6 +146,8 @@ static int help(int argc, char *argv[], void *userdata) {
" set-timeout SECONDS Set the menu timeout\n"
" set-timeout-oneshot SECONDS\n"
" Set the menu timeout for the next boot only\n"
" unlink ID Remove boot loader entry\n"
" cleanup Remove files in ESP not referenced in any boot entry\n"
"\n%3$ssystemd-boot Commands:%4$s\n"
" install Install systemd-boot to the ESP and EFI variables\n"
" update Update systemd-boot in the ESP and EFI variables\n"
@ -179,6 +182,7 @@ static int help(int argc, char *argv[], void *userdata) {
" Install all supported EFI architectures\n"
" --efi-boot-option-description=DESCRIPTION\n"
" Description of the entry in the boot option list\n"
" --dry-run Dry run (unlink and cleanup)\n"
"\nSee the %2$s for details.\n",
program_invocation_short_name,
link,
@ -206,6 +210,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_JSON,
ARG_ARCH_ALL,
ARG_EFI_BOOT_OPTION_DESCRIPTION,
ARG_DRY_RUN,
};
static const struct option options[] = {
@ -230,6 +235,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "json", required_argument, NULL, ARG_JSON },
{ "all-architectures", no_argument, NULL, ARG_ARCH_ALL },
{ "efi-boot-option-description", required_argument, NULL, ARG_EFI_BOOT_OPTION_DESCRIPTION },
{ "dry-run", no_argument, NULL, ARG_DRY_RUN },
{}
};
@ -379,6 +385,10 @@ static int parse_argv(int argc, char *argv[]) {
return r;
break;
case ARG_DRY_RUN:
arg_dry_run = true;
break;
case '?':
return -EINVAL;
@ -387,7 +397,7 @@ static int parse_argv(int argc, char *argv[]) {
}
if ((arg_root || arg_image) && argv[optind] && !STR_IN_SET(argv[optind], "status", "list",
"install", "update", "remove", "is-installed", "random-seed"))
"install", "update", "remove", "is-installed", "random-seed", "unlink", "cleanup"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Options --root= and --image= are not supported with verb %s.",
argv[optind]);
@ -398,6 +408,9 @@ static int parse_argv(int argc, char *argv[]) {
if (arg_install_source != ARG_INSTALL_SOURCE_AUTO && !arg_root && !arg_image)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--install-from-host is only supported with --root= or --image=.");
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");
return 1;
}
@ -412,6 +425,8 @@ static int bootctl_main(int argc, char *argv[]) {
{ "kernel-identify", 2, 2, 0, verb_kernel_identify },
{ "kernel-inspect", 2, 2, 0, verb_kernel_inspect },
{ "list", VERB_ANY, 1, 0, verb_list },
{ "unlink", 2, 2, 0, verb_unlink },
{ "cleanup", VERB_ANY, 1, 0, verb_list },
{ "set-default", 2, 2, 0, verb_set_efivar },
{ "set-oneshot", 2, 2, 0, verb_set_efivar },
{ "set-timeout", 2, 2, 0, verb_set_efivar },

View File

@ -39,6 +39,7 @@ extern char *arg_root;
extern char *arg_image;
extern InstallSource arg_install_source;
extern char *arg_efi_boot_option_description;
extern bool arg_dry_run;
static inline const char *arg_dollar_boot_path(void) {
/* $BOOT shall be the XBOOTLDR partition if it exists, and otherwise the ESP */

View File

@ -32,7 +32,7 @@ MACHINE_ID="$KERNEL_INSTALL_MACHINE_ID"
ENTRY_TOKEN="$KERNEL_INSTALL_ENTRY_TOKEN"
BOOT_ROOT="$KERNEL_INSTALL_BOOT_ROOT"
BOOT_MNT="$(stat -c %m "$BOOT_ROOT")"
[ -n "$BOOT_MNT" ] || BOOT_MNT="$(stat -c %m "$BOOT_ROOT")"
if [ "$BOOT_MNT" = '/' ]; then
ENTRY_DIR="$ENTRY_DIR_ABS"
else

View File

@ -9,6 +9,8 @@ plugin="${2:?}"
D="$(mktemp --tmpdir --directory "test-kernel-install.XXXXXXXXXX")"
export _KERNEL_INSTALL_BOOTCTL="$PROJECT_BUILD_ROOT/bootctl"
# shellcheck disable=SC2064
trap "rm -rf '$D'" EXIT INT QUIT PIPE
mkdir -p "$D/boot"
@ -31,6 +33,7 @@ EOF
export KERNEL_INSTALL_CONF_ROOT="$D/sources"
export KERNEL_INSTALL_PLUGINS="$plugin"
export BOOT_ROOT="$D/boot"
export BOOT_MNT="$D/boot"
export MACHINE_ID='3e0484f3634a418b8e6a39e8828b03e3'
"$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
@ -82,3 +85,32 @@ grep -qE '^initrd .*/the-token/1.1.1/initrd' "$entry"
grep -qE 'image' "$BOOT_ROOT/the-token/1.1.1/linux"
grep -qE 'initrd' "$BOOT_ROOT/the-token/1.1.1/initrd"
if test -x "$_KERNEL_INSTALL_BOOTCTL"; then
echo "Testing bootctl"
e2="${entry%+*}_2.conf"
cp "$entry" "$e2"
export SYSTEMD_ESP_PATH=/
# create file that is not referenced. Check if cleanup removes
# it but leaves the rest alone
:> "$BOOT_ROOT/the-token/1.1.2/initrd"
"$_KERNEL_INSTALL_BOOTCTL" --root="$BOOT_ROOT" cleanup
test ! -e "$BOOT_ROOT/the-token/1.1.2/initrd"
test -e "$BOOT_ROOT/the-token/1.1.2/linux"
test -e "$BOOT_ROOT/the-token/1.1.1/linux"
test -e "$BOOT_ROOT/the-token/1.1.1/initrd"
# now remove duplicated entry and make sure files are left over
"$_KERNEL_INSTALL_BOOTCTL" --root="$BOOT_ROOT" unlink "${e2##*/}"
test -e "$BOOT_ROOT/the-token/1.1.1/linux"
test -e "$BOOT_ROOT/the-token/1.1.1/initrd"
test -e "$entry"
test ! -e "$e2"
# remove last entry referencing those files
entry_id="${entry##*/}"
entry_id="${entry_id%+*}.conf"
"$_KERNEL_INSTALL_BOOTCTL" --root="$BOOT_ROOT" unlink "$entry_id"
test ! -e "$entry"
test ! -e "$BOOT_ROOT/the-token/1.1.1/linux"
test ! -e "$BOOT_ROOT/the-token/1.1.1/initrd"
fi

View File

@ -15,6 +15,7 @@ path = run_command(sh, '-c', 'echo "$PATH"', check: true).stdout().strip()
test_env = environment()
test_env.set('SYSTEMD_LANGUAGE_FALLBACK_MAP', language_fallback_map)
test_env.set('PATH', project_build_root + ':' + path)
test_env.set('PROJECT_BUILD_ROOT', project_build_root)
############################################################