diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index eb0d4bff76..dc56c59058 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -125,6 +125,17 @@ + + + + + If specified, the VM is run with a temporary snapshot of its file system that is removed + immediately when the VM terminates. Only works with currently. + + Note that will not work with . + + + diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 6cfc0e7395..d2adc64931 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -107,6 +107,7 @@ #include "shift-uid.h" #include "signal-util.h" #include "siphash24.h" +#include "snapshot-util.h" #include "socket-util.h" #include "stat-util.h" #include "stdio-util.h" @@ -5915,7 +5916,7 @@ static int do_cleanup(void) { } static int run(int argc, char *argv[]) { - bool remove_directory = false, remove_image = false, veth_created = false; + bool remove_image = false, veth_created = false; _cleanup_close_ int master = -EBADF, userns_fd = -EBADF, mount_fd = -EBADF; _cleanup_fdset_free_ FDSet *fds = NULL; int r, ret = EXIT_SUCCESS; @@ -5923,6 +5924,7 @@ static int run(int argc, char *argv[]) { struct ExposeArgs expose_args = {}; _cleanup_(release_lock_file) LockFile tree_global_lock = LOCK_FILE_INIT, tree_local_lock = LOCK_FILE_INIT; _cleanup_(rmdir_and_freep) char *rootdir = NULL; + _cleanup_(rm_rf_subvolume_and_freep) char *snapshot_dir = NULL; _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; _cleanup_(dissected_image_unrefp) DissectedImage *dissected_image = NULL; _cleanup_(sd_netlink_unrefp) sd_netlink *nfnl = NULL; @@ -6064,63 +6066,27 @@ static int run(int argc, char *argv[]) { } if (arg_ephemeral) { - _cleanup_free_ char *np = NULL; - r = chase_and_update(&arg_directory, 0); if (r < 0) goto finish; - /* If the specified path is a mount point we generate the new snapshot immediately - * inside it under a random name. However if the specified is not a mount point we - * create the new snapshot in the parent directory, just next to it. */ - r = path_is_mount_point(arg_directory); - if (r < 0) { - log_error_errno(r, "Failed to determine whether directory %s is mount point: %m", arg_directory); - goto finish; - } - if (r > 0) - r = tempfn_random_child(arg_directory, "machine.", &np); - else - r = tempfn_random(arg_directory, "machine.", &np); - if (r < 0) { - log_error_errno(r, "Failed to generate name for directory snapshot: %m"); - goto finish; - } - - /* We take an exclusive lock on this image, since it's our private, ephemeral copy - * only owned by us and no one else. */ - r = image_path_lock( + r = create_ephemeral_snapshot( + arg_directory, arg_privileged ? RUNTIME_SCOPE_SYSTEM : RUNTIME_SCOPE_USER, - np, - LOCK_EX|LOCK_NB, - arg_privileged ? &tree_global_lock : NULL, - &tree_local_lock); + arg_read_only, + &tree_global_lock, + &tree_local_lock, + &snapshot_dir); if (r < 0) { - log_error_errno(r, "Failed to lock %s: %m", np); + log_error_errno(r, "Failed to create ephemeral snapshot: %m"); goto finish; } - { - BLOCK_SIGNALS(SIGINT); - r = btrfs_subvol_snapshot_at(AT_FDCWD, arg_directory, AT_FDCWD, np, - (arg_read_only ? BTRFS_SNAPSHOT_READ_ONLY : 0) | - BTRFS_SNAPSHOT_FALLBACK_COPY | - BTRFS_SNAPSHOT_FALLBACK_DIRECTORY | - BTRFS_SNAPSHOT_RECURSIVE | - BTRFS_SNAPSHOT_QUOTA | - BTRFS_SNAPSHOT_SIGINT); - } - if (r == -EINTR) { - log_error_errno(r, "Interrupted while copying file system tree to %s, removed again.", np); - goto finish; - } + r = free_and_strdup(&arg_directory, snapshot_dir); if (r < 0) { - log_error_errno(r, "Failed to create snapshot %s from %s: %m", np, arg_directory); + log_oom(); goto finish; } - - free_and_replace(arg_directory, np); - remove_directory = true; } else { r = chase_and_update(&arg_directory, arg_template ? CHASE_NONEXISTENT : 0); if (r < 0) @@ -6471,14 +6437,6 @@ finish: pager_close(); - if (remove_directory && arg_directory) { - int k; - - k = rm_rf(arg_directory, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME); - if (k < 0) - log_warning_errno(k, "Cannot remove '%s', ignoring: %m", arg_directory); - } - if (remove_image && arg_image) { if (unlink(arg_image) < 0) log_warning_errno(errno, "Can't remove image file '%s', ignoring: %m", arg_image); diff --git a/src/shared/meson.build b/src/shared/meson.build index 9fa976925a..87802469de 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -177,6 +177,7 @@ shared_sources = files( 'sleep-config.c', 'smack-util.c', 'smbios11.c', + 'snapshot-util.c', 'socket-label.c', 'socket-netlink.c', 'specifier.c', diff --git a/src/shared/snapshot-util.c b/src/shared/snapshot-util.c new file mode 100644 index 0000000000..a05cbcc8ea --- /dev/null +++ b/src/shared/snapshot-util.c @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "alloc-util.h" +#include "btrfs-util.h" +#include "discover-image.h" +#include "log.h" +#include "mountpoint-util.h" +#include "snapshot-util.h" +#include "signal-util.h" +#include "tmpfile-util.h" + +int create_ephemeral_snapshot( + const char *directory, + RuntimeScope scope, + bool read_only, + LockFile *tree_global_lock, + LockFile *tree_local_lock, + char **ret_new_path) { + + _cleanup_free_ char *np = NULL; + int r; + + /* If the specified path is a mount point we generate the new snapshot immediately + * inside it under a random name. However if the specified is not a mount point we + * create the new snapshot in the parent directory, just next to it. */ + r = path_is_mount_point(directory); + if (r < 0) + return log_debug_errno(r, "Failed to determine whether directory %s is mount point: %m", directory); + if (r > 0) + r = tempfn_random_child(directory, "snapshot.", &np); + else + r = tempfn_random(directory, "snapshot.", &np); + if (r < 0) + return log_debug_errno(r, "Failed to generate name for directory snapshot: %m"); + + /* We take an exclusive lock on this image, since it's our private, ephemeral copy + * only owned by us and no one else. */ + r = image_path_lock( + scope, + np, + LOCK_EX|LOCK_NB, + scope == RUNTIME_SCOPE_SYSTEM ? tree_global_lock : NULL, + tree_local_lock); + if (r < 0) + return log_debug_errno(r, "Failed to lock %s: %m", np); + + { + BLOCK_SIGNALS(SIGINT); + r = btrfs_subvol_snapshot_at(AT_FDCWD, directory, AT_FDCWD, np, + (read_only ? BTRFS_SNAPSHOT_READ_ONLY : 0) | + BTRFS_SNAPSHOT_FALLBACK_COPY | + BTRFS_SNAPSHOT_FALLBACK_DIRECTORY | + BTRFS_SNAPSHOT_RECURSIVE | + BTRFS_SNAPSHOT_QUOTA | + BTRFS_SNAPSHOT_SIGINT); + } + if (r < 0) + return log_debug_errno(r, "Failed to create snapshot %s from %s: %m", np, directory); + + *ret_new_path = TAKE_PTR(np); + + return 0; +} diff --git a/src/shared/snapshot-util.h b/src/shared/snapshot-util.h new file mode 100644 index 0000000000..18a593f5c8 --- /dev/null +++ b/src/shared/snapshot-util.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "runtime-scope.h" + +/* create_ephemeral_snapshot - create a snapshot of the given directory. + * + * It will use a btrfs snapshot when available with fallback to traditional dir copy. It will set the global + * and local lock files based on the passed runtime scope. On success the new directory path is returned via + * `ret_new_path`. + * + * The caller is responsible for the cleanup of the directory, using `_cleanup_(rm_rf_subvolume_and_freep)` + * is recommended. + */ +int create_ephemeral_snapshot( + const char *directory, + RuntimeScope scope, + bool read_only, + LockFile *tree_global_lock, + LockFile *tree_local_lock, + char **ret_new_path); diff --git a/src/vmspawn/vmspawn-settings.h b/src/vmspawn/vmspawn-settings.h index 1cfe4ffd72..ee937c993a 100644 --- a/src/vmspawn/vmspawn-settings.h +++ b/src/vmspawn/vmspawn-settings.h @@ -35,6 +35,7 @@ typedef enum SettingsMask { SETTING_START_MODE = UINT64_C(1) << 0, SETTING_MACHINE_ID = UINT64_C(1) << 6, SETTING_BIND_MOUNTS = UINT64_C(1) << 11, + SETTING_EPHEMERAL = UINT64_C(1) << 24, SETTING_DIRECTORY = UINT64_C(1) << 26, SETTING_CREDENTIALS = UINT64_C(1) << 30, _SETTING_FORCE_ENUM_WIDTH = UINT64_MAX diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index c2be2c8038..988a391cda 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -62,6 +62,7 @@ #include "random-util.h" #include "rm-rf.h" #include "signal-util.h" +#include "snapshot-util.h" #include "socket-util.h" #include "stat-util.h" #include "stdio-util.h" @@ -145,6 +146,7 @@ static char **arg_bind_user = NULL; static char *arg_bind_user_shell = NULL; static bool arg_bind_user_shell_copy = false; static char **arg_bind_user_groups = NULL; +static bool arg_ephemeral = false; static RuntimeScope arg_runtime_scope = _RUNTIME_SCOPE_INVALID; STATIC_DESTRUCTOR_REGISTER(arg_directory, freep); @@ -190,6 +192,7 @@ static int help(void) { " --system Interact with system manager\n" "\n%3$sImage:%4$s\n" " -D --directory=PATH Root directory for the VM\n" + " -x --ephemeral Run VM with snapshot of the disk or directory\n" " -i --image=FILE|DEVICE Root file system disk image or device for the VM\n" " --image-format=FORMAT Specify disk image format (raw, qcow2; default: raw)\n" "\n%3$sHost Configuration:%4$s\n" @@ -327,6 +330,7 @@ static int parse_argv(int argc, char *argv[]) { { "no-pager", no_argument, NULL, ARG_NO_PAGER }, { "image", required_argument, NULL, 'i' }, { "image-format", required_argument, NULL, ARG_IMAGE_FORMAT }, + { "ephemeral", no_argument, NULL, 'x' }, { "directory", required_argument, NULL, 'D' }, { "machine", required_argument, NULL, 'M' }, { "slice", required_argument, NULL, 'S' }, @@ -382,7 +386,7 @@ static int parse_argv(int argc, char *argv[]) { assert(argv); optind = 0; - while ((c = getopt_long(argc, argv, "+hD:i:M:nqs:G:S:", options, NULL)) >= 0) + while ((c = getopt_long(argc, argv, "+hD:i:xM:nqs:G:S:", options, NULL)) >= 0) switch (c) { case 'h': return help(); @@ -429,6 +433,10 @@ static int parse_argv(int argc, char *argv[]) { } break; + case 'x': + arg_ephemeral = true; + break; + case ARG_NO_PAGER: arg_pager_flags |= PAGER_DISABLE; break; @@ -794,6 +802,9 @@ static int parse_argv(int argc, char *argv[]) { if (!strv_isempty(arg_bind_user_groups) && strv_isempty(arg_bind_user)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --bind-user-group= without --bind-user="); + if (arg_ephemeral && arg_extra_drives.n_drives > 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --ephemeral with --extra-drive="); + if (argc > optind) { arg_kernel_cmdline_extra = strv_copy(argv + optind); if (!arg_kernel_cmdline_extra) @@ -1843,6 +1854,8 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { _cleanup_(ovmf_config_freep) OvmfConfig *ovmf_config = NULL; _cleanup_free_ char *qemu_binary = NULL, *mem = NULL, *kernel = NULL; _cleanup_(rm_rf_physical_and_freep) char *ssh_private_key_path = NULL, *ssh_public_key_path = NULL; + _cleanup_(rm_rf_subvolume_and_freep) char *snapshot_directory = NULL; + _cleanup_(release_lock_file) LockFile tree_global_lock = LOCK_FILE_INIT, tree_local_lock = LOCK_FILE_INIT; _cleanup_close_ int notify_sock_fd = -EBADF; _cleanup_strv_free_ char **cmdline = NULL; _cleanup_free_ int *pass_fds = NULL; @@ -2319,7 +2332,8 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (!escaped_image) return log_oom(); - if (strv_extendf(&cmdline, "if=none,id=vmspawn,file=%s,format=%s,discard=%s", escaped_image, image_format_to_string(arg_image_format), on_off(arg_discard_disk)) < 0) + if (strv_extendf(&cmdline, "if=none,id=vmspawn,file=%s,format=%s,discard=%s,snapshot=%s", + escaped_image, image_format_to_string(arg_image_format), on_off(arg_discard_disk), on_off(arg_ephemeral)) < 0) return log_oom(); _cleanup_free_ char *image_fn = NULL; @@ -2366,6 +2380,21 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (!GREEDY_REALLOC(children, n_children + 1)) return log_oom(); + if (arg_ephemeral) { + r = create_ephemeral_snapshot(arg_directory, + arg_runtime_scope, + /* read-only */ false, + &tree_global_lock, + &tree_local_lock, + &snapshot_directory); + if (r < 0) + return r; + + arg_directory = strdup(snapshot_directory); + if (!arg_directory) + return log_oom(); + } + r = start_virtiofsd( unit, arg_directory, @@ -3065,6 +3094,11 @@ static int determine_names(void) { if (r < 0) return log_error_errno(r, "Failed to extract file name from '%s': %m", arg_directory); } + /* Add a random suffix when this is an ephemeral machine, so that we can run many + * instances at once without manually having to specify -M each time. */ + if (arg_ephemeral) + if (strextendf(&arg_machine, "-%016" PRIx64, random_u64()) < 0) + return log_oom(); hostname_cleanup(arg_machine); if (!hostname_is_valid(arg_machine, 0))