From: Lennart Poettering Date: Mon, 10 Nov 2025 11:13:11 +0000 (+0100) Subject: add mstack tool for accessing mstacks from the command line X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8187cd18d61c9459f2fdb7591c9eb7c73afea24d;p=thirdparty%2Fsystemd.git add mstack tool for accessing mstacks from the command line --- diff --git a/meson.build b/meson.build index bf419b7f60f..61a2c253ee7 100644 --- a/meson.build +++ b/meson.build @@ -2379,6 +2379,7 @@ subdir('src/measure') subdir('src/modules-load') subdir('src/mount') subdir('src/mountfsd') +subdir('src/mstack') subdir('src/mute-console') subdir('src/network') subdir('src/notify') diff --git a/src/mstack/meson.build b/src/mstack/meson.build new file mode 100644 index 00000000000..27a618f3717 --- /dev/null +++ b/src/mstack/meson.build @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + executable_template + { + 'name' : 'systemd-mstack', + 'public' : true, + 'sources' : files('mstack-tool.c'), + }, +] + +install_symlink('mount.mstack', + pointing_to : sbin_to_bin + 'systemd-mstack', + install_dir : sbindir) diff --git a/src/mstack/mstack-tool.c b/src/mstack/mstack-tool.c new file mode 100644 index 00000000000..01a71d6e567 --- /dev/null +++ b/src/mstack/mstack-tool.c @@ -0,0 +1,436 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +#include "argv-util.h" +#include "build.h" +#include "chase.h" +#include "errno-util.h" +#include "extract-word.h" +#include "fd-util.h" +#include "format-table.h" +#include "image-policy.h" +#include "main-func.h" +#include "mount-util.h" +#include "mountpoint-util.h" +#include "mstack.h" +#include "parse-argument.h" +#include "pretty-print.h" +#include "string-util.h" + +static enum { + ACTION_INSPECT, + ACTION_MOUNT, + ACTION_UMOUNT, +} arg_action = ACTION_INSPECT; +static char *arg_what = NULL; +static char *arg_where = NULL; +static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF; +static PagerFlags arg_pager_flags = 0; +static int arg_legend = true; +static MStackFlags arg_mstack_flags = 0; +static bool arg_rmdir = false; +static ImagePolicy *arg_image_policy = NULL; +static ImageFilter *arg_image_filter = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_what, freep); +STATIC_DESTRUCTOR_REGISTER(arg_where, freep); +STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); +STATIC_DESTRUCTOR_REGISTER(arg_image_filter, image_filter_freep); + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-mstack", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] WHAT\n" + "%1$s [OPTIONS...] --mount WHAT WHERE\n" + "%1$s [OPTIONS...] --umount WHERE\n" + "\n%5$sInspect or apply mount stack.%6$s\n\n" + "%3$sOptions:%4$s\n" + " --no-pager Do not pipe output into a pager\n" + " --no-legend Do not print the column headers\n" + " --json=pretty|short|off Generate JSON output\n" + " -r --read-only Mount read-only\n" + " --mkdir Make mount directory before mounting, if missing\n" + " --rmdir Remove mount directory after unmounting\n" + " --image-policy=POLICY\n" + " Specify image dissection policy\n" + " --image-filter=FILTER\n" + " Specify image dissection filter\n" + "\n%3$sCommands:%4$s\n" + " -h --help Show this help\n" + " --version Show package version\n" + " -m --mount Mount the mstack to the specified directory\n" + " -M Shortcut for --mount --mkdir\n" + " -u --umount Unmount the image from the specified directory\n" + " -U Shortcut for --umount --rmdir\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), ansi_normal(), + ansi_highlight(), ansi_normal()); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_NO_LEGEND, + ARG_JSON, + ARG_MKDIR, + ARG_RMDIR, + ARG_IMAGE_POLICY, + ARG_IMAGE_FILTER, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + { "mount", no_argument, NULL, 'm' }, + { "umount", no_argument, NULL, 'u' }, + { "json", required_argument, NULL, ARG_JSON }, + { "read-only", no_argument, NULL, 'r' }, + { "rmdir", no_argument, NULL, ARG_RMDIR }, + { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, + { "image-filter", required_argument, NULL, ARG_IMAGE_FILTER }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hmMuUr", options, NULL)) >= 0) { + + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + + break; + + case 'r': + arg_mstack_flags |= MSTACK_RDONLY; + break; + + case ARG_IMAGE_POLICY: + r = parse_image_policy_argument(optarg, &arg_image_policy); + if (r < 0) + return r; + break; + + case ARG_IMAGE_FILTER: { + _cleanup_(image_filter_freep) ImageFilter *f = NULL; + r = image_filter_parse(optarg, &f); + if (r < 0) + return log_error_errno(r, "Failed to parse image filter expression: %s", optarg); + + image_filter_free(arg_image_filter); + arg_image_filter = TAKE_PTR(f); + break; + } + + case ARG_MKDIR: + arg_mstack_flags |= MSTACK_MKDIR; + break; + + case ARG_RMDIR: + arg_rmdir = true; + break; + + case 'm': + arg_action = ACTION_MOUNT; + break; + + case 'M': + /* Shortcut combination of --mkdir + --mount */ + arg_action = ACTION_MOUNT; + arg_mstack_flags |= MSTACK_MKDIR; + break; + + case 'u': + arg_action = ACTION_UMOUNT; + break; + + case 'U': + /* Shortcut combination of --rmdir + --umount */ + arg_action = ACTION_UMOUNT; + arg_rmdir = true; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + } + + switch (arg_action) { + + case ACTION_INSPECT: + if (optind + 1 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one argument."); + + r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_what); + if (r < 0) + return r; + + break; + + case ACTION_MOUNT: + if (optind + 2 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected two arguments."); + + r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_what); + if (r < 0) + return r; + + r = parse_path_argument(argv[optind+1], /* suppress_root= */ false, &arg_where); + if (r < 0) + return r; + + break; + + case ACTION_UMOUNT: + if (optind + 1 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one argument."); + + r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_where); + if (r < 0) + return r; + + break; + + default: + assert_not_reached(); + } + + return 1; +} + +static int parse_argv_as_mount_helper(int argc, char *argv[]) { + const char *options = NULL; + bool fake = false; + int c, r; + + /* Implements util-linux "external helper" command line interface, as per mount(8) man page. */ + + while ((c = getopt(argc, argv, "sfnvN:o:t:")) >= 0) { + switch (c) { + + case 'f': + fake = true; + break; + + case 'o': + options = optarg; + break; + + case 't': + if (!streq(optarg, "mstack")) + log_debug("Unexpected file system type '%s', ignoring.", optarg); + break; + + case 's': /* sloppy mount options */ + case 'n': /* aka --no-mtab */ + case 'v': /* aka --verbose */ + log_debug("Ignoring option -%c, not implemented.", c); + break; + + case 'N': /* aka --namespace= */ + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Option -%c is not implemented, refusing.", c); + + case '?': + return -EINVAL; + } + } + + if (optind + 2 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Expected an image file path and target directory as only argument."); + + for (const char *p = options;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, ",", EXTRACT_KEEP_QUOTE); + if (r < 0) + return log_error_errno(r, "Failed to extract mount option: %m"); + if (r == 0) + break; + + if (streq(word, "ro")) + SET_FLAG(arg_mstack_flags, MSTACK_RDONLY, true); + else if (streq(word, "rw")) + SET_FLAG(arg_mstack_flags, MSTACK_RDONLY, false); + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown mount option '%s'.", word); + } + + if (fake) + return 0; + + r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_what); + if (r < 0) + return r; + + r = parse_path_argument(argv[optind+1], /* suppress_root= */ false, &arg_where); + if (r < 0) + return r; + + arg_action = ACTION_MOUNT; + return 1; +} + +static int inspect_mstack(void) { + _cleanup_(mstack_freep) MStack *mstack = NULL; + int r; + + assert(arg_what); + + r = mstack_load(arg_what, /* dir_fd= */ -EBADF, &mstack); + if (r < 0) + return log_debug_errno(r, "Failed to load .mstack/ directory '%s': %m", arg_what); + + _cleanup_(table_unrefp) Table *t = NULL; + + t = table_new("type", "name", "image", "what", "where", "sort"); + if (!t) + return log_oom(); + + table_set_ersatz_string(t, TABLE_ERSATZ_DASH); + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + _cleanup_free_ char *w = NULL; + r = fd_get_path(m->what_fd, &w); + if (r < 0) + return log_error_errno(r, "Failed to get path of what file descriptor: %m"); + + r = table_add_many( + t, + TABLE_STRING, mstack_mount_type_to_string(m->mount_type), + TABLE_STRING, m->what, + TABLE_STRING, image_type_to_string(m->image_type), + TABLE_PATH, w, + TABLE_PATH, m->where ?: ((mstack->root_mount && mstack->root_mount != m) ? "/usr" : "/"), + TABLE_STRING, m->sort_key); + if (r < 0) + return table_log_add_error(r); + } + + return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); +} + +static int mount_mstack(void) { + int r; + + assert(arg_what); + assert(arg_where); + + r = mstack_apply( + arg_what, + /* dir_fd= */ -EBADF, + arg_where, + /* temp_mount_dir= */ NULL, /* auto-create temporary directory */ + /* userns_fd= */ -EBADF, + arg_image_policy, + arg_image_filter, + arg_mstack_flags, + /* ret_root_fd= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to apply .mstack/ directory '%s': %m", arg_what); + + return 0; +} + +static int umount_mstack(void) { + int r; + + assert(arg_where); + + _cleanup_free_ char *canonical = NULL; + _cleanup_close_ int fd = chase_and_open(arg_where, /* root= */ NULL, /* chase_flags= */ 0, O_DIRECTORY, &canonical); + if (fd == -ENOTDIR) + return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "'%s' is not a directory", arg_where); + if (fd < 0) + return log_error_errno(fd, "Failed to resolve path '%s': %m", arg_where); + + r = is_mount_point_at(fd, /* path= */ NULL, /* flags= */ 0); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "'%s' is not a mount point", canonical); + if (r < 0) + return log_error_errno(r, "Failed to determine whether '%s' is a mount point: %m", canonical); + + fd = safe_close(fd); + + r = umount_recursive(canonical, 0); + if (r < 0) + return log_error_errno(r, "Failed to unmount '%s': %m", canonical); + + if (arg_rmdir) { + r = RET_NERRNO(rmdir(canonical)); + if (r < 0) + return log_error_errno(r, "Failed to remove mount directory '%s': %m", canonical); + } + + return 0; +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + if (invoked_as(argv, "mount.mstack")) + r = parse_argv_as_mount_helper(argc, argv); + else + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + switch (arg_action) { + + case ACTION_INSPECT: + return inspect_mstack(); + + case ACTION_MOUNT: + return mount_mstack(); + + case ACTION_UMOUNT: + return umount_mstack(); + + default: + assert_not_reached(); + } +} + +DEFINE_MAIN_FUNCTION(run);