1
0
mirror of https://github.com/systemd/systemd synced 2026-03-14 09:04:47 +01:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Yu Watanabe
5a35a6d9d4
vmspawn: add --ephemeral (#40505)
This PR implements --ephemeral for vmspawn in the same way as nspawn.

Note that this does not support snapshots for --extra-drives.
2026-02-04 11:43:22 +09:00
Michael Vogt
6e67fc2938 vmspawn: add --ephemeral option similar to nspawn
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.
2026-02-03 20:22:14 +01:00
Michael Vogt
96dd2df514 nspawn: extract snapshot creation into new shared helper
This commit extracts the creation of the snapshot for
nspawn into a shared helper. Its not a lot of code but its
subtle so having a single place seems beneficial.

While extracting the snapshot code got tweaked/simplified based
on the feedback from Yu (many thanks!).

With that we can simplify the snapshot delete and error handling
as well. The shared helper is then going to be used by vmspawn
when it also gets the --ephemeral option.
2026-02-02 09:04:37 +01:00
7 changed files with 147 additions and 56 deletions

View File

@ -125,6 +125,17 @@
<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>

View File

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

View File

@ -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',

View File

@ -0,0 +1,65 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/file.h>
#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;
}

View File

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

View 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

View 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,
@ -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))