diff --git a/TODO b/TODO index 216ed7e58d..25165de973 100644 --- a/TODO +++ b/TODO @@ -1086,9 +1086,6 @@ Features: uses RootDirectory= or RootImage=. (Might also over-mount /sys/class/dmi/id/*{uuid,serial} with /dev/null). -* kernel-install: - - add --all switch for rerunning kernel-install for all installed kernels - * doc: prep a document explaining resolved's internal objects, i.e. Query vs. Question vs. Transaction vs. Stream and so on. diff --git a/man/kernel-install.xml b/man/kernel-install.xml index f7a5d8a76b..ee8c0069df 100644 --- a/man/kernel-install.xml +++ b/man/kernel-install.xml @@ -44,6 +44,11 @@ KERNEL-IMAGE INITRD-FILE + + kernel-install + OPTIONS + list + @@ -81,15 +86,17 @@ - add - KERNEL-VERSION - KERNEL-IMAGE [INITRD-FILE ...] + add [[[KERNEL-VERSION] KERNEL-IMAGE] [INITRD-FILE ...]] - This command expects a kernel version string and a path to a kernel image file as arguments. - Optionally, one or more initrd images may be specified as well (note that plugins might generate - additional ones). + This command takes a kernel version string and a path to a kernel image file as arguments. If + the former is omitted, specified as an empty string or as "-" it defaults to the current kernel + version, i.e. the same string uname -r returns. If the latter is omitted, + specified as an empty string or as "-" defaults to + /usr/lib/modules/KERNEL_VERSION/vmlinuz. Optionally, + one or more initrd images may be specified as well (note that plugins might generate additional + ones). The executable files from /usr/lib/kernel/install.d/*.install and /etc/kernel/install.d/*.install (i.e. the plugins) are called with the @@ -144,6 +151,20 @@ + + + add-all + + + + This is the same as add (see above), but invokes the operation iteratively + for every installed kernel in /usr/lib/modules/. This operation is only + supported on systems where the kernel image is installed in + /usr/lib/modules/KERNEL-VERSION/vmlinuz. + + + + remove KERNEL-VERSION @@ -179,17 +200,28 @@ - inspect [[KERNEL-VERSION] KERNEL-IMAGE] [INITRD-FILE ...] + inspect [[[KERNEL-VERSION] KERNEL-IMAGE] [INITRD-FILE ...]] + Takes the same parameters as add. + Shows the various paths and parameters configured or auto-detected. In particular shows the - values of the various $KERNEL_INSTALL_* environment variables listed - below. The option can be used to get the output of this verb as a JSON - object. + values of the various $KERNEL_INSTALL_* environment variables listed below, as + they would be passed to plugins. The option can be used to get the output + of this verb as a JSON object. + + list + + Shows the various installed kernels. This enumerates the subdirectories of + /usr/lib/modules/, and shows whether a kernel image is installed there. + + + + @@ -332,6 +364,7 @@ + @@ -641,6 +674,31 @@ + + + /usr/lib/modules/KERNEL-VERSION/ + + + Location for installed kernel modules and other kernel related resources. For each locally + installed kernel a directory named after the kernel version (uname -r) is + kept. + + + + + + + /usr/lib/modules/KERNEL-VERSION/vmlinuz + + + Location for installed kernel images. This is the recommended location for OS package + managers to install kernel images into (as applicable), from which kernel-install + add then copies it into the final boot partition. + + + + + For various cases listed above, if the $KERNEL_INSTALL_CONF_ROOT environment diff --git a/src/basic/string-util.h b/src/basic/string-util.h index 6b6a5746fe..b6d8be3083 100644 --- a/src/basic/string-util.h +++ b/src/basic/string-util.h @@ -301,3 +301,24 @@ bool version_is_valid(const char *s); bool version_is_valid_versionspec(const char *s); ssize_t strlevenshtein(const char *x, const char *y); + +static inline int strdup_or_null(const char *s, char **ret) { + char *c; + + assert(ret); + + /* This is a lot like strdup(), but is happy with NULL strings, and does not treat that as error, but + * copies the NULL value. */ + + if (!s) { + *ret = NULL; + return 0; + } + + c = strdup(s); + if (!c) + return -ENOMEM; + + *ret = c; + return 1; +} diff --git a/src/kernel-install/kernel-install.c b/src/kernel-install/kernel-install.c index cae93362b6..6631054d07 100644 --- a/src/kernel-install/kernel-install.c +++ b/src/kernel-install/kernel-install.c @@ -2,11 +2,13 @@ #include #include +#include -#include "build.h" #include "boot-entry.h" +#include "build.h" #include "chase.h" #include "conf-files.h" +#include "dirent-util.h" #include "env-file.h" #include "env-util.h" #include "exec-util.h" @@ -14,6 +16,7 @@ #include "fileio.h" #include "find-esp.h" #include "format-table.h" +#include "fs-util.h" #include "id128-util.h" #include "image-policy.h" #include "kernel-image.h" @@ -23,6 +26,7 @@ #include "parse-argument.h" #include "path-util.h" #include "pretty-print.h" +#include "recurse-dir.h" #include "rm-rf.h" #include "stat-util.h" #include "string-table.h" @@ -40,6 +44,7 @@ static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; static char *arg_root = NULL; static char *arg_image = NULL; static ImagePolicy *arg_image_policy = NULL; +static bool arg_legend = true; STATIC_DESTRUCTOR_REGISTER(arg_esp_path, freep); STATIC_DESTRUCTOR_REGISTER(arg_xbootldr_path, freep); @@ -96,6 +101,8 @@ typedef struct Context { char **envp; } Context; +#define CONTEXT_NULL (Context) { .rfd = -EBADF } + static void context_done(Context *c) { assert(c); @@ -120,6 +127,75 @@ static void context_done(Context *c) { safe_close(c->rfd); } +static int context_copy(const Context *source, Context *ret) { + int r; + + assert(source); + assert(ret); + + _cleanup_(context_done) Context copy = (Context) { + .rfd = -EBADF, + .action = source->action, + .machine_id = source->machine_id, + .machine_id_is_random = source->machine_id_is_random, + .kernel_image_type = source->kernel_image_type, + .layout = source->layout, + .entry_token_type = source->entry_token_type, + }; + + copy.rfd = fd_reopen(source->rfd, O_CLOEXEC|O_DIRECTORY|O_PATH); + if (copy.rfd < 0) + return copy.rfd; + + r = strdup_or_null(source->layout_other, ©.layout_other); + if (r < 0) + return r; + r = strdup_or_null(source->conf_root, ©.conf_root); + if (r < 0) + return r; + r = strdup_or_null(source->boot_root, ©.boot_root); + if (r < 0) + return r; + r = strdup_or_null(source->entry_token, ©.entry_token); + if (r < 0) + return r; + r = strdup_or_null(source->entry_dir, ©.entry_dir); + if (r < 0) + return r; + r = strdup_or_null(source->version, ©.version); + if (r < 0) + return r; + r = strdup_or_null(source->kernel, ©.kernel); + if (r < 0) + return r; + copy.initrds = strv_copy(source->initrds); + if (!copy.initrds) + return -ENOMEM; + r = strdup_or_null(source->initrd_generator, ©.initrd_generator); + if (r < 0) + return r; + r = strdup_or_null(source->uki_generator, ©.uki_generator); + if (r < 0) + return r; + r = strdup_or_null(source->staging_area, ©.staging_area); + if (r < 0) + return r; + copy.plugins = strv_copy(source->plugins); + if (!copy.plugins) + return -ENOMEM; + copy.argv = strv_copy(source->argv); + if (!copy.argv) + return -ENOMEM; + copy.envp = strv_copy(source->envp); + if (!copy.envp) + return -ENOMEM; + + *ret = copy; + copy = CONTEXT_NULL; + + return 0; +} + static int context_open_root(Context *c) { assert(c); assert(c->rfd < 0); @@ -1019,11 +1095,67 @@ static bool bypass(void) { return true; } +static int do_add( + Context *c, + const char *version, + const char *kernel, + char **initrds) { + + int r; + + assert(c); + assert(version); + assert(kernel); + + r = context_set_version(c, version); + if (r < 0) + return r; + + r = context_set_kernel(c, kernel); + if (r < 0) + return r; + + r = context_set_initrds(c, initrds); + if (r < 0) + return r; + + r = context_prepare_execution(c); + if (r < 0) + return r; + + return context_execute(c); +} + +static int kernel_from_version(const char *version, char **ret_kernel) { + _cleanup_free_ char *vmlinuz = NULL; + int r; + + assert(version); + + vmlinuz = path_join("/usr/lib/modules/", version, "/vmlinuz"); + if (!vmlinuz) + return log_oom(); + + r = laccess(vmlinuz, F_OK); + if (r < 0) { + if (r == -ENOENT) + return log_error_errno(r, "Kernel image not installed to '%s', requiring manual kernel image path specification.", vmlinuz); + + return log_error_errno(r, "Failed to determin if kernel image is installed to '%s': %m", vmlinuz); + } + + *ret_kernel = TAKE_PTR(vmlinuz); + return 0; +} + static int verb_add(int argc, char *argv[], void *userdata) { Context *c = ASSERT_PTR(userdata); + _cleanup_free_ char *vmlinuz = NULL; + const char *version, *kernel; + char **initrds; + struct utsname un; int r; - assert(argc >= 3); assert(argv); if (arg_root) @@ -1034,23 +1166,106 @@ static int verb_add(int argc, char *argv[], void *userdata) { c->action = ACTION_ADD; - r = context_set_version(c, argv[1]); - if (r < 0) - return r; + /* We use the same order of arguments that "inspect" introduced, i.e. if only on argument is + * specified we take it as the kernel path, not the version, i.e. it's the first argument that is + * optional, not the 2nd. */ + version = argc > 2 ? empty_or_dash_to_null(argv[1]) : NULL; + kernel = argc > 2 ? empty_or_dash_to_null(argv[2]) : + (argc > 1 ? empty_or_dash_to_null(argv[1]) : NULL); + initrds = strv_skip(argv, 3); - r = context_set_kernel(c, argv[2]); - if (r < 0) - return r; + if (!version) { + assert_se(uname(&un) >= 0); + version = un.release; + } - r = context_set_initrds(c, strv_skip(argv, 3)); - if (r < 0) - return r; + if (!kernel) { + r = kernel_from_version(version, &vmlinuz); + if (r < 0) + return r; - r = context_prepare_execution(c); - if (r < 0) - return r; + kernel = vmlinuz; + } - return context_execute(c); + return do_add(c, version, kernel, initrds); +} + +static int verb_add_all(int argc, char *argv[], void *userdata) { + Context *c = ASSERT_PTR(userdata); + _cleanup_close_ int fd = -EBADF; + size_t n = 0; + int ret = 0, r; + + assert(argv); + + if (bypass()) + return 0; + + c->action = ACTION_ADD; + + fd = open("/usr/lib/modules", O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(fd, "Failed to open /usr/lib/modules/: %m"); + + _cleanup_free_ DirectoryEntries *de = NULL; + r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de); + if (r < 0) + return log_error_errno(r, "Failed to numerate /usr/lib/modules/ contents: %m"); + + FOREACH_ARRAY(d, de->entries, de->n_entries) { + + _cleanup_free_ char *j = path_join("/usr/lib/modules/", (*d)->d_name); + if (!j) + return log_oom(); + + r = dirent_ensure_type(fd, *d); + if (r < 0) { + if (r != -ENOENT) /* don't log if just gone by now */ + log_debug_errno(r, "Failed to check if '%s' is a directory, ignoring: %m", j); + continue; + } + + if ((*d)->d_type != DT_DIR) + continue; + + _cleanup_free_ char *fn = path_join((*d)->d_name, "vmlinuz"); + if (!fn) + return log_oom(); + + if (faccessat(fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno != ENOENT) + log_debug_errno(errno, "Failed to check if '/usr/lib/modules/%s/vmlinuz' exists, ignoring: %m", (*d)->d_name); + + log_notice("Not adding version '%s', because kernel image not found.", (*d)->d_name); + continue; + } + + _cleanup_(context_done) Context copy = CONTEXT_NULL; + + r = context_copy(c, ©); + if (r < 0) + return log_error_errno(r, "Failed to copy execution context: %m"); + + _cleanup_free_ char *full = path_join("/usr/lib/modules/", fn); + if (!full) + return log_oom(); + + r = do_add(©, + /* version= */ (*d)->d_name, + /* kernel= */ full, + /* initrds= */ NULL); + RET_GATHER(ret, r); + + if (r >= 0) + n++; + } + + if (n > 0) + log_info("Installed %zu kernels.", n); + else if (ret == 0) + ret = log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No kernels to install found."); + + return ret; } static int run_as_installkernel(int argc, char *argv[], Context *c) { @@ -1082,6 +1297,10 @@ static int verb_remove(int argc, char *argv[], void *userdata) { c->action = ACTION_REMOVE; + /* Note, we do not automatically derive the kernel version to remove from uname() here (unlike we do + * it for the "add" verb), since we don't want to make it too easy to uninstall your running + * kernel, as a safety precaution */ + r = context_set_version(c, argv[1]); if (r < 0) return r; @@ -1096,28 +1315,49 @@ static int verb_remove(int argc, char *argv[], void *userdata) { static int verb_inspect(int argc, char *argv[], void *userdata) { Context *c = ASSERT_PTR(userdata); _cleanup_(table_unrefp) Table *t = NULL; + _cleanup_free_ char *vmlinuz = NULL; + const char *version, *kernel; + char **initrds; + struct utsname un; int r; c->action = ACTION_INSPECT; - if (argc == 2) { - r = context_set_kernel(c, argv[1]); - if (r < 0) - return r; - } else if (argc >= 3) { - r = context_set_version(c, argv[1]); - if (r < 0) - return r; + /* When only a single parameter is specified 'inspect' it's the kernel image path, and not the kernel + * version. i.e. it's the first argument that is optional, not the 2nd. That's a bit unfortunate, but + * we keep the behaviour for compatibility. If users want to specify only the version (and have the + * kernel image path derived automatically), then they may specify an empty string or "dash" as + * kernel image path. */ + version = argc > 2 ? empty_or_dash_to_null(argv[1]) : NULL; + kernel = argc > 2 ? empty_or_dash_to_null(argv[2]) : + (argc > 1 ? empty_or_dash_to_null(argv[1]) : NULL); + initrds = strv_skip(argv, 3); - r = context_set_kernel(c, argv[2]); - if (r < 0) - return r; - - r = context_set_initrds(c, strv_skip(argv, 3)); - if (r < 0) - return r; + if (!version) { + assert_se(uname(&un) >= 0); + version = un.release; } + if (!kernel) { + r = kernel_from_version(version, &vmlinuz); + if (r < 0) + return r; + + kernel = vmlinuz; + } + + r = context_set_version(c, version); + if (r < 0) + return r; + + r = context_set_kernel(c, kernel); + if (r < 0) + return r; + + r = context_set_initrds(c, initrds); + if (r < 0) + return r; + r = context_prepare_execution(c); if (r < 0) return r; @@ -1183,6 +1423,68 @@ static int verb_inspect(int argc, char *argv[], void *userdata) { return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, /* show_header= */ false); } +static int verb_list(int argc, char *argv[], void *userdata) { + _cleanup_close_ int fd = -EBADF; + int r; + + fd = open("/usr/lib/modules", O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(fd, "Failed to open /usr/lib/modules/: %m"); + + _cleanup_free_ DirectoryEntries *de = NULL; + r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de); + if (r < 0) + return log_error_errno(r, "Failed to numerate /usr/lib/modules/ contents: %m"); + + _cleanup_(table_unrefp) Table *table = NULL; + table = table_new("version", "has kernel", "path"); + if (!table) + return log_oom(); + + table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + table_set_align_percent(table, table_get_cell(table, 0, 1), 100); + + FOREACH_ARRAY(d, de->entries, de->n_entries) { + + _cleanup_free_ char *j = path_join("/usr/lib/modules/", (*d)->d_name); + if (!j) + return log_oom(); + + r = dirent_ensure_type(fd, *d); + if (r < 0) { + if (r != -ENOENT) /* don't log if just gone by now */ + log_debug_errno(r, "Failed to check if '%s' is a directory, ignoring: %m", j); + continue; + } + + if ((*d)->d_type != DT_DIR) + continue; + + _cleanup_free_ char *fn = path_join((*d)->d_name, "vmlinuz"); + if (!fn) + return log_oom(); + + bool exists; + if (faccessat(fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno != ENOENT) + log_debug_errno(errno, "Failed to check if '/usr/lib/modules/%s/vmlinuz' exists, ignoring: %m", (*d)->d_name); + + exists = false; + } else + exists = true; + + r = table_add_many(table, + TABLE_STRING, (*d)->d_name, + TABLE_BOOLEAN_CHECKMARK, exists, + TABLE_SET_COLOR, ansi_highlight_green_red(exists), + TABLE_PATH, j); + if (r < 0) + return table_log_add_error(r); + } + + return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend); +} + static int help(void) { _cleanup_free_ char *link = NULL; int r; @@ -1192,13 +1494,15 @@ static int help(void) { return log_oom(); printf("%1$s [OPTIONS...] COMMAND ...\n\n" - "%2$sAdd and remove kernel and initrd images to and from /boot%3$s\n" - "\nUsage:\n" - " kernel-install [OPTIONS...] add KERNEL-VERSION KERNEL-IMAGE [INITRD ...]\n" + "%5$sAdd and remove kernel and initrd images to and from /boot/%6$s\n" + "\n%3$sUsage:%4$s\n" + " kernel-install [OPTIONS...] add [[[KERNEL-VERSION] KERNEL-IMAGE] [INITRD ...]]\n" + " kernel-install [OPTIONS...] add-all\n" " kernel-install [OPTIONS...] remove KERNEL-VERSION\n" - " kernel-install [OPTIONS...] inspect [KERNEL-VERSION] KERNEL-IMAGE [INITRD ...]\n" - "\n" - "Options:\n" + " kernel-install [OPTIONS...] inspect [[[KERNEL-VERSION] KERNEL-IMAGE]\n" + " [INITRD ...]]\n" + " kernel-install [OPTIONS...] list\n" + "\n%3$sOptions:%4$s\n" " -h --help Show this help\n" " --version Show package version\n" " -v --verbose Increase verbosity\n" @@ -1210,6 +1514,7 @@ static int help(void) { " Entry token to use for this installation\n" " --no-pager Do not pipe inspect output into a pager\n" " --json=pretty|short|off Generate JSON output\n" + " --no-legend Do not show the headers and footers\n" " --root=PATH Operate on an alternate filesystem root\n" " --image=PATH Operate on disk image as filesystem root\n" " --image-policy=POLICY Specify disk image dissection policy\n" @@ -1218,11 +1523,13 @@ static int help(void) { " installkernel [OPTIONS...] VERSION VMLINUZ [MAP] [INSTALLATION-DIR]\n" "(The optional arguments are passed by kernel build system, but ignored.)\n" "\n" - "See the %4$s for details.\n", + "See the %2$s for details.\n", program_invocation_short_name, - ansi_highlight(), + link, + ansi_underline(), ansi_normal(), - link); + ansi_highlight(), + ansi_normal()); return 0; } @@ -1230,6 +1537,7 @@ static int help(void) { static int parse_argv(int argc, char *argv[], Context *c) { enum { ARG_VERSION = 0x100, + ARG_NO_LEGEND, ARG_ESP_PATH, ARG_BOOT_PATH, ARG_MAKE_ENTRY_DIRECTORY, @@ -1253,6 +1561,7 @@ static int parse_argv(int argc, char *argv[], Context *c) { { "root", required_argument, NULL, ARG_ROOT }, { "image", required_argument, NULL, ARG_IMAGE }, { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, {} }; int t, r; @@ -1269,6 +1578,10 @@ static int parse_argv(int argc, char *argv[], Context *c) { case ARG_VERSION: return version(); + case ARG_NO_LEGEND: + arg_legend = false; + break; + case 'v': log_set_max_level(LOG_DEBUG); arg_verbose = true; @@ -1347,9 +1660,11 @@ static int parse_argv(int argc, char *argv[], Context *c) { static int run(int argc, char* argv[]) { static const Verb verbs[] = { - { "add", 3, VERB_ANY, 0, verb_add }, + { "add", 1, VERB_ANY, 0, verb_add }, + { "add-all", 1, 1, 0, verb_add_all }, { "remove", 2, VERB_ANY, 0, verb_remove }, { "inspect", 1, VERB_ANY, VERB_DEFAULT, verb_inspect }, + { "list", 1, 1, 0, verb_list }, {} }; _cleanup_(context_done) Context c = { diff --git a/src/kernel-install/test-kernel-install.sh b/src/kernel-install/test-kernel-install.sh index 931fae5372..db412ec524 100755 --- a/src/kernel-install/test-kernel-install.sh +++ b/src/kernel-install/test-kernel-install.sh @@ -71,7 +71,7 @@ grep -qE 'image' "$BOOT_ROOT/the-token/1.1.1/linux" grep -qE 'initrd' "$BOOT_ROOT/the-token/1.1.1/initrd" grep -qE 'DTBDTB' "$BOOT_ROOT/the-token/1.1.1/whatever.dtb" -"$kernel_install" inspect +test -f /usr/lib/modules/"$(uname -r)"/vmlinuz && "$kernel_install" inspect "$kernel_install" inspect "$D/sources/linux" "$kernel_install" -v remove 1.1.1