From: Christian Brauner Date: Fri, 1 May 2026 11:38:12 +0000 (+0200) Subject: docs,test: --bind-volume / bind-volume / unbind-volume X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=8a4451d2f0d994562ec01f88283b23986565126e;p=thirdparty%2Fsystemd.git docs,test: --bind-volume / bind-volume / unbind-volume - Document the new --bind-volume= option in systemd-vmspawn(1) and the new bind-volume / unbind-volume verbs in machinectl(1). - Add an integration test (TEST-87-AUX-UTILS-VM.bind-volume.sh) covering boot-time attach via --bind-volume, runtime attach via 'machinectl bind-volume', runtime detach via 'machinectl unbind-volume', the StorageImmutable rejection of attempts to detach boot-time volumes, and the NoSuchStorage rejection of detach on unknown names. - Strike "hook-up in systemd-vmspawn" from TODO.md; the nspawn and service-manager hookups remain. Signed-off-by: Christian Brauner (Amutable) --- diff --git a/TODO.md b/TODO.md index 61114235f32..f3d1070c6c7 100644 --- a/TODO.md +++ b/TODO.md @@ -138,7 +138,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later - StorageProvider interface + storagectl - hook-up in systemd-nspawn - - hook-up in systemd-vmspawn - hook-up in service manager (BindVolume=) - introduce a locking concept: right now all access to volumes is fully shared. Let's add a basic locking concept: supporting backends can take an diff --git a/man/machinectl.xml b/man/machinectl.xml index b4fb15b4f93..10ab225074d 100644 --- a/man/machinectl.xml +++ b/man/machinectl.xml @@ -357,6 +357,38 @@ + + bind-volume NAME SPEC + + Acquire a storage volume from a + storagectl1 + provider and attach it to the running machine. SPEC is a string of the form + PROVIDER:VOLUME[:CONFIG][:K=V,…], + identical in grammar to the argument of + systemd-vmspawn1. + + The attached volume is identified by the name PROVIDER:VOLUME + and may be detached at runtime via unbind-volume. Currently only supported for + systemd-vmspawn machines that expose an + io.systemd.MachineInstance control socket. + + + + + + unbind-volume NAME STORAGE-NAME + + Detach a storage volume from the running machine. STORAGE-NAME + is the PROVIDER:VOLUME + identifier that was specified at bind-volume time. Volumes that were attached at machine + startup (e.g. via on + systemd-vmspawn1) + cannot be detached and will fail with + io.systemd.MachineInstance.StorageImmutable. + + + + copy-to NAME PATH [PATH] diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index 5c5ec4ccbcd..b23c6651422 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -566,6 +566,39 @@ + + + + Acquire a storage volume from a + storagectl1 + provider and attach it to the virtual machine. PROVIDER is the + provider name (typically block or fs). VOLUME + is the volume name passed to the provider's Acquire() method. + CONFIG selects the guest device type and takes one of + virtio-blk, virtio-scsi, nvme, or + scsi-cd. If empty or omitted, defaults to virtio-blk. + + The trailing comma-separated K=V list passes parameters to + io.systemd.StorageProvider.Acquire(): template=, + create= (one of any, new, open), + read-only= (or ro=; takes a boolean or auto), + size= / create-size= (size for created volumes), + request-as= (one of blk, reg, + dir; dir is rejected by vmspawn). + + Each attached volume is identified by the name PROVIDER:VOLUME. + Volumes attached at startup via this option cannot be detached at runtime via + machinectl unbind-volume; only volumes added at runtime via + machinectl bind-volume are removable. + + The provider is looked up under + /run/systemd/io.systemd.StorageProvider/ for system mode (or + $XDG_RUNTIME_DIR/systemd/io.systemd.StorageProvider/ for user mode), matching + the runtime scope chosen via / . + + + + diff --git a/test/units/TEST-87-AUX-UTILS-VM.bind-volume.sh b/test/units/TEST-87-AUX-UTILS-VM.bind-volume.sh new file mode 100755 index 00000000000..6339e390936 --- /dev/null +++ b/test/units/TEST-87-AUX-UTILS-VM.bind-volume.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# Test --bind-volume / machinectl bind-volume / unbind-volume integration with the +# StorageProvider Varlink interface. +# +# Exercises: +# - --bind-volume parser + runtime_directory_generic + Acquire round-trip +# - boot-time attach via DriveInfo (non-removable) +# - runtime hotplug via io.systemd.MachineInstance.AddStorage (removable) +# - runtime hot-remove via io.systemd.MachineInstance.RemoveStorage +# - StorageImmutable rejection for boot-time attached volumes +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +if [[ -v ASAN_OPTIONS ]]; then + echo "vmspawn launches QEMU which doesn't work under ASan, skipping" + exit 0 +fi + +if ! command -v systemd-vmspawn >/dev/null 2>&1; then + echo "systemd-vmspawn not found, skipping" + exit 0 +fi + +if ! command -v storagectl >/dev/null 2>&1; then + echo "storagectl not found, skipping" + exit 0 +fi + +if ! find_qemu_binary; then + echo "QEMU not found, skipping" + exit 0 +fi + +if ! command -v mke2fs >/dev/null 2>&1; then + echo "mke2fs not found, skipping" + exit 0 +fi + +# Storage providers are socket-activated; skip if the fs provider socket isn't present. +if ! test -S /run/systemd/io.systemd.StorageProvider/fs; then + echo "StorageProvider fs socket not found, skipping" + exit 0 +fi + +# Find a kernel for direct boot +KERNEL="" +for k in /usr/lib/modules/"$(uname -r)"/vmlinuz /boot/vmlinuz-"$(uname -r)" /boot/vmlinuz; do + if [[ -f "$k" ]]; then + KERNEL="$k" + break + fi +done + +if [[ -z "$KERNEL" ]]; then + echo "No kernel found for direct VM boot, skipping" + exit 0 +fi + +WORKDIR="$(mktemp -d /tmp/test-bind-volume.XXXXXXXXXX)" + +at_exit() { + set +e + if [[ -n "${MACHINE:-}" ]]; then + if machinectl status "$MACHINE" &>/dev/null; then + machinectl terminate "$MACHINE" 2>/dev/null + timeout 10 bash -c "while machinectl status '$MACHINE' &>/dev/null; do sleep .5; done" 2>/dev/null + fi + fi + [[ -n "${VMSPAWN_PID:-}" ]] && { kill "$VMSPAWN_PID" 2>/dev/null; wait "$VMSPAWN_PID" 2>/dev/null; } + rm -rf "$WORKDIR" + rm -f /var/lib/storage/test-bind-volume-*.volume +} +trap at_exit EXIT + +# Build a minimal root for direct boot — guest just sleeps. +mkdir -p "$WORKDIR/rootfs/sbin" +cat >"$WORKDIR/rootfs/sbin/init" <<'INITEOF' +#!/bin/sh +exec sleep infinity +INITEOF +chmod +x "$WORKDIR/rootfs/sbin/init" + +truncate -s 256M "$WORKDIR/root.raw" +mke2fs -t ext4 -q -d "$WORKDIR/rootfs" "$WORKDIR/root.raw" + +BOOT_VOL="test-bind-volume-boot-$$" +RUNTIME_VOL="test-bind-volume-runtime-$$" + +wait_for_machine() { + local machine="$1" pid="$2" log="$3" + timeout 30 bash -c " + while ! machinectl list --no-legend 2>/dev/null | grep >/dev/null '$machine'; do + if ! kill -0 $pid 2>/dev/null; then + echo 'vmspawn exited before machine registration' + cat '$log' + exit 77 + fi + sleep .5 + done + " || { + local rc=$? + if [[ $rc -eq 77 ]]; then exit 0; fi + exit "$rc" + } +} + +# --- Boot the VM with one boot-time bind-volume --- +MACHINE="test-bind-volume-$$" +systemd-vmspawn \ + --machine="$MACHINE" \ + --ram=256M \ + --image="$WORKDIR/root.raw" \ + --bind-volume="fs:${BOOT_VOL}::create=new,size=64M,template=sparse-file" \ + --linux="$KERNEL" \ + --tpm=no \ + --console=headless \ + root=/dev/vda rw \ + &>"$WORKDIR/vmspawn.log" & +VMSPAWN_PID=$! + +wait_for_machine "$MACHINE" "$VMSPAWN_PID" "$WORKDIR/vmspawn.log" +echo "Machine '$MACHINE' registered" + +VARLINK_ADDR=$(varlinkctl call /run/systemd/machine/io.systemd.Machine \ + io.systemd.Machine.List "{\"name\":\"$MACHINE\"}" | jq -r '.controlAddress') +assert_neq "$VARLINK_ADDR" "null" + +varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Describe '{}' \ + | jq -e '.running == true' >/dev/null +echo "VM running with boot-time bind-volume attached" + +# --- Hot-add a second volume via machinectl bind-volume (must succeed) --- +machinectl bind-volume "$MACHINE" \ + "fs:${RUNTIME_VOL}:virtio-scsi:create=new,size=32M,template=sparse-file" +echo "Hot-added runtime bind-volume succeeded" + +# --- Hot-remove the runtime-added volume (must succeed) --- +machinectl unbind-volume "$MACHINE" "fs:${RUNTIME_VOL}" +echo "Hot-removed runtime bind-volume succeeded" + +# --- Removing the boot-time volume must fail with StorageImmutable --- +if machinectl unbind-volume "$MACHINE" "fs:${BOOT_VOL}" 2>"$WORKDIR/unbind.err"; then + echo "ERROR: unbind-volume of boot-time volume should have failed" + cat "$WORKDIR/unbind.err" + exit 1 +fi +grep StorageImmutable "$WORKDIR/unbind.err" >/dev/null +echo "Boot-time bind-volume correctly rejected with StorageImmutable" + +# --- Removing a non-existent name must fail with NoSuchStorage --- +if machinectl unbind-volume "$MACHINE" "fs:no-such-volume-$$" 2>"$WORKDIR/unbind-noexist.err"; then + echo "ERROR: unbind-volume of non-existent name should have failed" + cat "$WORKDIR/unbind-noexist.err" + exit 1 +fi +grep NoSuchStorage "$WORKDIR/unbind-noexist.err" >/dev/null +echo "Non-existent unbind-volume correctly rejected with NoSuchStorage" + +machinectl terminate "$MACHINE" +timeout 10 bash -c "while machinectl status '$MACHINE' &>/dev/null; do sleep .5; done" +timeout 10 bash -c "while kill -0 '$VMSPAWN_PID' 2>/dev/null; do sleep .5; done" +echo "All bind-volume tests passed"