]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
vmspawn: add --ephemeral option similar to nspawn 40505/head
authorMichael Vogt <michael@amutable.com>
Sun, 1 Feb 2026 13:07:40 +0000 (14:07 +0100)
committerMichael Vogt <michael@amutable.com>
Tue, 3 Feb 2026 19:22:14 +0000 (20:22 +0100)
This patch adds a `--ephemeral` option to vmspawn that will turn
on snapshot mode on the qemu disk passed via `--image` or the
directory passed via `--directory`. For disk images it uses the
native mechanism that qemu provides. For directories it (re)uses
the snapshot mechanism that nspawn is using, i.e. it will create
a btrfs snapshot if possible and if not falls back to a traditional
directory copy.

man/systemd-vmspawn.xml
src/nspawn/nspawn.c
src/vmspawn/vmspawn-settings.h
src/vmspawn/vmspawn.c

index eb0d4bff7644b563d753efbd1af43a8b6b27c432..dc56c590587a26bd855a0257be0b358595d7c10e 100644 (file)
           <xi:include href="version-info.xml" xpointer="v260"/></listitem>
         </varlistentry>
 
+        <varlistentry>
+          <term><option>-x</option></term>
+          <term><option>--ephemeral</option></term>
+
+          <listitem><para>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 <option>--image=</option> currently.
+
+          Note that <option>--ephemeral</option> will not work with <option>--extra-drive=</option>.</para>
+
+          <xi:include href="version-info.xml" xpointer="v260"/></listitem>
+        </varlistentry>
       </variablelist>
     </refsect2>
 
index a010245da4d3d8fc540ba02aa709cb46e3db0214..d2adc649318865e7b28688c6ff6b7edad73cbab4 100644 (file)
@@ -6082,8 +6082,8 @@ static int run(int argc, char *argv[]) {
                                 goto finish;
                         }
 
-                        arg_directory = strdup(snapshot_dir);
-                        if (!arg_directory) {
+                        r = free_and_strdup(&arg_directory, snapshot_dir);
+                        if (r < 0) {
                                 log_oom();
                                 goto finish;
                         }
index 1cfe4ffd72978c37c9973d3081f54af986dbec51..ee937c993ac8873dfd25b127470fc0d3a5729efe 100644 (file)
@@ -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
index 06f6961b1a8b3228d804707366737725aebd71e1..7f58f62b0435659cb902fadc84699d3bcf58d0dc 100644 (file)
@@ -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,
@@ -3063,6 +3092,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))