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))