* [PATCH qemu-server 0/6] add hotplug tests and fix uncovered bugs
@ 2026-09-10 11:00 Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 1/6] tests: hotplug: add initial hotplug test harness Dominik Csapak
` (5 more replies)
0 siblings, 6 replies; 7+ messages in thread
From: Dominik Csapak @ 2026-09-10 11:00 UTC (permalink / raw)
To: pve-devel
Recently it was uncovered that hotplugging scsiX >= 14 does not work
in all situations. Inspired by this, this attempts to add some hotplug
regression tests by simulating the qmp/hmp environment, recording the
calls and testing against some invariants.
This uncovered two additional bugs in the hotplug code:
* hotplugging a tablet device into an aarch64 vm fails
* failure to hotplug a disk with iothreds leaves the iothread behind
The series first adds the harness + refactor of shared code from the
cfg2cmd tests, then adds some tests that behave ok, then adds some
tests that show some defect and finally it fixes the two mentioned bugs.
If wanted i can fix the bugs and add the relevant regression test in one
step, but this way it is much more visible how they fail.
Dominik Csapak (6):
tests: hotplug: add initial hotplug test harness
tests: hotplug: add some test cases
tests: hotplug: add cases for known defects
tests: hotplug: add test case for adding scsi14 on qemu 11.1
hotplug: fix vm_deviceplug call for 'tablet' and 'keyboard' on aarch64
hotplug: remove iothread if adding drive device failed
src/PVE/QemuServer.pm | 29 +-
src/test/CommandLineMocks.pm | 556 +++++++++++++
src/test/Makefile | 12 +-
.../aarch64/disk-add-default-machine.conf | 20 +
.../disk-add-default-machine.conf.expected | 28 +
src/test/hotplug/aarch64/disk-add-virtio.conf | 20 +
.../aarch64/disk-add-virtio.conf.expected | 29 +
.../hotplug/aarch64/net-add-pci-bridge.conf | 20 +
.../aarch64/net-add-pci-bridge.conf.expected | 28 +
src/test/hotplug/aarch64/net-add.conf | 20 +
.../hotplug/aarch64/net-add.conf.expected | 28 +
src/test/hotplug/aarch64/tablet-disable.conf | 20 +
.../aarch64/tablet-disable.conf.expected | 25 +
src/test/hotplug/aarch64/tablet-enable.conf | 21 +
.../aarch64/tablet-enable.conf.expected | 25 +
.../hotplug/aarch64/x86-guest-net-add.conf | 18 +
.../aarch64/x86-guest-net-add.conf.expected | 26 +
src/test/hotplug/agent-fstrim.conf | 17 +
src/test/hotplug/agent-fstrim.conf.expected | 20 +
src/test/hotplug/balloon.conf | 18 +
src/test/hotplug/balloon.conf.expected | 22 +
src/test/hotplug/cdrom-eject.conf | 17 +
src/test/hotplug/cdrom-eject.conf.expected | 24 +
src/test/hotplug/cpu-add-too-many.conf | 18 +
.../hotplug/cpu-add-too-many.conf.expected | 25 +
src/test/hotplug/cpu-add.conf | 18 +
src/test/hotplug/cpu-add.conf.expected | 23 +
src/test/hotplug/cpu-remove.conf | 18 +
src/test/hotplug/cpu-remove.conf.expected | 23 +
src/test/hotplug/cpulimit.conf | 16 +
src/test/hotplug/cpulimit.conf.expected | 21 +
.../hotplug/disk-add-default-machine.conf | 16 +
.../disk-add-default-machine.conf.expected | 25 +
src/test/hotplug/disk-add-fail.conf | 17 +
src/test/hotplug/disk-add-fail.conf.expected | 32 +
src/test/hotplug/disk-add-legacy-drive.conf | 16 +
.../disk-add-legacy-drive.conf.expected | 23 +
src/test/hotplug/disk-add-scsi-single.conf | 16 +
.../disk-add-scsi-single.conf.expected | 27 +
src/test/hotplug/disk-add-scsi14-old.conf | 15 +
.../hotplug/disk-add-scsi14-old.conf.expected | 23 +
.../hotplug/disk-add-scsi14-qemu11.1.conf | 15 +
.../disk-add-scsi14-qemu11.1.conf.expected | 24 +
src/test/hotplug/disk-add-virtio.conf | 16 +
.../hotplug/disk-add-virtio.conf.expected | 26 +
src/test/hotplug/disk-remove-bootdisk.conf | 16 +
.../disk-remove-bootdisk.conf.expected | 23 +
src/test/hotplug/disk-remove-busy.conf | 18 +
.../hotplug/disk-remove-busy.conf.expected | 25 +
src/test/hotplug/disk-remove-scsi-single.conf | 17 +
.../disk-remove-scsi-single.conf.expected | 25 +
src/test/hotplug/disk-remove-virtio.conf | 17 +
.../hotplug/disk-remove-virtio.conf.expected | 24 +
src/test/hotplug/disk-skip-ide.conf | 16 +
src/test/hotplug/disk-skip-ide.conf.expected | 22 +
src/test/hotplug/disk-throttle.conf | 16 +
src/test/hotplug/disk-throttle.conf.expected | 20 +
src/test/hotplug/fast-plug.conf | 17 +
src/test/hotplug/fast-plug.conf.expected | 20 +
src/test/hotplug/memory-add.conf | 18 +
src/test/hotplug/memory-add.conf.expected | 25 +
src/test/hotplug/memory-remove.conf | 18 +
src/test/hotplug/memory-remove.conf.expected | 25 +
src/test/hotplug/net-add-fail.conf | 17 +
src/test/hotplug/net-add-fail.conf.expected | 28 +
src/test/hotplug/net-add-pci-bridge.conf | 16 +
.../hotplug/net-add-pci-bridge.conf.expected | 25 +
src/test/hotplug/net-add.conf | 16 +
src/test/hotplug/net-add.conf.expected | 25 +
src/test/hotplug/net-change-bridge.conf | 16 +
.../hotplug/net-change-bridge.conf.expected | 25 +
src/test/hotplug/net-change-model.conf | 16 +
.../hotplug/net-change-model.conf.expected | 27 +
src/test/hotplug/net-change-rate.conf | 16 +
.../hotplug/net-change-rate.conf.expected | 20 +
src/test/hotplug/net-hotplug-disabled.conf | 17 +
.../net-hotplug-disabled.conf.expected | 23 +
src/test/hotplug/net-remove.conf | 17 +
src/test/hotplug/net-remove.conf.expected | 22 +
src/test/hotplug/tablet-disable.conf | 16 +
src/test/hotplug/tablet-disable.conf.expected | 21 +
src/test/hotplug/tablet-enable.conf | 17 +
src/test/hotplug/tablet-enable.conf.expected | 21 +
src/test/hotplug/usb-add-spice.conf | 17 +
src/test/hotplug/usb-add-spice.conf.expected | 23 +
src/test/hotplug/usb-add.conf | 16 +
src/test/hotplug/usb-add.conf.expected | 22 +
src/test/hotplug/usb-remove-last.conf | 17 +
.../hotplug/usb-remove-last.conf.expected | 21 +
src/test/run_config2command_tests.pl | 552 +------------
src/test/run_hotplug_tests.pl | 780 ++++++++++++++++++
91 files changed, 3160 insertions(+), 547 deletions(-)
create mode 100644 src/test/CommandLineMocks.pm
create mode 100644 src/test/hotplug/aarch64/disk-add-default-machine.conf
create mode 100644 src/test/hotplug/aarch64/disk-add-default-machine.conf.expected
create mode 100644 src/test/hotplug/aarch64/disk-add-virtio.conf
create mode 100644 src/test/hotplug/aarch64/disk-add-virtio.conf.expected
create mode 100644 src/test/hotplug/aarch64/net-add-pci-bridge.conf
create mode 100644 src/test/hotplug/aarch64/net-add-pci-bridge.conf.expected
create mode 100644 src/test/hotplug/aarch64/net-add.conf
create mode 100644 src/test/hotplug/aarch64/net-add.conf.expected
create mode 100644 src/test/hotplug/aarch64/tablet-disable.conf
create mode 100644 src/test/hotplug/aarch64/tablet-disable.conf.expected
create mode 100644 src/test/hotplug/aarch64/tablet-enable.conf
create mode 100644 src/test/hotplug/aarch64/tablet-enable.conf.expected
create mode 100644 src/test/hotplug/aarch64/x86-guest-net-add.conf
create mode 100644 src/test/hotplug/aarch64/x86-guest-net-add.conf.expected
create mode 100644 src/test/hotplug/agent-fstrim.conf
create mode 100644 src/test/hotplug/agent-fstrim.conf.expected
create mode 100644 src/test/hotplug/balloon.conf
create mode 100644 src/test/hotplug/balloon.conf.expected
create mode 100644 src/test/hotplug/cdrom-eject.conf
create mode 100644 src/test/hotplug/cdrom-eject.conf.expected
create mode 100644 src/test/hotplug/cpu-add-too-many.conf
create mode 100644 src/test/hotplug/cpu-add-too-many.conf.expected
create mode 100644 src/test/hotplug/cpu-add.conf
create mode 100644 src/test/hotplug/cpu-add.conf.expected
create mode 100644 src/test/hotplug/cpu-remove.conf
create mode 100644 src/test/hotplug/cpu-remove.conf.expected
create mode 100644 src/test/hotplug/cpulimit.conf
create mode 100644 src/test/hotplug/cpulimit.conf.expected
create mode 100644 src/test/hotplug/disk-add-default-machine.conf
create mode 100644 src/test/hotplug/disk-add-default-machine.conf.expected
create mode 100644 src/test/hotplug/disk-add-fail.conf
create mode 100644 src/test/hotplug/disk-add-fail.conf.expected
create mode 100644 src/test/hotplug/disk-add-legacy-drive.conf
create mode 100644 src/test/hotplug/disk-add-legacy-drive.conf.expected
create mode 100644 src/test/hotplug/disk-add-scsi-single.conf
create mode 100644 src/test/hotplug/disk-add-scsi-single.conf.expected
create mode 100644 src/test/hotplug/disk-add-scsi14-old.conf
create mode 100644 src/test/hotplug/disk-add-scsi14-old.conf.expected
create mode 100644 src/test/hotplug/disk-add-scsi14-qemu11.1.conf
create mode 100644 src/test/hotplug/disk-add-scsi14-qemu11.1.conf.expected
create mode 100644 src/test/hotplug/disk-add-virtio.conf
create mode 100644 src/test/hotplug/disk-add-virtio.conf.expected
create mode 100644 src/test/hotplug/disk-remove-bootdisk.conf
create mode 100644 src/test/hotplug/disk-remove-bootdisk.conf.expected
create mode 100644 src/test/hotplug/disk-remove-busy.conf
create mode 100644 src/test/hotplug/disk-remove-busy.conf.expected
create mode 100644 src/test/hotplug/disk-remove-scsi-single.conf
create mode 100644 src/test/hotplug/disk-remove-scsi-single.conf.expected
create mode 100644 src/test/hotplug/disk-remove-virtio.conf
create mode 100644 src/test/hotplug/disk-remove-virtio.conf.expected
create mode 100644 src/test/hotplug/disk-skip-ide.conf
create mode 100644 src/test/hotplug/disk-skip-ide.conf.expected
create mode 100644 src/test/hotplug/disk-throttle.conf
create mode 100644 src/test/hotplug/disk-throttle.conf.expected
create mode 100644 src/test/hotplug/fast-plug.conf
create mode 100644 src/test/hotplug/fast-plug.conf.expected
create mode 100644 src/test/hotplug/memory-add.conf
create mode 100644 src/test/hotplug/memory-add.conf.expected
create mode 100644 src/test/hotplug/memory-remove.conf
create mode 100644 src/test/hotplug/memory-remove.conf.expected
create mode 100644 src/test/hotplug/net-add-fail.conf
create mode 100644 src/test/hotplug/net-add-fail.conf.expected
create mode 100644 src/test/hotplug/net-add-pci-bridge.conf
create mode 100644 src/test/hotplug/net-add-pci-bridge.conf.expected
create mode 100644 src/test/hotplug/net-add.conf
create mode 100644 src/test/hotplug/net-add.conf.expected
create mode 100644 src/test/hotplug/net-change-bridge.conf
create mode 100644 src/test/hotplug/net-change-bridge.conf.expected
create mode 100644 src/test/hotplug/net-change-model.conf
create mode 100644 src/test/hotplug/net-change-model.conf.expected
create mode 100644 src/test/hotplug/net-change-rate.conf
create mode 100644 src/test/hotplug/net-change-rate.conf.expected
create mode 100644 src/test/hotplug/net-hotplug-disabled.conf
create mode 100644 src/test/hotplug/net-hotplug-disabled.conf.expected
create mode 100644 src/test/hotplug/net-remove.conf
create mode 100644 src/test/hotplug/net-remove.conf.expected
create mode 100644 src/test/hotplug/tablet-disable.conf
create mode 100644 src/test/hotplug/tablet-disable.conf.expected
create mode 100644 src/test/hotplug/tablet-enable.conf
create mode 100644 src/test/hotplug/tablet-enable.conf.expected
create mode 100644 src/test/hotplug/usb-add-spice.conf
create mode 100644 src/test/hotplug/usb-add-spice.conf.expected
create mode 100644 src/test/hotplug/usb-add.conf
create mode 100644 src/test/hotplug/usb-add.conf.expected
create mode 100644 src/test/hotplug/usb-remove-last.conf
create mode 100644 src/test/hotplug/usb-remove-last.conf.expected
create mode 100644 src/test/run_hotplug_tests.pl
--
2.47.3
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH qemu-server 1/6] tests: hotplug: add initial hotplug test harness
2026-09-10 11:00 [PATCH qemu-server 0/6] add hotplug tests and fix uncovered bugs Dominik Csapak
@ 2026-09-10 11:00 ` Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 2/6] tests: hotplug: add some test cases Dominik Csapak
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2026-09-10 11:00 UTC (permalink / raw)
To: pve-devel
Add a new test harness for testing the hotplug mechanism. A test case
consists of failure modes and a vm config with a [PENDING] section.
After setting up the initial device list via config_to_command, the
pending section is applied and all qmp/hmp/etc. calls are recorded and
compared with the expected file for each test.
In addition, there are certain constraints tested, like hotplugging into
a hotplugged bridge fails, a bus is not hotpluggable, or the device
already exists.
Note that errors in tests don't let the tests fail, but are recorded
in the output so that we can test behavior that we might need to
keep to preserve backwards compatibility.
After hotplugging, the resulting config is checked again with
config_to_command and the device lists are compared (any difference here
could lead to a resource leak and/or live migration failure).
The harness is modeled after cfg2cmd. It also contains cases for x86 and
aarch64 host architectures.
Since most mocks and setup steps overlap with the cfg2cmd tests, factor
out the relevant code into a separate module that be can used by both.
The exact setup and diff code change slightly, but the comparison and
output should behave the same.
No test cases are added yet, these come in separate patches.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/test/CommandLineMocks.pm | 556 +++++++++++++++++++
src/test/Makefile | 6 +-
src/test/run_config2command_tests.pl | 552 +------------------
src/test/run_hotplug_tests.pl | 780 +++++++++++++++++++++++++++
4 files changed, 1354 insertions(+), 540 deletions(-)
create mode 100644 src/test/CommandLineMocks.pm
create mode 100644 src/test/run_hotplug_tests.pl
diff --git a/src/test/CommandLineMocks.pm b/src/test/CommandLineMocks.pm
new file mode 100644
index 00000000..a5883073
--- /dev/null
+++ b/src/test/CommandLineMocks.pm
@@ -0,0 +1,556 @@
+package CommandLineMocks;
+
+# Mocks for generating the QEMU command line of a VM config in tests, shared
+# between the test scripts for the command line itself and for hotplugging. The
+# environment (storage configuration, PCI devices, mappings, ...) is fixed,
+# while a few properties of the host can be set per test via set_test_env().
+# Also contains helpers that are useful and needed by multiple test harnesses.
+
+use v5.36;
+
+use JSON qw(decode_json);
+use Socket qw(AF_INET AF_INET6);
+use Test::MockModule;
+
+use PVE::INotify;
+use PVE::Mapping::PCI;
+use PVE::Mapping::USB;
+use PVE::ProcFSTools;
+use PVE::QemuServer::CPUConfig;
+use PVE::QemuServer::Drive;
+use PVE::QemuServer::Helpers;
+use PVE::QemuServer::Memory;
+use PVE::QemuServer::OVMF;
+use PVE::QemuServer::PCI;
+use PVE::QemuServer;
+use PVE::Storage::RBDPlugin;
+use PVE::Storage::ZFSPlugin;
+use PVE::Storage;
+use PVE::SysFSTools;
+use PVE::Tools qw(run_command);
+
+use base 'Exporter';
+
+our @EXPORT_OK = qw(
+ get_storage_config
+ get_test_qemu_version
+ set_test_env
+ diff
+);
+
+my $real_qemu_version = PVE::QemuServer::Helpers::kvm_user_version(); # not yet mocked
+
+# the properties of the host for the current test, see set_test_env()
+my $test_env = {};
+
+# The mocker objects need to stay alive for the mocks to stay in effect. File-scoped lexicals of a
+# module are freed after loading it, so package variables are used for them.
+
+my $storage_config = {
+ ids => {
+ local => {
+ content => {
+ images => 1,
+ iso => 1,
+ },
+ path => '/var/lib/vz',
+ type => 'dir',
+ shared => 0,
+ },
+ localsnapext => {
+ content => {
+ images => 1,
+ },
+ path => '/var/lib/vzsnapext',
+ type => 'dir',
+ shared => 0,
+ 'snapshot-as-volume-chain' => 1,
+ },
+ noimages => {
+ content => {
+ iso => 1,
+ },
+ path => '/var/lib/vz',
+ type => 'dir',
+ },
+ 'btrfs-store' => {
+ content => {
+ images => 1,
+ },
+ path => '/butter/bread',
+ type => 'btrfs',
+ },
+ 'cifs-store' => {
+ shared => 1,
+ path => '/mnt/pve/cifs-store',
+ username => 'guest',
+ server => '127.0.0.42',
+ type => 'cifs',
+ share => 'CIFShare',
+ content => {
+ images => 1,
+ iso => 1,
+ },
+ },
+ 'rbd-store' => {
+ monhost => '127.0.0.42,127.0.0.21,::1',
+ fsid => 'fc4181a6-56eb-4f68-b452-8ba1f381ca2a',
+ content => {
+ images => 1,
+ },
+ type => 'rbd',
+ pool => 'cpool',
+ username => 'admin',
+ shared => 1,
+ },
+ 'krbd-store' => {
+ monhost => '127.0.0.42,127.0.0.21,::1',
+ fsid => 'fc4181a6-56eb-4f68-b452-8ba1f381ca2a',
+ content => {
+ images => 1,
+ },
+ type => 'rbd',
+ pool => 'cpool',
+ username => 'admin',
+ shared => 1,
+ krbd => 1,
+ },
+ 'zfs-over-iscsi-store' => {
+ type => 'zfs',
+ iscsiprovider => "comstar",
+ lio_tpg => "tpg1",
+ portal => "127.0.0.1",
+ target => "iqn.2019-10.org.test:foobar",
+ pool => "tank",
+ content => {
+ images => 1,
+ },
+ },
+ 'lvm-store' => {
+ vgname => 'veegee',
+ type => 'lvm',
+ content => {
+ images => 1,
+ },
+ },
+ 'local-lvm' => {
+ vgname => 'pve',
+ bwlimit => 'restore=1024',
+ type => 'lvmthin',
+ thinpool => 'data',
+ content => {
+ images => 1,
+ },
+ },
+ },
+};
+
+my $pci_devs = [
+ "0000:00:02.0",
+ "0000:00:43.1",
+ "0000:00:f4.0",
+ "0000:00:ff.1",
+ "0000:0f:f2.0",
+ "0000:d0:13.0",
+ "0000:d0:15.1",
+ "0000:d0:15.2",
+ "0000:d0:17.0",
+ "0000:f0:42.0",
+ "0000:f0:43.0",
+ "0000:f0:43.1",
+ "1234:f0:43.1",
+ "0000:01:00.4",
+ "0000:01:00.5",
+ "0000:01:00.6",
+ "0000:07:10.0",
+ "0000:07:10.1",
+ "0000:07:10.4",
+];
+
+my $pci_map_config = {
+ ids => {
+ someGpu => {
+ type => 'pci',
+ mdev => 1,
+ map => [
+ 'node=localhost,path=0000:01:00.4,id=10de:2231,iommugroup=1',
+ 'node=localhost,path=0000:01:00.5,id=10de:2231,iommugroup=1',
+ 'node=localhost,path=0000:01:00.6,id=10de:2231,iommugroup=1',
+ ],
+ },
+ someNic => {
+ type => 'pci',
+ map => [
+ 'node=localhost,path=0000:07:10.0,id=8086:1520,iommugroup=2',
+ 'node=localhost,path=0000:07:10.1,id=8086:1520,iommugroup=2',
+ 'node=localhost,path=0000:07:10.4,id=8086:1520,iommugroup=2',
+ ],
+ },
+ },
+};
+
+my $usb_map_config = {};
+
+my $cpu_hw_capabilities = {
+ # Gathered from an AMD EPYC 9475F running kernel 6.11.11-2-pve
+ 'amd-turin-9005' =>
+ '{ "amd-sev": { "cbitpos": 51, "reduced-phys-bits": 6, "sev-support": true,'
+ . ' "sev-support-es": true, "sev-support-snp": true } }',
+ # TODO: others?
+};
+
+# Set the properties of the (mocked) host for the following test, fields are:
+# qemu_version: version of the installed QEMU binary (defaults to the real version)
+# host_arch: x86_64 | aarch64 (defaults to x86_64)
+# host-cpu-vendor: AuthenticAMD | GenuineIntel | etc. (defaults to GenuineIntel)
+# hw_capabilities: a CPU from $cpu_hw_capabilities above or a raw JSON string
+sub set_test_env($env) {
+ $test_env = $env;
+}
+
+sub get_test_qemu_version() {
+ return $test_env->{qemu_version} // $real_qemu_version // '2.12';
+}
+
+sub get_storage_config() {
+ return $storage_config;
+}
+
+our $procfs_tools_module = Test::MockModule->new('PVE::ProcFSTools');
+$procfs_tools_module->mock(
+ read_cpuinfo => sub {
+ my $res = $procfs_tools_module->original('read_cpuinfo')->();
+ my $vendor = $test_env->{'host-cpu-vendor'} // 'GenuineIntel';
+ $res->{vendor} = $vendor;
+ return $res;
+ },
+);
+
+our $qemu_server_module;
+$qemu_server_module = Test::MockModule->new('PVE::QemuServer');
+$qemu_server_module->mock(
+ kvm_user_version => sub {
+ return get_test_qemu_version();
+ },
+ kvm_version => sub {
+ return get_test_qemu_version();
+ },
+ kernel_has_vhost_net => sub {
+ return 1; # TODO: make this per-test configurable?
+ },
+ get_iscsi_initiator_name => sub {
+ return 'iqn.1993-08.org.debian:01:aabbccddeeff';
+ },
+ cleanup_pci_devices => {
+ # do nothing
+ },
+);
+
+our $qemu_server_ovmf_module = Test::MockModule->new("PVE::QemuServer::OVMF");
+$qemu_server_ovmf_module->mock(
+ file_exists => sub {
+ my ($path) = @_;
+ return 1;
+ },
+ file_get_size => sub {
+ my ($path) = @_;
+ if ($path =~ m/OVMF(32)?_(SEV_)?VARS_4M/) {
+ return 528 * 1024;
+ } elsif ($path =~ m/OVMF_VARS/) {
+ return 128 * 1024;
+ } elsif ($path =~ m/AAVMF_VARS/) {
+ return 64 * 1024 * 1024;
+ } elsif ($path =~ m/RISCV_VIRT_VARS/) {
+ return 32 * 1024 * 1024;
+ } else {
+ die "unknown ovmf vars image '$path' - implement me";
+ }
+ },
+);
+
+our $storage_module = Test::MockModule->new("PVE::Storage");
+$storage_module->mock(
+ activate_volumes => sub {
+ return;
+ },
+ deactivate_volumes => sub {
+ return;
+ },
+ volume_snapshot_info => sub {
+ my ($cfg, $volid) = @_;
+
+ my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
+
+ my $snapshots = {};
+ if ($storeid eq 'localsnapext') {
+ $snapshots = {
+ current => {
+ file => 'var/lib/vzsnapext/images/8006/vm-8006-disk-0.qcow2',
+ parent => 'snap2',
+ },
+ snap2 => {
+ file => '/var/lib/vzsnapext/images/8006/snap2-vm-8006-disk-0.qcow2',
+ parent => 'snap1',
+ },
+ snap1 => {
+ file => '/var/lib/vzsnapext/images/8006/snap1-vm-8006-disk-0.qcow2',
+ },
+ };
+ } elsif ($storeid eq 'lvm-store') {
+ $snapshots = {
+ current => {
+ file => '/dev/veegee/vm-8006-disk-0.qcow2',
+ parent => 'snap2',
+ },
+ snap2 => {
+ file => '/dev/veegee/snap2-vm-8006-disk-0.qcow2',
+ parent => 'snap1',
+ },
+ snap1 => {
+ file => '/dev/veegee/snap1-vm-8006-disk-0.qcow2',
+ },
+ };
+ }
+ return $snapshots;
+ },
+);
+
+our $file_stat_module = Test::MockModule->new("File::stat");
+$file_stat_module->mock(
+ stat => sub {
+ my ($path) = @_;
+ my $st = $file_stat_module->original('stat')->($0);
+ $st->[2] = 25008 if $path =~ m!/dev/!; # block device
+ return $st;
+ },
+);
+
+our $zfsplugin_module = Test::MockModule->new("PVE::Storage::ZFSPlugin");
+$zfsplugin_module->mock(
+ zfs_get_lu_name => sub {
+ return "foobar";
+ },
+ zfs_get_lun_number => sub {
+ return "0";
+ },
+);
+
+our $rbdplugin_module = Test::MockModule->new("PVE::Storage::RBDPlugin");
+$rbdplugin_module->mock(
+ rbd_volume_config_set => sub {
+ return;
+ },
+);
+
+our $qemu_server_helpers;
+$qemu_server_helpers = Test::MockModule->new('PVE::QemuServer::Helpers');
+$qemu_server_helpers->mock(
+ get_host_phys_address_bits => sub {
+ return 46;
+ },
+);
+
+our $qemu_server_memory;
+$qemu_server_memory = Test::MockModule->new('PVE::QemuServer::Memory');
+$qemu_server_memory->mock(
+ hugepages_chunk_size_supported => sub {
+ return 1;
+ },
+ host_numanode_exists => sub {
+ my ($id) = @_;
+ return 1;
+ },
+);
+
+our $pve_common_tools;
+$pve_common_tools = Test::MockModule->new('PVE::Tools');
+$pve_common_tools->mock(
+ next_vnc_port => sub {
+ my ($family, $address) = @_;
+
+ return '5900';
+ },
+ next_spice_port => sub {
+ my ($family, $address) = @_;
+
+ return '61000';
+ },
+ getaddrinfo_all => sub {
+ my ($hostname, @opts) = @_;
+ die "need stable hostname" if $hostname ne 'localhost';
+ return (
+ {
+ addr => Socket::pack_sockaddr_in(0, Socket::INADDR_LOOPBACK),
+ family => AF_INET, # IPv4
+ protocol => 6,
+ socktype => 1,
+ },
+ );
+ },
+ get_host_arch => sub {
+ return $test_env->{host_arch} // 'x86_64';
+ },
+);
+
+our $pve_common_file;
+$pve_common_file = Test::MockModule->new('PVE::File');
+$pve_common_file->mock(
+ file_copy => sub {
+ my ($filename, $dst, $max, $perm) = @_;
+ if ($dst =~ m|^/run/qemu-server/efidisk|) {
+ return;
+ }
+ return $pve_common_file->original('file_copy')->($filename, $dst, $max, $perm);
+ },
+);
+
+our $pve_cpuconfig;
+$pve_cpuconfig = Test::MockModule->new('PVE::QemuServer::CPUConfig');
+$pve_cpuconfig->mock(
+ load_custom_cpu_model_config => sub {
+ # mock custom CPU model config
+ return PVE::QemuServer::CPUConfig->parse_config(
+ "cpu-models.conf",
+ <<EOF,
+
+# "qemu64" is also a default CPU, used here to test that this doesn't matter
+cpu-model: qemu64
+ reported-model athlon
+ flags +aes;+avx;-kvm_pv_unhalt
+ hv-vendor-id testvend
+ phys-bits 40
+
+cpu-model: alldefault
+
+EOF
+ );
+ },
+ get_hw_capabilities => sub {
+ my $hw_capabilities_raw;
+ if (!defined($test_env->{hw_capabilities})) {
+ # default to barebone uncapable HW
+ $hw_capabilities_raw =
+ '{"amd-sev":{"cbitpos":0,"reduced-phys-bits":0,"sev-support":false,'
+ . '"sev-support-es":false,"sev-support-snp":false}}';
+ } elsif (my $cpu_hw_caps = $cpu_hw_capabilities->{ lc($test_env->{hw_capabilities}) }) {
+ $hw_capabilities_raw = $cpu_hw_caps;
+ } else {
+ $hw_capabilities_raw = $test_env->{hw_capabilities};
+ }
+
+ my $hw_capabilities = decode_json($hw_capabilities_raw);
+ return $hw_capabilities;
+ },
+);
+
+our $pve_common_network;
+$pve_common_network = Test::MockModule->new('PVE::Network');
+$pve_common_network->mock(
+ read_bridge_mtu => sub {
+ my ($bridge_name) = @_;
+
+ if ($bridge_name eq 'vxlan_bridge') {
+ return 1450;
+ }
+
+ return 1500;
+ },
+);
+
+our $pve_common_inotify;
+$pve_common_inotify = Test::MockModule->new('PVE::INotify');
+$pve_common_inotify->mock(
+ nodename => sub {
+ return 'localhost';
+ },
+);
+
+our $pve_common_sysfstools;
+$pve_common_sysfstools = Test::MockModule->new('PVE::SysFSTools');
+$pve_common_sysfstools->mock(
+ lspci => sub {
+ my ($filter, $verbose) = @_;
+
+ return [
+ map { { id => $_ } }
+ grep {
+ !defined($filter)
+ || (!ref($filter) && $_ =~ m/^(0000:)?\Q$filter\E/)
+ || (ref($filter) eq 'CODE' && $filter->({ id => $_ }))
+ } sort @$pci_devs
+ ];
+ },
+ pci_device_info => sub {
+ my ($path, $noerr) = @_;
+
+ if ($path =~ m/^0000:01:00/) {
+ return {
+ mdev => 1,
+ iommugroup => 1,
+ mdev => 1,
+ vendor => "0x10de",
+ device => "0x2231",
+ };
+ } elsif ($path =~ m/^0000:07:10/) {
+ return {
+ iommugroup => 2,
+ vendor => "0x8086",
+ device => "0x1520",
+ };
+ } else {
+ return {};
+ }
+ },
+);
+
+our $qemu_drive_module;
+$qemu_drive_module = Test::MockModule->new('PVE::QemuServer::Drive');
+$qemu_drive_module->mock(
+ get_cdrom_path => sub {
+ return "/dev/cdrom";
+ },
+);
+
+our $mapping_usb_module = Test::MockModule->new("PVE::Mapping::USB");
+$mapping_usb_module->mock(
+ config => sub {
+ return $usb_map_config;
+ },
+);
+
+our $mapping_pci_module = Test::MockModule->new("PVE::Mapping::PCI");
+$mapping_pci_module->mock(
+ config => sub {
+ return $pci_map_config;
+ },
+);
+
+our $pci_module = Test::MockModule->new("PVE::QemuServer::PCI");
+$pci_module->mock(
+ reserve_pci_usage => sub {
+ die "reserve_pci_usage should not be called for 'qm showcmd'\n";
+ },
+ create_nvidia_device => sub {
+ die "create_nvidia_device should not be called for 'qm showcmd'\n";
+ },
+);
+
+sub diff($expected, $got) {
+ return if $expected eq $got;
+
+ my $tmp = File::Temp->new();
+ print $tmp $expected;
+ $tmp->flush();
+
+ my $diff = '';
+ run_command(
+ ['diff', '-up', '--label', 'expected', $tmp->filename(), '--label', 'got', '-'],
+ input => $got,
+ outfunc => sub { $diff .= "$_[0]\n"; },
+ noerr => 1,
+ );
+
+ return $diff;
+}
+
+1;
diff --git a/src/test/Makefile b/src/test/Makefile
index cf589f41..b539a6b7 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -1,15 +1,15 @@
all: test
-test: test_snapshot test_cfg_to_cmd test_cfg_to_cmd_aarch64 test_pci_addr_conflicts test_pci_reservation test_qemu_img_convert test_migration test_restore_config test_parse_config
+test: test_snapshot test_cfg_to_cmd test_cfg_to_cmd_aarch64 test_hotplug test_hotplug_aarch64 test_pci_addr_conflicts test_pci_reservation test_qemu_img_convert test_migration test_restore_config test_parse_config
test_snapshot: run_snapshot_tests.pl
./run_snapshot_tests.pl
./test_get_replicatable_volumes.pl
-test_cfg_to_cmd: run_config2command_tests.pl cfg2cmd/*.conf
+test_cfg_to_cmd: run_config2command_tests.pl CommandLineMocks.pm cfg2cmd/*.conf
perl -I../ ./run_config2command_tests.pl
-test_cfg_to_cmd_aarch64: run_config2command_tests.pl cfg2cmd/aarch64/*.conf
+test_cfg_to_cmd_aarch64: run_config2command_tests.pl CommandLineMocks.pm cfg2cmd/aarch64/*.conf
perl -I../ ./run_config2command_tests.pl cfg2cmd/aarch64
test_qemu_img_convert: run_qemu_img_convert_tests.pl
diff --git a/src/test/run_config2command_tests.pl b/src/test/run_config2command_tests.pl
index 28f538d3..26da57f0 100755
--- a/src/test/run_config2command_tests.pl
+++ b/src/test/run_config2command_tests.pl
@@ -2,184 +2,22 @@
use v5.36;
-use lib qw(..);
+use lib qw(.. .);
-use JSON qw(decode_json);
use Test::More;
use Test::MockModule;
-use Socket qw(AF_INET AF_INET6);
use PVE::File qw(file_get_contents file_set_contents);
-use PVE::Tools qw(run_command);
-use PVE::INotify;
-use PVE::SysFSTools;
-
use PVE::QemuConfig;
use PVE::QemuServer;
-use PVE::QemuServer::Drive;
-use PVE::QemuServer::Helpers;
use PVE::QemuServer::Monitor;
-use PVE::QemuServer::OVMF;
use PVE::QemuServer::QMPHelpers;
use PVE::QemuServer::CPUConfig;
use PVE::Storage;
-my $base_env = {
- storage_config => {
- ids => {
- local => {
- content => {
- images => 1,
- iso => 1,
- },
- path => '/var/lib/vz',
- type => 'dir',
- shared => 0,
- },
- localsnapext => {
- content => {
- images => 1,
- },
- path => '/var/lib/vzsnapext',
- type => 'dir',
- shared => 0,
- 'snapshot-as-volume-chain' => 1,
- },
- noimages => {
- content => {
- iso => 1,
- },
- path => '/var/lib/vz',
- type => 'dir',
- },
- 'btrfs-store' => {
- content => {
- images => 1,
- },
- path => '/butter/bread',
- type => 'btrfs',
- },
- 'cifs-store' => {
- shared => 1,
- path => '/mnt/pve/cifs-store',
- username => 'guest',
- server => '127.0.0.42',
- type => 'cifs',
- share => 'CIFShare',
- content => {
- images => 1,
- iso => 1,
- },
- },
- 'rbd-store' => {
- monhost => '127.0.0.42,127.0.0.21,::1',
- fsid => 'fc4181a6-56eb-4f68-b452-8ba1f381ca2a',
- content => {
- images => 1,
- },
- type => 'rbd',
- pool => 'cpool',
- username => 'admin',
- shared => 1,
- },
- 'krbd-store' => {
- monhost => '127.0.0.42,127.0.0.21,::1',
- fsid => 'fc4181a6-56eb-4f68-b452-8ba1f381ca2a',
- content => {
- images => 1,
- },
- type => 'rbd',
- pool => 'cpool',
- username => 'admin',
- shared => 1,
- krbd => 1,
- },
- 'zfs-over-iscsi-store' => {
- type => 'zfs',
- iscsiprovider => "comstar",
- lio_tpg => "tpg1",
- portal => "127.0.0.1",
- target => "iqn.2019-10.org.test:foobar",
- pool => "tank",
- content => {
- images => 1,
- },
- },
- 'lvm-store' => {
- vgname => 'veegee',
- type => 'lvm',
- content => {
- images => 1,
- },
- },
- 'local-lvm' => {
- vgname => 'pve',
- bwlimit => 'restore=1024',
- type => 'lvmthin',
- thinpool => 'data',
- content => {
- images => 1,
- },
- },
- },
- },
- vmid => 8006,
- real_qemu_version => PVE::QemuServer::Helpers::kvm_user_version(), # not yet mocked
-};
+use CommandLineMocks qw(get_storage_config get_test_qemu_version set_test_env diff);
-my $pci_devs = [
- "0000:00:02.0",
- "0000:00:43.1",
- "0000:00:f4.0",
- "0000:00:ff.1",
- "0000:0f:f2.0",
- "0000:d0:13.0",
- "0000:d0:15.1",
- "0000:d0:15.2",
- "0000:d0:17.0",
- "0000:f0:42.0",
- "0000:f0:43.0",
- "0000:f0:43.1",
- "1234:f0:43.1",
- "0000:01:00.4",
- "0000:01:00.5",
- "0000:01:00.6",
- "0000:07:10.0",
- "0000:07:10.1",
- "0000:07:10.4",
-];
-
-my $pci_map_config = {
- ids => {
- someGpu => {
- type => 'pci',
- mdev => 1,
- map => [
- 'node=localhost,path=0000:01:00.4,id=10de:2231,iommugroup=1',
- 'node=localhost,path=0000:01:00.5,id=10de:2231,iommugroup=1',
- 'node=localhost,path=0000:01:00.6,id=10de:2231,iommugroup=1',
- ],
- },
- someNic => {
- type => 'pci',
- map => [
- 'node=localhost,path=0000:07:10.0,id=8086:1520,iommugroup=2',
- 'node=localhost,path=0000:07:10.1,id=8086:1520,iommugroup=2',
- 'node=localhost,path=0000:07:10.4,id=8086:1520,iommugroup=2',
- ],
- },
- },
-};
-
-my $usb_map_config = {},
-
- my $cpu_hw_capabilities = {
- # Gathered from an AMD EPYC 9475F running kernel 6.11.11-2-pve
- 'amd-turin-9005' =>
- '{ "amd-sev": { "cbitpos": 51, "reduced-phys-bits": 6, "sev-support": true,'
- . ' "sev-support-es": true, "sev-support-snp": true } }',
- # TODO: others?
- };
+my $vmid = 8006;
my $current_test; # = {
# description => 'Test description', # if available
@@ -212,8 +50,6 @@ sub parse_test($config_fn) {
my $description = $config->{description} // '';
- $current_test->{'host-cpu-vendor'} = 'GenuineIntel';
-
while ($description =~ /^\h*(.*?)\h*$/gm) {
my $line = $1;
next if !$line || $line =~ /^#/;
@@ -244,140 +80,11 @@ sub parse_test($config_fn) {
}
$current_test->{testname} = $testname;
- PVE::QemuServer::CPUConfig::initialize_cpu_models();
-}
+ set_test_env($current_test);
-sub get_test_qemu_version {
- $current_test->{qemu_version} // $base_env->{real_qemu_version} // '2.12';
+ PVE::QemuServer::CPUConfig::initialize_cpu_models();
}
-my $procfs_tools_module = Test::MockModule->new('PVE::ProcFSTools');
-$procfs_tools_module->mock(
- read_cpuinfo => sub {
- my $res = $procfs_tools_module->original('read_cpuinfo')->();
- my $vendor = $current_test->{'host-cpu-vendor'}
- or die "internal error - host cpu vendor not set";
- $res->{vendor} = $vendor;
- return $res;
- },
-);
-
-my $qemu_server_module;
-$qemu_server_module = Test::MockModule->new('PVE::QemuServer');
-$qemu_server_module->mock(
- kvm_user_version => sub {
- return get_test_qemu_version();
- },
- kvm_version => sub {
- return get_test_qemu_version();
- },
- kernel_has_vhost_net => sub {
- return 1; # TODO: make this per-test configurable?
- },
- get_iscsi_initiator_name => sub {
- return 'iqn.1993-08.org.debian:01:aabbccddeeff';
- },
- cleanup_pci_devices => {
- # do nothing
- },
-);
-
-my $qemu_server_ovmf_module = Test::MockModule->new("PVE::QemuServer::OVMF");
-$qemu_server_ovmf_module->mock(
- file_exists => sub {
- my ($path) = @_;
- return 1;
- },
- file_get_size => sub {
- my ($path) = @_;
- if ($path =~ m/OVMF(32)?_(SEV_)?VARS_4M/) {
- return 528 * 1024;
- } elsif ($path =~ m/OVMF_VARS/) {
- return 128 * 1024;
- } elsif ($path =~ m/AAVMF_VARS/) {
- return 64 * 1024 * 1024;
- } elsif ($path =~ m/RISCV_VIRT_VARS/) {
- return 32 * 1024 * 1024;
- } else {
- die "unknown ovmf vars image '$path' - implement me";
- }
- },
-);
-
-my $storage_module = Test::MockModule->new("PVE::Storage");
-$storage_module->mock(
- activate_volumes => sub {
- return;
- },
- deactivate_volumes => sub {
- return;
- },
- volume_snapshot_info => sub {
- my ($cfg, $volid) = @_;
-
- my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
-
- my $snapshots = {};
- if ($storeid eq 'localsnapext') {
- $snapshots = {
- current => {
- file => 'var/lib/vzsnapext/images/8006/vm-8006-disk-0.qcow2',
- parent => 'snap2',
- },
- snap2 => {
- file => '/var/lib/vzsnapext/images/8006/snap2-vm-8006-disk-0.qcow2',
- parent => 'snap1',
- },
- snap1 => {
- file => '/var/lib/vzsnapext/images/8006/snap1-vm-8006-disk-0.qcow2',
- },
- };
- } elsif ($storeid eq 'lvm-store') {
- $snapshots = {
- current => {
- file => '/dev/veegee/vm-8006-disk-0.qcow2',
- parent => 'snap2',
- },
- snap2 => {
- file => '/dev/veegee/snap2-vm-8006-disk-0.qcow2',
- parent => 'snap1',
- },
- snap1 => {
- file => '/dev/veegee/snap1-vm-8006-disk-0.qcow2',
- },
- };
- }
- return $snapshots;
- },
-);
-
-my $file_stat_module = Test::MockModule->new("File::stat");
-$file_stat_module->mock(
- stat => sub {
- my ($path) = @_;
- my $st = $file_stat_module->original('stat')->('./run_config2command_tests.pl');
- $st->[2] = 25008 if $path =~ m!/dev/!; # block device
- return $st;
- },
-);
-
-my $zfsplugin_module = Test::MockModule->new("PVE::Storage::ZFSPlugin");
-$zfsplugin_module->mock(
- zfs_get_lu_name => sub {
- return "foobar";
- },
- zfs_get_lun_number => sub {
- return "0";
- },
-);
-
-my $rbdplugin_module = Test::MockModule->new("PVE::Storage::RBDPlugin");
-$rbdplugin_module->mock(
- rbd_volume_config_set => sub {
- return;
- },
-);
-
my $qemu_server_config;
$qemu_server_config = Test::MockModule->new('PVE::QemuConfig');
$qemu_server_config->mock(
@@ -388,185 +95,14 @@ $qemu_server_config->mock(
},
);
-my $qemu_server_helpers;
-$qemu_server_helpers = Test::MockModule->new('PVE::QemuServer::Helpers');
-$qemu_server_helpers->mock(
- get_host_phys_address_bits => sub {
- return 46;
- },
-);
-
-my $qemu_server_memory;
-$qemu_server_memory = Test::MockModule->new('PVE::QemuServer::Memory');
-$qemu_server_memory->mock(
- hugepages_chunk_size_supported => sub {
- return 1;
- },
- host_numanode_exists => sub {
- my ($id) = @_;
- return 1;
- },
-);
-
-my $pve_common_tools;
-$pve_common_tools = Test::MockModule->new('PVE::Tools');
-$pve_common_tools->mock(
- next_vnc_port => sub {
- my ($family, $address) = @_;
-
- return '5900';
- },
- next_spice_port => sub {
- my ($family, $address) = @_;
-
- return '61000';
- },
- getaddrinfo_all => sub {
- my ($hostname, @opts) = @_;
- die "need stable hostname" if $hostname ne 'localhost';
- return (
- {
- addr => Socket::pack_sockaddr_in(0, Socket::INADDR_LOOPBACK),
- family => AF_INET, # IPv4
- protocol => 6,
- socktype => 1,
- },
- );
- },
- get_host_arch => sub {
- return $current_test->{host_arch} // 'x86_64';
- },
-);
-
-my $pve_common_file;
-$pve_common_file = Test::MockModule->new('PVE::File');
-$pve_common_file->mock(
- file_copy => sub {
- my ($filename, $dst, $max, $perm) = @_;
- if ($dst =~ m|^/run/qemu-server/efidisk|) {
- return;
- }
- return $pve_common_file->original('file_copy')->($filename, $dst, $max, $perm);
- },
-);
-
-my $pve_cpuconfig;
-$pve_cpuconfig = Test::MockModule->new('PVE::QemuServer::CPUConfig');
-$pve_cpuconfig->mock(
- load_custom_cpu_model_config => sub {
- # mock custom CPU model config
- return PVE::QemuServer::CPUConfig->parse_config(
- "cpu-models.conf",
- <<EOF,
-
-# "qemu64" is also a default CPU, used here to test that this doesn't matter
-cpu-model: qemu64
- reported-model athlon
- flags +aes;+avx;-kvm_pv_unhalt
- hv-vendor-id testvend
- phys-bits 40
-
-cpu-model: alldefault
-
-EOF
- );
- },
- get_hw_capabilities => sub {
- my $hw_capabilities_raw;
- if (!defined($current_test->{hw_capabilities})) {
- # default to barebone uncapable HW
- $hw_capabilities_raw =
- '{"amd-sev":{"cbitpos":0,"reduced-phys-bits":0,"sev-support":false,'
- . '"sev-support-es":false,"sev-support-snp":false}}';
- } elsif (
- my $cpu_hw_caps = $cpu_hw_capabilities->{ lc($current_test->{hw_capabilities}) }
- ) {
- $hw_capabilities_raw = $cpu_hw_caps;
- } else {
- $hw_capabilities_raw = $current_test->{hw_capabilities};
- }
-
- my $hw_capabilities = decode_json($hw_capabilities_raw);
- return $hw_capabilities;
- },
-);
-
-my $pve_common_network;
-$pve_common_network = Test::MockModule->new('PVE::Network');
-$pve_common_network->mock(
- read_bridge_mtu => sub {
- my ($bridge_name) = @_;
-
- if ($bridge_name eq 'vxlan_bridge') {
- return 1450;
- }
-
- return 1500;
- },
-);
-
-my $pve_common_inotify;
-$pve_common_inotify = Test::MockModule->new('PVE::INotify');
-$pve_common_inotify->mock(
- nodename => sub {
- return 'localhost';
- },
-);
-
-my $pve_common_sysfstools;
-$pve_common_sysfstools = Test::MockModule->new('PVE::SysFSTools');
-$pve_common_sysfstools->mock(
- lspci => sub {
- my ($filter, $verbose) = @_;
-
- return [
- map { { id => $_ } }
- grep {
- !defined($filter)
- || (!ref($filter) && $_ =~ m/^(0000:)?\Q$filter\E/)
- || (ref($filter) eq 'CODE' && $filter->({ id => $_ }))
- } sort @$pci_devs
- ];
- },
- pci_device_info => sub {
- my ($path, $noerr) = @_;
-
- if ($path =~ m/^0000:01:00/) {
- return {
- mdev => 1,
- iommugroup => 1,
- mdev => 1,
- vendor => "0x10de",
- device => "0x2231",
- };
- } elsif ($path =~ m/^0000:07:10/) {
- return {
- iommugroup => 2,
- vendor => "0x8086",
- device => "0x1520",
- };
- } else {
- return {};
- }
- },
-);
-
-my $qemu_drive_module;
-$qemu_drive_module = Test::MockModule->new('PVE::QemuServer::Drive');
-$qemu_drive_module->mock(
- get_cdrom_path => sub {
- return "/dev/cdrom";
- },
-);
-
my $qemu_monitor_module;
$qemu_monitor_module = Test::MockModule->new('PVE::QemuServer::Monitor');
$qemu_monitor_module->mock(
mon_cmd => sub {
- my ($vmid, $cmd) = @_;
+ my ($id, $cmd) = @_;
- die "invalid vmid: $vmid (expected: $base_env->{vmid})"
- if $vmid != $base_env->{vmid};
+ die "invalid vmid: $id (expected: $vmid)"
+ if $id != $vmid;
if ($cmd eq 'query-version') {
my $ver = get_test_qemu_version();
@@ -584,61 +120,6 @@ $qemu_monitor_module->mock(
},
);
-my $mapping_usb_module = Test::MockModule->new("PVE::Mapping::USB");
-$mapping_usb_module->mock(
- config => sub {
- return $usb_map_config;
- },
-);
-
-my $mapping_pci_module = Test::MockModule->new("PVE::Mapping::PCI");
-$mapping_pci_module->mock(
- config => sub {
- return $pci_map_config;
- },
-);
-
-my $pci_module = Test::MockModule->new("PVE::QemuServer::PCI");
-$pci_module->mock(
- reserve_pci_usage => sub {
- die "reserve_pci_usage should not be called for 'qm showcmd'\n";
- },
- create_nvidia_device => sub {
- die "create_nvidia_device should not be called for 'qm showcmd'\n";
- },
-);
-
-sub diff($a, $b) {
- return if $a eq $b;
-
- my ($ra, $wa) = POSIX::pipe();
- my ($rb, $wb) = POSIX::pipe();
- my $ha = IO::Handle->new_from_fd($wa, 'w');
- my $hb = IO::Handle->new_from_fd($wb, 'w');
-
- open my $diffproc, '-|', 'diff', '-up', "/proc/self/fd/$ra", "/proc/self/fd/$rb" ## no critic
- or die "failed to run program 'diff': $!";
- POSIX::close($ra);
- POSIX::close($rb);
-
- open my $f1, '<', \$a;
- open my $f2, '<', \$b;
- my ($line1, $line2);
- do {
- $ha->print($line1) if defined($line1 = <$f1>);
- $hb->print($line2) if defined($line2 = <$f2>);
- } while (defined($line1 // $line2));
- close $f1;
- close $f2;
- close $ha;
- close $hb;
-
- local $/ = undef;
- my $diff = <$diffproc>;
- close $diffproc;
- die "files differ:\n$diff";
-}
-
$SIG{__WARN__} = sub {
my $warning = shift;
chomp $warning;
@@ -663,7 +144,7 @@ sub do_test($config_fn) {
my $testname = $current_test->{testname};
- my ($vmid, $storecfg) = $base_env->@{qw(vmid storage_config)};
+ my $storecfg = get_storage_config();
my $cmdline = eval { PVE::QemuServer::vm_commandline($storecfg, $vmid) };
my $err = $@;
@@ -699,26 +180,23 @@ sub do_test($config_fn) {
my $cmd_fn = "$config_fn.cmd";
- if (-f $cmd_fn) {
+ if (!-f $cmd_fn) {
+ file_set_contents($cmd_fn, $cmdline);
+ } else {
my $cmdline_expected = file_get_contents($cmd_fn);
my $cmd_expected = [split /\s*\\?\n\s*/, $cmdline_expected];
my $cmd = [split /\s*\\?\n\s*/, $cmdline];
- # uncomment for easier debugging
- #file_set_contents("$cmd_fn.tmp", $cmdline);
-
my $exp = join("\n", @$cmd_expected);
my $got = join("\n", @$cmd);
- eval { diff($exp, $got) };
- if (my $err = $@) {
+
+ if (my $diff = diff($exp, $got)) {
fail("$testname");
- note($err);
+ note("files differ:\n$diff");
} else {
pass("$testname");
}
- } else {
- file_set_contents($cmd_fn, $cmdline);
}
}
diff --git a/src/test/run_hotplug_tests.pl b/src/test/run_hotplug_tests.pl
new file mode 100644
index 00000000..0cfeb350
--- /dev/null
+++ b/src/test/run_hotplug_tests.pl
@@ -0,0 +1,780 @@
+#!/usr/bin/perl
+
+# Regression tests for applying pending changes to a running VM via hotplug.
+#
+# Each test is a VM config in 'hotplug/*.conf' with the changes to apply in a [PENDING] section. The
+# monitor of the VM is mocked with a minimal model of a running QEMU instance, that tracks the
+# present devices, buses, block nodes, backends and QOM objects and enforces the constraints between
+# them. Every monitor command and every host-side action (network, SDN, storage, cgroup) that is
+# issued while applying the pending changes is recorded. Afterwards, the model is compared to the
+# model of a VM freshly started with the resulting config, which is what the target of a live
+# migration is, to detect deviations like differing PCI addresses or left-over devices. The
+# recording, the resulting hotplug errors, the differences and the resulting VM config are compared
+# to the corresponding 'hotplug/*.conf.expected' file. If that file does not exist yet, it is
+# generated and needs to be manually verified. Tests for an aarch64 host are in 'hotplug/aarch64/'.
+
+use v5.36;
+
+use lib qw(.. .);
+
+# the hotplug helpers wait between retries when verifying (un)plugged devices, do not wait in tests
+BEGIN {
+ *CORE::GLOBAL::sleep = sub { return 0; };
+}
+
+use JSON;
+use Storable qw(dclone);
+use Test::More;
+use Test::MockModule;
+
+use PVE::File qw(file_get_contents file_set_contents);
+use PVE::QemuConfig;
+use PVE::QemuServer;
+use PVE::QemuServer::Blockdev;
+use PVE::QemuServer::CGroup;
+use PVE::QemuServer::CPUConfig;
+use PVE::QemuServer::Drive;
+use PVE::QemuServer::DriveDevice;
+use PVE::QemuServer::Helpers;
+use PVE::QemuServer::Machine;
+use PVE::QemuServer::Monitor;
+use PVE::QemuServer::Network;
+use PVE::Storage;
+
+# the mocks for generating the command line of a VM, shared with the cfg2cmd tests
+use CommandLineMocks qw(get_storage_config get_test_qemu_version set_test_env diff);
+
+my $vmid = 8006;
+my $fake_config_fn = "hotplug/qemu-server/$vmid.conf";
+
+my $storecfg = get_storage_config();
+
+my $current_test; # = {
+# testname => 'file name and description',
+# description => 'Test description', # if available
+# qemu_version => '10.1',
+# host_arch => 'x86_64',
+# fail_device_add => { deviceid => 1 },
+# fail_device_del => { deviceid => 1 },
+# fail_command => { command => 1 },
+# config => { config hash },
+# };
+
+# The model of the running QEMU instance, set up from the command line that config_to_command()
+# generates for the config. The number of online vCPUs and the present memory DIMMs are derived
+# from the 'cpuN' and 'dimmN' devices. Buses are derived from the machine type and the present
+# bridges and controllers.
+my $vm_state; # = {
+# machine => 'pc-i440fx-10.1+pve0', # the machine type of the running VM
+# buses => { bus => 1 }, # buses provided by the machine itself
+# devices => { qdev_id => { device options referencing buses and backends } },
+# hotplugged => { qdev_id => 1 }, # devices that were added during the test
+# netdevs => { netdev_id => 1 },
+# chardevs => { chardev_id => 1 },
+# drives => { drive_id => 1 }, # legacy '-drive' backends (machine version < 10.0)
+# blocknodes => { node_name => 1 }, # explicitly added block nodes (machine version >= 10.0)
+# objects => { object_id => { arguments of object-add } }, # QOM objects
+# };
+
+my $log; # all recorded actions of the current test
+
+# use the config description to describe the test and the state of the running VM, fields are:
+# TEST: A single line describing the test, gets outputted
+# QEMU_VERSION: \d+\.\d+(\.\d+)? version of the running QEMU binary (defaults to current version)
+# HOST_ARCH: x86_64 | aarch64 (default to x86_64, to make tests stable)
+# FAIL_DEVICE_ADD: <id> QEMU accepts 'device_add' for this device, but it never shows up
+# FAIL_DEVICE_DEL: <id> QEMU accepts 'device_del' for this device, but it never goes away
+# FAIL_COMMAND: <name> the QMP or HMP command with this name fails
+# all fields are optional, the last three can be specified multiple times
+sub parse_test($config_fn) {
+ $current_test = {
+ fail_device_add => {},
+ fail_device_del => {},
+ fail_command => {},
+ };
+
+ my $config_raw = file_get_contents($config_fn);
+ my $config = PVE::QemuServer::parse_vm_config($fake_config_fn, $config_raw);
+
+ $current_test->{config} = $config;
+
+ my $description = $config->{description} // '';
+
+ while ($description =~ /^\h*(.*?)\h*$/gm) {
+ my $line = $1;
+ next if !$line || $line =~ /^#/;
+
+ if ($line =~ /^TEST:\s*(.*)\s*$/) {
+ $current_test->{description} = "$1";
+ } elsif ($line =~ /^QEMU_VERSION:\s*(.*)\s*$/) {
+ $current_test->{qemu_version} = "$1";
+ } elsif ($line =~ /^HOST_ARCH:\s*(.*)\s*$/) {
+ $current_test->{host_arch} = "$1";
+ } elsif ($line =~ /^FAIL_DEVICE_ADD:\s*(\S+)\s*$/) {
+ $current_test->{fail_device_add}->{$1} = 1;
+ } elsif ($line =~ /^FAIL_DEVICE_DEL:\s*(\S+)\s*$/) {
+ $current_test->{fail_device_del}->{$1} = 1;
+ } elsif ($line =~ /^FAIL_COMMAND:\s*(\S+)\s*$/) {
+ $current_test->{fail_command}->{$1} = 1;
+ }
+ }
+
+ # the description only describes the test and is not part of the resulting config
+ delete $config->{description};
+
+ $config_fn =~ /([^\/]+)$/;
+ my $testname = "$1";
+ if (my $desc = $current_test->{description}) {
+ $testname = "'$testname' - $desc";
+ }
+ $current_test->{testname} = $testname;
+
+ set_test_env($current_test);
+
+ # the 'host' CPU model is registered for the host architecture of the current test
+ PVE::QemuServer::CPUConfig::initialize_cpu_models();
+}
+
+# The buses a machine provides on its own. Everything else (PCI bridges, SCSI and USB controllers)
+# comes from devices on the command line or from the config files read via -readconfig.
+sub machine_buses($conf, $machine) {
+ return { map { $_ => 1 } qw(pcie.0) } if $machine =~ m/^virt/;
+ return { map { $_ => 1 } ('pcie.0', map { "ide.$_" } 0 .. 5) }
+ if PVE::QemuServer::Machine::machine_type_is_q35($conf);
+ return { map { $_ => 1 } qw(pci.0 ide.0 ide.1) };
+}
+
+# Parse a comma separated option string like 'driver,id=x,bus=y' into a hash. A leading value
+# without key is stored under $first_key.
+sub parse_options($string, $first_key = undef) {
+ my $options = {};
+ for my $part (split(/,/, $string)) {
+ if ($part =~ m/^([^=]+)=(.*)$/) {
+ $options->{$1} = $2;
+ } elsif (defined($first_key)) {
+ $options->{$first_key} = $part;
+ }
+ }
+ return $options;
+}
+
+# Parse the devices from a QEMU config file (-readconfig), which is shipped in 'usr/' of the repo.
+sub parse_readconfig($path) {
+ my ($name) = $path =~ m|([^/]+)$|;
+ my $devices = {};
+ my $current;
+ for my $line (split(/\n/, file_get_contents("../usr/$name"))) {
+ if ($line =~ m/^\[device "([^"]+)"\]/) {
+ $current = $devices->{$1} = { id => $1 };
+ } elsif ($line =~ m/^\[/) {
+ $current = undef;
+ } elsif ($current && $line =~ m/^\s*(\S+)\s*=\s*"([^"]*)"/) {
+ $current->{$1} = $2;
+ }
+ }
+ return $devices;
+}
+
+# QEMU resolves the unversioned aliases 'pc', 'q35' and 'virt' to the versioned default machine of
+# the running binary, which is also what query-machines reports for the running VM. The pve version
+# is kept as it was requested on the command line.
+sub resolve_machine_alias($machine) {
+ my ($type, $pve_version) = split(/\+/, $machine, 2);
+ my $aliases = { pc => 'pc-i440fx', q35 => 'pc-q35', virt => 'virt' };
+ if (my $prefix = $aliases->{$type}) {
+ my ($version) = get_test_qemu_version() =~ m/^(\d+\.\d+)/;
+ $type = "$prefix-$version";
+ }
+ return defined($pve_version) ? "$type+$pve_version" : $type;
+}
+
+# Returns the model of a VM freshly started with the given config, derived from the command line
+# that config_to_command() generates for it.
+sub vm_state_from_config($conf) {
+ my $cmd = PVE::QemuServer::config_to_command(
+ $storecfg, $vmid, dclone($conf), PVE::QemuServer::load_defaults(), {},
+ );
+
+ my $state = {
+ machine => resolve_machine_alias(PVE::QemuServer::Machine::get_vm_machine($conf)),
+ devices => {},
+ hotplugged => {},
+ netdevs => {},
+ chardevs => {},
+ drives => {},
+ blocknodes => {},
+ objects => {},
+ };
+
+ for (my $i = 0; $i < scalar($cmd->@*) - 1; $i++) {
+ my ($opt, $arg) = $cmd->@[$i, $i + 1];
+ if ($opt eq '-machine') {
+ $state->{machine} = resolve_machine_alias(parse_options($arg)->{type});
+ } elsif ($opt eq '-device') {
+ my $device = parse_options($arg, 'driver');
+ $state->{devices}->{ $device->{id} } = $device if $device->{id};
+ } elsif ($opt eq '-readconfig') {
+ my $devices = parse_readconfig($arg);
+ $state->{devices}->{$_} = $devices->{$_} for keys $devices->%*;
+ } elsif ($opt eq '-netdev') {
+ $state->{netdevs}->{ parse_options($arg)->{id} } = 1;
+ } elsif ($opt eq '-chardev') {
+ $state->{chardevs}->{ parse_options($arg, 'backend')->{id} } = 1;
+ } elsif ($opt eq '-drive') {
+ $state->{drives}->{ parse_options($arg)->{id} } = 1;
+ } elsif ($opt eq '-blockdev') {
+ $state->{blocknodes}->{ decode_json($arg)->{'node-name'} } = 1;
+ } elsif ($opt eq '-object') {
+ my $object = $arg =~ m/^\{/ ? decode_json($arg) : parse_options($arg, 'qom-type');
+ # sizes on the command line have a unit, QMP uses bytes
+ if (defined($object->{size}) && $object->{size} =~ m/^(\d+)([MG])$/) {
+ $object->{size} = $1 * 1024 * 1024 * ($2 eq 'G' ? 1024 : 1);
+ }
+ $state->{objects}->{ $object->{id} } = $object;
+ }
+ }
+
+ $state->{buses} = machine_buses($conf, $state->{machine});
+
+ return $state;
+}
+
+sub setup_vm_state($conf) {
+ $vm_state = vm_state_from_config($conf);
+}
+
+# Throttle limits set via QMP contain all properties, while the throttle group generated for the
+# command line only contains the configured ones. Drop the defaults to make them comparable.
+sub normalized_object($object) {
+ $object = dclone($object);
+ if (($object->{'qom-type'} // '') eq 'throttle-group' && $object->{limits}) {
+ my $limits = $object->{limits};
+ my $is_default = sub { $limits->{ $_[0] } == ($_[0] =~ m/-max-length$/ ? 1 : 0) };
+ $object->{limits} =
+ { map { $_ => $limits->{$_} } grep { !$is_default->($_) } keys %$limits };
+ }
+ return to_json($object, { canonical => 1 });
+}
+
+# Compare the model of the running VM with the model of a VM freshly started with the resulting
+# config, like the target of a live migration is. Returns a list of the differences. The boot index
+# is not compared, as hotplugged devices are added without one.
+sub compare_with_fresh_vm($conf) {
+ my $running = $vm_state;
+ my $fresh = vm_state_from_config($conf);
+ my @differences = ();
+
+ my $compare_ids = sub {
+ my ($kind, $running_ids, $fresh_ids) = @_;
+ my $only_in = sub {
+ my ($ids, $others, $desc) = @_;
+ push @differences, "$kind $_: only present in $desc"
+ for sort grep { !$others->{$_} } keys $ids->%*;
+ };
+ $only_in->($running_ids, $fresh_ids, 'running VM');
+ $only_in->($fresh_ids, $running_ids, 'freshly started VM');
+ };
+
+ push @differences, "machine: running '$running->{machine}' vs fresh '$fresh->{machine}'"
+ if $running->{machine} ne $fresh->{machine};
+
+ $compare_ids->('device', $running->{devices}, $fresh->{devices});
+ for my $id (sort grep { $fresh->{devices}->{$_} } keys $running->{devices}->%*) {
+ my $running_device = $running->{devices}->{$id};
+ my $fresh_device = $fresh->{devices}->{$id};
+ my %options = map { $_ => 1 } keys $running_device->%*, keys $fresh_device->%*;
+ delete $options{bootindex};
+ for my $option (sort keys %options) {
+ my $running_value = $running_device->{$option} // '<undef>';
+ my $fresh_value = $fresh_device->{$option} // '<undef>';
+ push @differences,
+ "device $id option $option: running '$running_value' vs fresh '$fresh_value'"
+ if "$running_value" ne "$fresh_value";
+ }
+ }
+
+ $compare_ids->('object', $running->{objects}, $fresh->{objects});
+ for my $id (sort grep { $fresh->{objects}->{$_} } keys $running->{objects}->%*) {
+ my $running_object = normalized_object($running->{objects}->{$id});
+ my $fresh_object = normalized_object($fresh->{objects}->{$id});
+ push @differences, "object $id: running $running_object vs fresh $fresh_object"
+ if $running_object ne $fresh_object;
+ }
+
+ for my $kind (qw(netdev chardev drive blocknode)) {
+ $compare_ids->($kind, $running->{"${kind}s"}, $fresh->{"${kind}s"});
+ }
+
+ return @differences;
+}
+
+# Record an action with its arguments. HMP commands are recorded as the full command line. Arguments
+# of QMP commands are recorded as they are, for other actions all scalars are stringified to be
+# independent from the internal representation.
+sub record($layer, $action, @args) {
+ my $line = "$layer $action";
+ if ($layer eq 'qmp') {
+ $line .= ' ' . to_json($args[0], { canonical => 1 });
+ } elsif (scalar(@args)) {
+ $line .= ' ' . to_json([@args], { canonical => 1 });
+ }
+ push $log->@*, $line;
+}
+
+sub sorted_devices($regex = qr/./) {
+ return sort grep { $_ =~ $regex } keys $vm_state->{devices}->%*;
+}
+
+# Returns the ID of a device referencing the given backend via the given device option.
+sub device_using($option, $value) {
+ my $devices = $vm_state->{devices};
+ my @users = grep { ($devices->{$_}->{$option} // '') eq $value } sort keys $devices->%*;
+ return $users[0];
+}
+
+sub bus_exists($bus) {
+ return 1 if $vm_state->{buses}->{$bus};
+ return 1 if $bus =~ m/^pci\.\d+$/ && $vm_state->{devices}->{$bus}; # PCI bridge
+ return 1 if $bus =~ m/^(.+)\.0$/ && $vm_state->{devices}->{$1}; # SCSI and USB controllers
+ return 0;
+}
+
+# Check the constraints for adding the device with the given options. Returns an error message if
+# the device cannot be added, otherwise adds it and returns nothing.
+sub add_device($options) {
+ my $id = $options->{id} or die "device_add without ID\n";
+ my $driver = $options->{driver};
+
+ return "Duplicate device ID '$id'" if $vm_state->{devices}->{$id};
+ return "simulated failure adding device '$id'" if $current_test->{fail_device_add}->{$id};
+
+ if (my $bus = $options->{bus}) {
+ return "Bus '$bus' does not support hotplugging" if $bus =~ m/^(ide\.\d+|ahci\d+\.\d+)$/;
+ # devices cannot be hotplugged into a PCI bridge that was itself hotplugged, as the guest
+ # firmware did not assign any resources to it
+ return "Bus '$bus' does not support hotplugging" if $vm_state->{hotplugged}->{$bus};
+ return "Bus '$bus' not found" if !bus_exists($bus);
+ }
+
+ my $backends = {
+ netdev => $vm_state->{netdevs},
+ chardev => $vm_state->{chardevs},
+ drive => { $vm_state->{drives}->%*, $vm_state->{blocknodes}->%* },
+ iothread => $vm_state->{objects},
+ memdev => $vm_state->{objects},
+ };
+ for my $option (sort keys $backends->%*) {
+ my $value = $options->{$option} or next;
+ return "Property '$driver.$option' can't find value '$value'"
+ if !$backends->{$option}->{$value};
+ }
+
+ $vm_state->{devices}->{$id} = $options;
+ $vm_state->{hotplugged}->{$id} = 1;
+ return;
+}
+
+# Handle QMP commands querying the state of the VM. Returns nothing for other commands.
+sub fake_qmp_query($execute, $arguments) {
+ if ($execute eq 'query-version') {
+ my ($major, $minor, $micro) = get_test_qemu_version() =~ m/^(\d+)\.(\d+)(?:\.(\d+))?/;
+ return { qemu => { major => $major, minor => $minor, micro => $micro // 0 } };
+ } elsif ($execute eq 'query-machines') {
+ my ($name, $pve_version) = split(/\+/, $vm_state->{machine}, 2);
+ my $machine = { name => $name, 'is-current' => JSON::true };
+ $machine->{'pve-version'} = $pve_version if $pve_version;
+ return [$machine];
+ } elsif ($execute eq 'query-pci') {
+ # everything that is not a disk or memory DIMM is reported as a PCI device with the qdev ID
+ my @devices =
+ map { { qdev_id => $_ } }
+ grep { !PVE::QemuServer::Drive::is_valid_drivename($_) && $_ !~ m/^dimm\d+$/ }
+ sorted_devices();
+ return [{ bus => 0, devices => \@devices }];
+ } elsif ($execute eq 'query-block') {
+ my @blocks = ();
+ for my $id (grep { PVE::QemuServer::Drive::is_valid_drivename($_) } sorted_devices()) {
+ my $qdev = $id =~ m/^virtio\d+$/ ? "/machine/peripheral/$id/virtio-backend" : $id;
+ push @blocks, { device => '', qdev => $qdev };
+ }
+ return \@blocks;
+ } elsif ($execute eq 'query-mice') {
+ return [{ name => 'QEMU HID Tablet' }] if $vm_state->{devices}->{tablet};
+ return [{ name => 'QEMU PS/2 Mouse' }];
+ } elsif ($execute eq 'qom-list') {
+ die "unexpected qom-list path '$arguments->{path}'\n"
+ if $arguments->{path} ne '/machine/peripheral';
+ return [map { { name => $_ } } sorted_devices()];
+ } elsif ($execute eq 'query-iothreads') {
+ my $objects = $vm_state->{objects};
+ return [
+ map { { id => $_, 'thread-id' => 1 } }
+ sort grep { $objects->{$_}->{'qom-type'} eq 'iothread' } keys $objects->%*
+ ];
+ } elsif ($execute eq 'query-cpus-fast') {
+ # the first vCPU is always present, the others are 'cpuN' devices
+ my @hotplugged_cpus = sorted_devices(qr/^cpu\d+$/);
+ return [map { { 'cpu-index' => $_ } } (0 .. scalar(@hotplugged_cpus))];
+ } elsif ($execute eq 'query-memory-devices') {
+ my $objects = $vm_state->{objects};
+ return [
+ map { {
+ type => 'dimm',
+ data => { id => $_, size => $objects->{"mem-$_"}->{size} },
+ } } sorted_devices(qr/^dimm\d+$/)
+ ];
+ } elsif ($execute eq 'query-named-block-nodes') {
+ return [
+ map { { 'node-name' => $_, children => [] } }
+ sort keys $vm_state->{blocknodes}->%*
+ ];
+ }
+
+ return;
+}
+
+# Handle commands issued via the human monitor. Errors are reported as text output like QEMU does.
+sub fake_hmp_cmd($cmdline) {
+ record('hmp', $cmdline);
+
+ my ($command, $args) = $cmdline =~ m/^(\S+)\s+(.*)$/;
+ return "Error: simulated failure of HMP command '$command'"
+ if $current_test->{fail_command}->{$command};
+
+ if ($command eq 'device_add') {
+ my $options = { map { split(/=/, $_, 2) } split(/,/, $args) };
+ return "Error: Parameter 'driver' is missing" if !$options->{driver};
+ my $err = add_device($options);
+ return $err ? "Error: $err" : '';
+ } elsif ($command eq 'device_del') {
+ my $id = $args;
+ return "Error: Device '$id' not found" if !$vm_state->{devices}->{$id};
+ # the guest never releases the device
+ return '' if $current_test->{fail_device_del}->{$id};
+ delete $vm_state->{devices}->{$id};
+ return '';
+ } elsif ($command eq 'drive_add') {
+ my ($id) = $args =~ m/,id=([^,"]+)/ or die "drive_add without ID: $cmdline\n";
+ return "Error: Duplicate ID '$id' for drive" if $vm_state->{drives}->{$id};
+ $vm_state->{drives}->{$id} = 1;
+ return 'OK';
+ } elsif ($command eq 'drive_del') {
+ my $id = $args;
+ return "Error: Device '$id' not found" if !delete $vm_state->{drives}->{$id};
+ return '';
+ }
+
+ die "unexpected HMP command: '$cmdline'\n";
+}
+
+# QMP commands that do not change the modeled state of the VM
+my $stateless_commands = {
+ map { $_ => 1 }
+ qw(
+ balloon
+ block_set_io_throttle
+ blockdev-change-medium
+ blockdev-close-tray
+ blockdev-open-tray
+ eject
+ )
+};
+
+# Replacement for PVE::QemuServer::Monitor::qmp_cmd, handling all monitor communication with the VM.
+sub fake_qmp_cmd {
+ my ($peer, $execute, %arguments) = @_;
+
+ die "unexpected QMP peer '$peer->{name}' of type '$peer->{type}'\n"
+ if $peer->{type} ne 'qmp' || $peer->{id} != $vmid;
+
+ delete $arguments{timeout};
+ my $noerr = delete $arguments{noerr};
+
+ return fake_hmp_cmd($arguments{'command-line'}) if $execute eq 'human-monitor-command';
+
+ # queries do not change the state of the VM and are not recorded
+ if (my $result = fake_qmp_query($execute, \%arguments)) {
+ return $result;
+ }
+
+ record('qmp', $execute, \%arguments);
+
+ my $fail = sub {
+ my ($msg) = @_;
+ return { error => $msg } if $noerr;
+ die "$msg\n";
+ };
+
+ return $fail->("simulated failure of QMP command '$execute'")
+ if $current_test->{fail_command}->{$execute};
+
+ return {} if $stateless_commands->{$execute};
+
+ my $id = $arguments{id};
+ my $node_name = $arguments{'node-name'};
+ my $devices = $vm_state->{devices};
+
+ if ($execute eq 'device_add') { # the QMP variant is only used for memory DIMMs
+ my $err = add_device(\%arguments);
+ return $fail->($err) if $err;
+ } elsif ($execute eq 'netdev_add') {
+ return $fail->("Duplicate ID '$id' for netdev") if $vm_state->{netdevs}->{$id};
+ $vm_state->{netdevs}->{$id} = 1;
+ } elsif ($execute eq 'netdev_del') {
+ return $fail->("Device '$id' not found") if !$vm_state->{netdevs}->{$id};
+ if (my $device = device_using('netdev', $id)) {
+ return $fail->("netdev '$id' is in use by device '$device'");
+ }
+ delete $vm_state->{netdevs}->{$id};
+ } elsif ($execute eq 'set_link') {
+ my $name = $arguments{name};
+ return $fail->("Device '$name' not found")
+ if !$devices->{$name} && !$vm_state->{netdevs}->{$name};
+ } elsif ($execute eq 'chardev-add') {
+ return $fail->("Duplicate ID '$id' for chardev") if $vm_state->{chardevs}->{$id};
+ $vm_state->{chardevs}->{$id} = 1;
+ } elsif ($execute eq 'object-add') {
+ return $fail->("Duplicate object ID '$id'") if $vm_state->{objects}->{$id};
+ $vm_state->{objects}->{$id} = \%arguments;
+ } elsif ($execute eq 'object-del') {
+ return $fail->("Object '$id' not found") if !$vm_state->{objects}->{$id};
+ for my $option (qw(iothread memdev)) {
+ if (my $device = device_using($option, $id)) {
+ return $fail->("Object '$id' is in use by device '$device'");
+ }
+ }
+ # the top block node of a drive uses the throttle group with the same name
+ if (my ($node) = $id =~ m/^throttle-(drive-.+)$/) {
+ return $fail->("Object '$id' is in use by node '$node'")
+ if $vm_state->{blocknodes}->{$node};
+ }
+ delete $vm_state->{objects}->{$id};
+ } elsif ($execute eq 'qom-set') { # only used for the limits of throttle groups
+ my $object = $vm_state->{objects}->{ $arguments{path} }
+ or return $fail->("Object '$arguments{path}' not found");
+ $object->{ $arguments{property} } = $arguments{value};
+ } elsif ($execute eq 'blockdev-add') {
+ return $fail->("Duplicate nodes with node-name='$node_name'")
+ if $vm_state->{blocknodes}->{$node_name};
+ $vm_state->{blocknodes}->{$node_name} = 1;
+ } elsif ($execute eq 'blockdev-del') {
+ return $fail->("Failed to find node with node-name='$node_name'")
+ if !$vm_state->{blocknodes}->{$node_name};
+ if (my $device = device_using('drive', $node_name)) {
+ return $fail->("Node '$node_name' is in use by device '$device'");
+ }
+ delete $vm_state->{blocknodes}->{$node_name};
+ } elsif ($execute eq 'blockdev-remove-medium') {
+ return $fail->("Device '$id' not found") if !$devices->{$id};
+ delete $devices->{$id}->{drive};
+ } elsif ($execute eq 'blockdev-insert-medium') {
+ return $fail->("Device '$id' not found") if !$devices->{$id};
+ return $fail->("Node '$node_name' not found") if !$vm_state->{blocknodes}->{$node_name};
+ $devices->{$id}->{drive} = $node_name;
+ } else {
+ die "unexpected QMP command: '$execute'\n";
+ }
+
+ return {};
+}
+
+my $monitor_module = Test::MockModule->new('PVE::QemuServer::Monitor');
+$monitor_module->mock(qmp_cmd => \&fake_qmp_cmd);
+
+# qmp_cmd is imported by these modules, so the imported copies need to be replaced too
+my $qemu_server_module = Test::MockModule->new('PVE::QemuServer');
+$qemu_server_module->mock(qmp_cmd => \&fake_qmp_cmd);
+
+my $blockdev_module = Test::MockModule->new('PVE::QemuServer::Blockdev');
+$blockdev_module->mock(qmp_cmd => \&fake_qmp_cmd);
+
+# the machine type of the running VM is derived from the QEMU version, so it has to be the mocked
+# one everywhere, not only for the imported copy in PVE::QemuServer
+my $qemu_server_helpers = Test::MockModule->new('PVE::QemuServer::Helpers');
+$qemu_server_helpers->mock(
+ kvm_user_version => \&get_test_qemu_version,
+ vm_running_locally => sub {
+ return 1;
+ },
+);
+
+my $drive_device_module = Test::MockModule->new('PVE::QemuServer::DriveDevice');
+$drive_device_module->mock(kvm_user_version => \&get_test_qemu_version);
+
+my $qemu_server_config = Test::MockModule->new('PVE::QemuConfig');
+$qemu_server_config->mock(
+ write_config => sub {
+ my ($class, $vmid, $conf) = @_;
+ return;
+ },
+);
+
+my $pve_cluster_module = Test::MockModule->new('PVE::Cluster');
+$pve_cluster_module->mock(
+ cfs_read_file => sub {
+ my ($file) = @_;
+
+ if ($file eq 'datacenter.cfg') {
+ return {};
+ } else {
+ die "'cfs_read_file' called for '$file' - missing mock?\n";
+ }
+ },
+);
+
+# returns a mock that records the call with all its arguments as an action of the given layer
+my sub recorder($layer, $action) {
+ return sub {
+ record($layer, $action, @_);
+ return;
+ };
+}
+
+my $pve_common_network = Test::MockModule->new('PVE::Network');
+$pve_common_network->mock(
+ tap_unplug => recorder('net', 'tap_unplug'),
+ tap_rate_limit => recorder('net', 'tap_rate_limit'),
+);
+
+my $qemu_network_module = Test::MockModule->new('PVE::QemuServer::Network');
+$qemu_network_module->mock(tap_plug => recorder('net', 'tap_plug'));
+
+my $sdn_vnets_module = Test::MockModule->new('PVE::Network::SDN::Vnets');
+$sdn_vnets_module->mock(
+ add_next_free_cidr => recorder('sdn', 'add_next_free_cidr'),
+ add_dhcp_mapping => recorder('sdn', 'add_dhcp_mapping'),
+ del_ips_from_mac => recorder('sdn', 'del_ips_from_mac'),
+);
+
+# the shared mocks turn these into no-ops, but here they are actions worth recording
+my $storage_module = Test::MockModule->new('PVE::Storage');
+$storage_module->mock(
+ activate_volumes => sub {
+ my ($cfg, $vollist) = @_;
+ record('storage', 'activate_volumes', $vollist->@*);
+ return;
+ },
+ vdisk_free => sub {
+ my ($cfg, $volid) = @_;
+ record('storage', 'vdisk_free', $volid);
+ return;
+ },
+);
+
+# detaching a disk checks permissions via the RPC environment, which is not initialized in tests
+my $rpcenv_module = Test::MockModule->new('PVE::RPCEnvironment');
+$rpcenv_module->mock(
+ get => sub {
+ return bless({}, 'PVE::RPCEnvironment');
+ },
+ get_user => sub {
+ return 'root@pam';
+ },
+ check => sub {
+ return 1;
+ },
+ check_vm_perm => sub {
+ return 1;
+ },
+);
+
+my $cgroup_module = Test::MockModule->new('PVE::QemuServer::CGroup');
+$cgroup_module->mock(
+ change_cpu_shares => sub {
+ my ($self, $shares) = @_;
+ record('cgroup', 'change_cpu_shares', $shares);
+ return;
+ },
+ change_cpu_quota => sub {
+ my ($self, $quota, $period) = @_;
+ record('cgroup', 'change_cpu_quota', $quota, $period);
+ return;
+ },
+);
+
+# warnings are part of the expected outcome, e.g. when cleaning up after a failed hotplug
+$SIG{__WARN__} = sub {
+ my $warning = shift;
+ chomp $warning;
+ $warning =~ s/ at \S+ line \d+\.?$//;
+ record('warn', $warning);
+};
+
+sub do_test($config_fn) {
+ die "no such input test config: $config_fn\n" if !-f $config_fn;
+
+ $log = []; # also record warnings while parsing the config
+ parse_test($config_fn);
+
+ my $testname = $current_test->{testname};
+ my $conf = $current_test->{config};
+
+ my $errors = {};
+ eval {
+ setup_vm_state($conf);
+ PVE::QemuServer::vmconfig_hotplug_pending($vmid, $conf, $storecfg, undef, $errors);
+ };
+ my $err = $@;
+
+ # error messages without trailing newline contain the source location, which changes too often
+ my $clean_error = sub {
+ my ($msg) = @_;
+ chomp $msg;
+ $msg =~ s/ at \S+ line \d+\.?$//;
+ return $msg;
+ };
+
+ my $result = "# recorded actions\n";
+ $result .= "$_\n" for $log->@*;
+ $result .= "\n# hotplug errors\n";
+ $result .= "$_: " . $clean_error->($errors->{$_}) . "\n" for sort keys $errors->%*;
+ $result .= "\n# died\n" . $clean_error->($err) . "\n" if $err;
+
+ # the target of a live migration is started with the resulting config, so it must match
+ my @differences = eval { compare_with_fresh_vm($conf) };
+ push @differences, "failed to start a VM with the resulting config: " . $clean_error->($@)
+ if $@;
+ $result .= "\n# differences to a freshly started VM\n";
+ $result .= "$_\n" for @differences;
+
+ $result .= "\n# resulting config\n";
+ $result .= PVE::QemuServer::write_vm_config($fake_config_fn, $conf);
+
+ my $expected_fn = "$config_fn.expected";
+
+ if (!-f $expected_fn) {
+ file_set_contents($expected_fn, $result);
+ pass("$testname (generated expected output)");
+ } else {
+ my $expected = file_get_contents($expected_fn);
+ if (my $diff = diff($expected, $result)) {
+ note("files differ:\n$diff");
+ fail("$testname");
+ } else {
+ pass("$testname");
+ }
+ }
+}
+
+print "testing hotplug of pending changes\n";
+
+# exec tests
+my $test_target = shift // 'hotplug';
+
+if (-f $test_target) {
+ do_test($test_target);
+} elsif (-d $test_target) {
+ PVE::File::dir_glob_foreach(
+ $test_target,
+ qr/.+\.conf/,
+ sub {
+ my ($file) = @_;
+
+ do_test("${test_target}/${file}");
+ },
+ );
+} else {
+ die "test target '$test_target' is neither file nor directory, cannot proceed\n";
+}
+
+done_testing();
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH qemu-server 2/6] tests: hotplug: add some test cases
2026-09-10 11:00 [PATCH qemu-server 0/6] add hotplug tests and fix uncovered bugs Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 1/6] tests: hotplug: add initial hotplug test harness Dominik Csapak
@ 2026-09-10 11:00 ` Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 3/6] tests: hotplug: add cases for known defects Dominik Csapak
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2026-09-10 11:00 UTC (permalink / raw)
To: pve-devel
Add multiple test cases for a variety of situations. Includes test cases
that are expected to fail too.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/test/Makefile | 6 ++++
.../aarch64/disk-add-default-machine.conf | 20 +++++++++++++
.../disk-add-default-machine.conf.expected | 28 ++++++++++++++++++
src/test/hotplug/aarch64/disk-add-virtio.conf | 20 +++++++++++++
.../aarch64/disk-add-virtio.conf.expected | 29 +++++++++++++++++++
.../hotplug/aarch64/net-add-pci-bridge.conf | 20 +++++++++++++
.../aarch64/net-add-pci-bridge.conf.expected | 28 ++++++++++++++++++
src/test/hotplug/aarch64/net-add.conf | 20 +++++++++++++
.../hotplug/aarch64/net-add.conf.expected | 28 ++++++++++++++++++
src/test/hotplug/aarch64/tablet-disable.conf | 20 +++++++++++++
.../aarch64/tablet-disable.conf.expected | 25 ++++++++++++++++
.../hotplug/aarch64/x86-guest-net-add.conf | 18 ++++++++++++
.../aarch64/x86-guest-net-add.conf.expected | 26 +++++++++++++++++
src/test/hotplug/agent-fstrim.conf | 17 +++++++++++
src/test/hotplug/agent-fstrim.conf.expected | 20 +++++++++++++
src/test/hotplug/balloon.conf | 18 ++++++++++++
src/test/hotplug/balloon.conf.expected | 22 ++++++++++++++
src/test/hotplug/cdrom-eject.conf | 17 +++++++++++
src/test/hotplug/cdrom-eject.conf.expected | 24 +++++++++++++++
src/test/hotplug/cpu-add-too-many.conf | 18 ++++++++++++
.../hotplug/cpu-add-too-many.conf.expected | 25 ++++++++++++++++
src/test/hotplug/cpu-add.conf | 18 ++++++++++++
src/test/hotplug/cpu-add.conf.expected | 23 +++++++++++++++
src/test/hotplug/cpu-remove.conf | 18 ++++++++++++
src/test/hotplug/cpu-remove.conf.expected | 23 +++++++++++++++
src/test/hotplug/cpulimit.conf | 16 ++++++++++
src/test/hotplug/cpulimit.conf.expected | 21 ++++++++++++++
.../hotplug/disk-add-default-machine.conf | 16 ++++++++++
.../disk-add-default-machine.conf.expected | 25 ++++++++++++++++
src/test/hotplug/disk-add-legacy-drive.conf | 16 ++++++++++
.../disk-add-legacy-drive.conf.expected | 23 +++++++++++++++
src/test/hotplug/disk-add-scsi-single.conf | 16 ++++++++++
.../disk-add-scsi-single.conf.expected | 27 +++++++++++++++++
src/test/hotplug/disk-add-virtio.conf | 16 ++++++++++
.../hotplug/disk-add-virtio.conf.expected | 26 +++++++++++++++++
src/test/hotplug/disk-remove-bootdisk.conf | 16 ++++++++++
.../disk-remove-bootdisk.conf.expected | 23 +++++++++++++++
src/test/hotplug/disk-remove-busy.conf | 18 ++++++++++++
.../hotplug/disk-remove-busy.conf.expected | 25 ++++++++++++++++
src/test/hotplug/disk-remove-scsi-single.conf | 17 +++++++++++
.../disk-remove-scsi-single.conf.expected | 25 ++++++++++++++++
src/test/hotplug/disk-remove-virtio.conf | 17 +++++++++++
.../hotplug/disk-remove-virtio.conf.expected | 24 +++++++++++++++
src/test/hotplug/disk-skip-ide.conf | 16 ++++++++++
src/test/hotplug/disk-skip-ide.conf.expected | 22 ++++++++++++++
src/test/hotplug/disk-throttle.conf | 16 ++++++++++
src/test/hotplug/disk-throttle.conf.expected | 20 +++++++++++++
src/test/hotplug/fast-plug.conf | 17 +++++++++++
src/test/hotplug/fast-plug.conf.expected | 20 +++++++++++++
src/test/hotplug/memory-add.conf | 18 ++++++++++++
src/test/hotplug/memory-add.conf.expected | 25 ++++++++++++++++
src/test/hotplug/memory-remove.conf | 18 ++++++++++++
src/test/hotplug/memory-remove.conf.expected | 25 ++++++++++++++++
src/test/hotplug/net-add-fail.conf | 17 +++++++++++
src/test/hotplug/net-add-fail.conf.expected | 28 ++++++++++++++++++
src/test/hotplug/net-add-pci-bridge.conf | 16 ++++++++++
.../hotplug/net-add-pci-bridge.conf.expected | 25 ++++++++++++++++
src/test/hotplug/net-add.conf | 16 ++++++++++
src/test/hotplug/net-add.conf.expected | 25 ++++++++++++++++
src/test/hotplug/net-change-bridge.conf | 16 ++++++++++
.../hotplug/net-change-bridge.conf.expected | 25 ++++++++++++++++
src/test/hotplug/net-change-model.conf | 16 ++++++++++
.../hotplug/net-change-model.conf.expected | 27 +++++++++++++++++
src/test/hotplug/net-change-rate.conf | 16 ++++++++++
.../hotplug/net-change-rate.conf.expected | 20 +++++++++++++
src/test/hotplug/net-hotplug-disabled.conf | 17 +++++++++++
.../net-hotplug-disabled.conf.expected | 23 +++++++++++++++
src/test/hotplug/net-remove.conf | 17 +++++++++++
src/test/hotplug/net-remove.conf.expected | 22 ++++++++++++++
src/test/hotplug/tablet-disable.conf | 16 ++++++++++
src/test/hotplug/tablet-disable.conf.expected | 21 ++++++++++++++
src/test/hotplug/tablet-enable.conf | 17 +++++++++++
src/test/hotplug/tablet-enable.conf.expected | 21 ++++++++++++++
src/test/hotplug/usb-add-spice.conf | 17 +++++++++++
src/test/hotplug/usb-add-spice.conf.expected | 23 +++++++++++++++
src/test/hotplug/usb-add.conf | 16 ++++++++++
src/test/hotplug/usb-add.conf.expected | 22 ++++++++++++++
src/test/hotplug/usb-remove-last.conf | 17 +++++++++++
.../hotplug/usb-remove-last.conf.expected | 21 ++++++++++++++
79 files changed, 1612 insertions(+)
create mode 100644 src/test/hotplug/aarch64/disk-add-default-machine.conf
create mode 100644 src/test/hotplug/aarch64/disk-add-default-machine.conf.expected
create mode 100644 src/test/hotplug/aarch64/disk-add-virtio.conf
create mode 100644 src/test/hotplug/aarch64/disk-add-virtio.conf.expected
create mode 100644 src/test/hotplug/aarch64/net-add-pci-bridge.conf
create mode 100644 src/test/hotplug/aarch64/net-add-pci-bridge.conf.expected
create mode 100644 src/test/hotplug/aarch64/net-add.conf
create mode 100644 src/test/hotplug/aarch64/net-add.conf.expected
create mode 100644 src/test/hotplug/aarch64/tablet-disable.conf
create mode 100644 src/test/hotplug/aarch64/tablet-disable.conf.expected
create mode 100644 src/test/hotplug/aarch64/x86-guest-net-add.conf
create mode 100644 src/test/hotplug/aarch64/x86-guest-net-add.conf.expected
create mode 100644 src/test/hotplug/agent-fstrim.conf
create mode 100644 src/test/hotplug/agent-fstrim.conf.expected
create mode 100644 src/test/hotplug/balloon.conf
create mode 100644 src/test/hotplug/balloon.conf.expected
create mode 100644 src/test/hotplug/cdrom-eject.conf
create mode 100644 src/test/hotplug/cdrom-eject.conf.expected
create mode 100644 src/test/hotplug/cpu-add-too-many.conf
create mode 100644 src/test/hotplug/cpu-add-too-many.conf.expected
create mode 100644 src/test/hotplug/cpu-add.conf
create mode 100644 src/test/hotplug/cpu-add.conf.expected
create mode 100644 src/test/hotplug/cpu-remove.conf
create mode 100644 src/test/hotplug/cpu-remove.conf.expected
create mode 100644 src/test/hotplug/cpulimit.conf
create mode 100644 src/test/hotplug/cpulimit.conf.expected
create mode 100644 src/test/hotplug/disk-add-default-machine.conf
create mode 100644 src/test/hotplug/disk-add-default-machine.conf.expected
create mode 100644 src/test/hotplug/disk-add-legacy-drive.conf
create mode 100644 src/test/hotplug/disk-add-legacy-drive.conf.expected
create mode 100644 src/test/hotplug/disk-add-scsi-single.conf
create mode 100644 src/test/hotplug/disk-add-scsi-single.conf.expected
create mode 100644 src/test/hotplug/disk-add-virtio.conf
create mode 100644 src/test/hotplug/disk-add-virtio.conf.expected
create mode 100644 src/test/hotplug/disk-remove-bootdisk.conf
create mode 100644 src/test/hotplug/disk-remove-bootdisk.conf.expected
create mode 100644 src/test/hotplug/disk-remove-busy.conf
create mode 100644 src/test/hotplug/disk-remove-busy.conf.expected
create mode 100644 src/test/hotplug/disk-remove-scsi-single.conf
create mode 100644 src/test/hotplug/disk-remove-scsi-single.conf.expected
create mode 100644 src/test/hotplug/disk-remove-virtio.conf
create mode 100644 src/test/hotplug/disk-remove-virtio.conf.expected
create mode 100644 src/test/hotplug/disk-skip-ide.conf
create mode 100644 src/test/hotplug/disk-skip-ide.conf.expected
create mode 100644 src/test/hotplug/disk-throttle.conf
create mode 100644 src/test/hotplug/disk-throttle.conf.expected
create mode 100644 src/test/hotplug/fast-plug.conf
create mode 100644 src/test/hotplug/fast-plug.conf.expected
create mode 100644 src/test/hotplug/memory-add.conf
create mode 100644 src/test/hotplug/memory-add.conf.expected
create mode 100644 src/test/hotplug/memory-remove.conf
create mode 100644 src/test/hotplug/memory-remove.conf.expected
create mode 100644 src/test/hotplug/net-add-fail.conf
create mode 100644 src/test/hotplug/net-add-fail.conf.expected
create mode 100644 src/test/hotplug/net-add-pci-bridge.conf
create mode 100644 src/test/hotplug/net-add-pci-bridge.conf.expected
create mode 100644 src/test/hotplug/net-add.conf
create mode 100644 src/test/hotplug/net-add.conf.expected
create mode 100644 src/test/hotplug/net-change-bridge.conf
create mode 100644 src/test/hotplug/net-change-bridge.conf.expected
create mode 100644 src/test/hotplug/net-change-model.conf
create mode 100644 src/test/hotplug/net-change-model.conf.expected
create mode 100644 src/test/hotplug/net-change-rate.conf
create mode 100644 src/test/hotplug/net-change-rate.conf.expected
create mode 100644 src/test/hotplug/net-hotplug-disabled.conf
create mode 100644 src/test/hotplug/net-hotplug-disabled.conf.expected
create mode 100644 src/test/hotplug/net-remove.conf
create mode 100644 src/test/hotplug/net-remove.conf.expected
create mode 100644 src/test/hotplug/tablet-disable.conf
create mode 100644 src/test/hotplug/tablet-disable.conf.expected
create mode 100644 src/test/hotplug/tablet-enable.conf
create mode 100644 src/test/hotplug/tablet-enable.conf.expected
create mode 100644 src/test/hotplug/usb-add-spice.conf
create mode 100644 src/test/hotplug/usb-add-spice.conf.expected
create mode 100644 src/test/hotplug/usb-add.conf
create mode 100644 src/test/hotplug/usb-add.conf.expected
create mode 100644 src/test/hotplug/usb-remove-last.conf
create mode 100644 src/test/hotplug/usb-remove-last.conf.expected
diff --git a/src/test/Makefile b/src/test/Makefile
index b539a6b7..35f118e6 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -12,6 +12,12 @@ test_cfg_to_cmd: run_config2command_tests.pl CommandLineMocks.pm cfg2cmd/*.conf
test_cfg_to_cmd_aarch64: run_config2command_tests.pl CommandLineMocks.pm cfg2cmd/aarch64/*.conf
perl -I../ ./run_config2command_tests.pl cfg2cmd/aarch64
+test_hotplug: run_hotplug_tests.pl CommandLineMocks.pm hotplug/*.conf
+ perl -I../ ./run_hotplug_tests.pl
+
+test_hotplug_aarch64: run_hotplug_tests.pl CommandLineMocks.pm hotplug/aarch64/*.conf
+ perl -I../ ./run_hotplug_tests.pl hotplug/aarch64
+
test_qemu_img_convert: run_qemu_img_convert_tests.pl
perl -I../ ./run_qemu_img_convert_tests.pl
diff --git a/src/test/hotplug/aarch64/disk-add-default-machine.conf b/src/test/hotplug/aarch64/disk-add-default-machine.conf
new file mode 100644
index 00000000..a6641772
--- /dev/null
+++ b/src/test/hotplug/aarch64/disk-add-default-machine.conf
@@ -0,0 +1,20 @@
+# TEST: hotplug a disk into an aarch64 VM without explicit machine type uses the default virt machine
+# HOST_ARCH: aarch64
+# QEMU_VERSION: 10.1.0
+arch: aarch64
+bios: ovmf
+bootdisk: scsi0
+cores: 2
+efidisk0: local:8006/vm-8006-disk-2.qcow2,efitype=4m
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
diff --git a/src/test/hotplug/aarch64/disk-add-default-machine.conf.expected b/src/test/hotplug/aarch64/disk-add-default-machine.conf.expected
new file mode 100644
index 00000000..b25f9a70
--- /dev/null
+++ b/src/test/hotplug/aarch64/disk-add-default-machine.conf.expected
@@ -0,0 +1,28 @@
+# recorded actions
+storage activate_volumes ["local:8006/vm-8006-disk-1.qcow2"]
+qmp object-add {"id":"iothread-virtio1","qom-type":"iothread"}
+qmp object-del {"id":"throttle-drive-virtio1"}
+qmp object-add {"id":"throttle-drive-virtio1","limits":{},"qom-type":"throttle-group"}
+qmp blockdev-add {"detect-zeroes":"on","discard":"ignore","driver":"throttle","file":{"cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"qcow2","file":{"aio":"io_uring","cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"file","filename":"/var/lib/vz/images/8006/vm-8006-disk-1.qcow2","node-name":"ef4c217a7f08cf59741edc1790023f5","read-only":false},"node-name":"ff4c217a7f08cf59741edc1790023f5","read-only":false},"node-name":"drive-virtio1","read-only":false,"throttle-group":"throttle-drive-virtio1"}
+hmp device_add driver=virtio-blk-pci,drive=drive-virtio1,id=virtio1,bus=pcie.0,addr=0xb,iothread=iothread-virtio1,write-cache=on
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+arch: aarch64
+bios: ovmf
+bootdisk: scsi0
+cores: 2
+efidisk0: local:8006/vm-8006-disk-2.qcow2,efitype=4m
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/aarch64/disk-add-virtio.conf b/src/test/hotplug/aarch64/disk-add-virtio.conf
new file mode 100644
index 00000000..929f7537
--- /dev/null
+++ b/src/test/hotplug/aarch64/disk-add-virtio.conf
@@ -0,0 +1,20 @@
+# TEST: hotplug a VirtIO block disk into an aarch64 VM uses the pcie root bus
+# HOST_ARCH: aarch64
+arch: aarch64
+bios: ovmf
+bootdisk: scsi0
+cores: 2
+efidisk0: local:8006/vm-8006-disk-2.qcow2,efitype=4m
+machine: virt-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
diff --git a/src/test/hotplug/aarch64/disk-add-virtio.conf.expected b/src/test/hotplug/aarch64/disk-add-virtio.conf.expected
new file mode 100644
index 00000000..f5bc17fa
--- /dev/null
+++ b/src/test/hotplug/aarch64/disk-add-virtio.conf.expected
@@ -0,0 +1,29 @@
+# recorded actions
+storage activate_volumes ["local:8006/vm-8006-disk-1.qcow2"]
+qmp object-add {"id":"iothread-virtio1","qom-type":"iothread"}
+qmp object-del {"id":"throttle-drive-virtio1"}
+qmp object-add {"id":"throttle-drive-virtio1","limits":{},"qom-type":"throttle-group"}
+qmp blockdev-add {"detect-zeroes":"on","discard":"ignore","driver":"throttle","file":{"cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"qcow2","file":{"aio":"io_uring","cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"file","filename":"/var/lib/vz/images/8006/vm-8006-disk-1.qcow2","node-name":"ef4c217a7f08cf59741edc1790023f5","read-only":false},"node-name":"ff4c217a7f08cf59741edc1790023f5","read-only":false},"node-name":"drive-virtio1","read-only":false,"throttle-group":"throttle-drive-virtio1"}
+hmp device_add driver=virtio-blk-pci,drive=drive-virtio1,id=virtio1,bus=pcie.0,addr=0xb,iothread=iothread-virtio1,write-cache=on
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+arch: aarch64
+bios: ovmf
+bootdisk: scsi0
+cores: 2
+efidisk0: local:8006/vm-8006-disk-2.qcow2,efitype=4m
+machine: virt-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/aarch64/net-add-pci-bridge.conf b/src/test/hotplug/aarch64/net-add-pci-bridge.conf
new file mode 100644
index 00000000..3e5fcc33
--- /dev/null
+++ b/src/test/hotplug/aarch64/net-add-pci-bridge.conf
@@ -0,0 +1,20 @@
+# TEST: hotplug a network device on the second PCI bridge of an aarch64 VM
+# HOST_ARCH: aarch64
+arch: aarch64
+bios: ovmf
+bootdisk: scsi0
+cores: 2
+efidisk0: local:8006/vm-8006-disk-2.qcow2,efitype=4m
+machine: virt-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+net6: virtio=BA:DC:0F:FE:E0:06,bridge=vmbr0
diff --git a/src/test/hotplug/aarch64/net-add-pci-bridge.conf.expected b/src/test/hotplug/aarch64/net-add-pci-bridge.conf.expected
new file mode 100644
index 00000000..cc3fef58
--- /dev/null
+++ b/src/test/hotplug/aarch64/net-add-pci-bridge.conf.expected
@@ -0,0 +1,28 @@
+# recorded actions
+sdn add_next_free_cidr ["vmbr0","hotplug","BA:DC:0F:FE:E0:06","8006",null,1]
+sdn add_dhcp_mapping ["vmbr0","BA:DC:0F:FE:E0:06","8006","hotplug"]
+qmp netdev_add {"downscript":"/usr/libexec/qemu-server/pve-bridgedown","id":"net6","ifname":"tap8006i6","script":"/usr/libexec/qemu-server/pve-bridge-hotplug","type":"tap","vhost":true}
+hmp device_add driver=virtio-net-pci,mac=BA:DC:0F:FE:E0:06,netdev=net6,bus=pci.1,addr=0x1,id=net6,rx_queue_size=1024,tx_queue_size=256,host_mtu=1500
+qmp set_link {"name":"net6","up":true}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+arch: aarch64
+bios: ovmf
+bootdisk: scsi0
+cores: 2
+efidisk0: local:8006/vm-8006-disk-2.qcow2,efitype=4m
+machine: virt-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+net6: virtio=BA:DC:0F:FE:E0:06,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/aarch64/net-add.conf b/src/test/hotplug/aarch64/net-add.conf
new file mode 100644
index 00000000..31f43011
--- /dev/null
+++ b/src/test/hotplug/aarch64/net-add.conf
@@ -0,0 +1,20 @@
+# TEST: hotplug a network device into an aarch64 VM uses the pcie root bus
+# HOST_ARCH: aarch64
+arch: aarch64
+bios: ovmf
+bootdisk: scsi0
+cores: 2
+efidisk0: local:8006/vm-8006-disk-2.qcow2,efitype=4m
+machine: virt-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+net1: virtio=BA:DC:0F:FE:E0:01,bridge=vmbr0,firewall=1
diff --git a/src/test/hotplug/aarch64/net-add.conf.expected b/src/test/hotplug/aarch64/net-add.conf.expected
new file mode 100644
index 00000000..6a158151
--- /dev/null
+++ b/src/test/hotplug/aarch64/net-add.conf.expected
@@ -0,0 +1,28 @@
+# recorded actions
+sdn add_next_free_cidr ["vmbr0","hotplug","BA:DC:0F:FE:E0:01","8006",null,1]
+sdn add_dhcp_mapping ["vmbr0","BA:DC:0F:FE:E0:01","8006","hotplug"]
+qmp netdev_add {"downscript":"/usr/libexec/qemu-server/pve-bridgedown","id":"net1","ifname":"tap8006i1","script":"/usr/libexec/qemu-server/pve-bridge-hotplug","type":"tap","vhost":true}
+hmp device_add driver=virtio-net-pci,mac=BA:DC:0F:FE:E0:01,netdev=net1,bus=pcie.0,addr=0x13,id=net1,rx_queue_size=1024,tx_queue_size=256,host_mtu=1500
+qmp set_link {"name":"net1","up":true}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+arch: aarch64
+bios: ovmf
+bootdisk: scsi0
+cores: 2
+efidisk0: local:8006/vm-8006-disk-2.qcow2,efitype=4m
+machine: virt-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+net1: virtio=BA:DC:0F:FE:E0:01,bridge=vmbr0,firewall=1
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/aarch64/tablet-disable.conf b/src/test/hotplug/aarch64/tablet-disable.conf
new file mode 100644
index 00000000..5dccd220
--- /dev/null
+++ b/src/test/hotplug/aarch64/tablet-disable.conf
@@ -0,0 +1,20 @@
+# TEST: disabling the tablet device on aarch64 also hot-unplugs the keyboard
+# HOST_ARCH: aarch64
+arch: aarch64
+bios: ovmf
+bootdisk: scsi0
+cores: 2
+efidisk0: local:8006/vm-8006-disk-2.qcow2,efitype=4m
+machine: virt-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+tablet: 0
diff --git a/src/test/hotplug/aarch64/tablet-disable.conf.expected b/src/test/hotplug/aarch64/tablet-disable.conf.expected
new file mode 100644
index 00000000..5c3343a0
--- /dev/null
+++ b/src/test/hotplug/aarch64/tablet-disable.conf.expected
@@ -0,0 +1,25 @@
+# recorded actions
+hmp device_del tablet
+hmp device_del keyboard
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+arch: aarch64
+bios: ovmf
+bootdisk: scsi0
+cores: 2
+efidisk0: local:8006/vm-8006-disk-2.qcow2,efitype=4m
+machine: virt-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+tablet: 0
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/aarch64/x86-guest-net-add.conf b/src/test/hotplug/aarch64/x86-guest-net-add.conf
new file mode 100644
index 00000000..21a3bb5a
--- /dev/null
+++ b/src/test/hotplug/aarch64/x86-guest-net-add.conf
@@ -0,0 +1,18 @@
+# TEST: hotplug a network device into an emulated x86_64 VM on an aarch64 host does not use vhost
+# HOST_ARCH: aarch64
+arch: x86_64
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+net1: virtio=BA:DC:0F:FE:E0:01,bridge=vmbr0,firewall=1
diff --git a/src/test/hotplug/aarch64/x86-guest-net-add.conf.expected b/src/test/hotplug/aarch64/x86-guest-net-add.conf.expected
new file mode 100644
index 00000000..371a021f
--- /dev/null
+++ b/src/test/hotplug/aarch64/x86-guest-net-add.conf.expected
@@ -0,0 +1,26 @@
+# recorded actions
+sdn add_next_free_cidr ["vmbr0","hotplug","BA:DC:0F:FE:E0:01","8006",null,1]
+sdn add_dhcp_mapping ["vmbr0","BA:DC:0F:FE:E0:01","8006","hotplug"]
+qmp netdev_add {"downscript":"/usr/libexec/qemu-server/pve-bridgedown","id":"net1","ifname":"tap8006i1","script":"/usr/libexec/qemu-server/pve-bridge-hotplug","type":"tap"}
+hmp device_add driver=virtio-net-pci,mac=BA:DC:0F:FE:E0:01,netdev=net1,bus=pci.0,addr=0x13,id=net1,rx_queue_size=1024,tx_queue_size=256,host_mtu=1500
+qmp set_link {"name":"net1","up":true}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+arch: x86_64
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+net1: virtio=BA:DC:0F:FE:E0:01,bridge=vmbr0,firewall=1
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/agent-fstrim.conf b/src/test/hotplug/agent-fstrim.conf
new file mode 100644
index 00000000..1cdd489e
--- /dev/null
+++ b/src/test/hotplug/agent-fstrim.conf
@@ -0,0 +1,17 @@
+# TEST: the fstrim_cloned_disks agent option can be changed on a running VM
+agent: 1
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+agent: 1,fstrim_cloned_disks=1
diff --git a/src/test/hotplug/agent-fstrim.conf.expected b/src/test/hotplug/agent-fstrim.conf.expected
new file mode 100644
index 00000000..bd524a22
--- /dev/null
+++ b/src/test/hotplug/agent-fstrim.conf.expected
@@ -0,0 +1,20 @@
+# recorded actions
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+agent: 1,fstrim_cloned_disks=1
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/balloon.conf b/src/test/hotplug/balloon.conf
new file mode 100644
index 00000000..67b5a1a9
--- /dev/null
+++ b/src/test/hotplug/balloon.conf
@@ -0,0 +1,18 @@
+# TEST: changing the balloon target with shares=0 is applied to the running VM
+balloon: 1024
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+shares: 0
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+balloon: 512
diff --git a/src/test/hotplug/balloon.conf.expected b/src/test/hotplug/balloon.conf.expected
new file mode 100644
index 00000000..ff29f4d3
--- /dev/null
+++ b/src/test/hotplug/balloon.conf.expected
@@ -0,0 +1,22 @@
+# recorded actions
+qmp balloon {"value":536870912}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+balloon: 512
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+shares: 0
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/cdrom-eject.conf b/src/test/hotplug/cdrom-eject.conf
new file mode 100644
index 00000000..c9333952
--- /dev/null
+++ b/src/test/hotplug/cdrom-eject.conf
@@ -0,0 +1,17 @@
+# TEST: removing the ISO of a CD-ROM drive ejects the medium and removes the block nodes
+bootdisk: scsi0
+cores: 2
+ide2: local:iso/debian-13.iso,media=cdrom
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+ide2: none,media=cdrom
diff --git a/src/test/hotplug/cdrom-eject.conf.expected b/src/test/hotplug/cdrom-eject.conf.expected
new file mode 100644
index 00000000..52e53550
--- /dev/null
+++ b/src/test/hotplug/cdrom-eject.conf.expected
@@ -0,0 +1,24 @@
+# recorded actions
+qmp blockdev-open-tray {"force":true,"id":"ide2"}
+qmp blockdev-remove-medium {"id":"ide2"}
+qmp blockdev-del {"node-name":"drive-ide2"}
+qmp object-del {"id":"throttle-drive-ide2"}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+ide2: none,media=cdrom
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/cpu-add-too-many.conf b/src/test/hotplug/cpu-add-too-many.conf
new file mode 100644
index 00000000..aedc6656
--- /dev/null
+++ b/src/test/hotplug/cpu-add-too-many.conf
@@ -0,0 +1,18 @@
+# TEST: hotplugging more vCPUs than the configured maximum fails
+bootdisk: scsi0
+cores: 4
+hotplug: network,disk,usb,cpu
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vcpus: 2
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+vcpus: 8
diff --git a/src/test/hotplug/cpu-add-too-many.conf.expected b/src/test/hotplug/cpu-add-too-many.conf.expected
new file mode 100644
index 00000000..c1fa1f96
--- /dev/null
+++ b/src/test/hotplug/cpu-add-too-many.conf.expected
@@ -0,0 +1,25 @@
+# recorded actions
+
+# hotplug errors
+vcpus: hotplug problem - you can't add more vcpus than maxcpus
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 4
+hotplug: network,disk,usb,cpu
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vcpus: 2
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+vcpus: 8
diff --git a/src/test/hotplug/cpu-add.conf b/src/test/hotplug/cpu-add.conf
new file mode 100644
index 00000000..a99d2739
--- /dev/null
+++ b/src/test/hotplug/cpu-add.conf
@@ -0,0 +1,18 @@
+# TEST: hotplug vCPUs
+bootdisk: scsi0
+cores: 4
+hotplug: network,disk,usb,cpu
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vcpus: 2
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+vcpus: 4
diff --git a/src/test/hotplug/cpu-add.conf.expected b/src/test/hotplug/cpu-add.conf.expected
new file mode 100644
index 00000000..4ea0a98c
--- /dev/null
+++ b/src/test/hotplug/cpu-add.conf.expected
@@ -0,0 +1,23 @@
+# recorded actions
+hmp device_add driver=kvm64-x86_64-cpu,id=cpu3,socket-id=0,core-id=2,thread-id=0
+hmp device_add driver=kvm64-x86_64-cpu,id=cpu4,socket-id=0,core-id=3,thread-id=0
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 4
+hotplug: network,disk,usb,cpu
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vcpus: 4
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/cpu-remove.conf b/src/test/hotplug/cpu-remove.conf
new file mode 100644
index 00000000..884220da
--- /dev/null
+++ b/src/test/hotplug/cpu-remove.conf
@@ -0,0 +1,18 @@
+# TEST: hot-unplug vCPUs starting with the highest one
+bootdisk: scsi0
+cores: 4
+hotplug: network,disk,usb,cpu
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vcpus: 4
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+vcpus: 2
diff --git a/src/test/hotplug/cpu-remove.conf.expected b/src/test/hotplug/cpu-remove.conf.expected
new file mode 100644
index 00000000..fd548448
--- /dev/null
+++ b/src/test/hotplug/cpu-remove.conf.expected
@@ -0,0 +1,23 @@
+# recorded actions
+hmp device_del cpu4
+hmp device_del cpu3
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 4
+hotplug: network,disk,usb,cpu
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vcpus: 2
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/cpulimit.conf b/src/test/hotplug/cpulimit.conf
new file mode 100644
index 00000000..066e1eb2
--- /dev/null
+++ b/src/test/hotplug/cpulimit.conf
@@ -0,0 +1,16 @@
+# TEST: changing the cpulimit is applied via cgroup
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+cpulimit: 2
diff --git a/src/test/hotplug/cpulimit.conf.expected b/src/test/hotplug/cpulimit.conf.expected
new file mode 100644
index 00000000..2dc5b68e
--- /dev/null
+++ b/src/test/hotplug/cpulimit.conf.expected
@@ -0,0 +1,21 @@
+# recorded actions
+cgroup change_cpu_quota [200,null]
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+cpulimit: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/disk-add-default-machine.conf b/src/test/hotplug/disk-add-default-machine.conf
new file mode 100644
index 00000000..db5cb7b4
--- /dev/null
+++ b/src/test/hotplug/disk-add-default-machine.conf
@@ -0,0 +1,16 @@
+# TEST: hotplug a disk into a VM without explicit machine type uses the default machine of the running QEMU
+# QEMU_VERSION: 10.1.0
+bootdisk: scsi0
+cores: 2
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
diff --git a/src/test/hotplug/disk-add-default-machine.conf.expected b/src/test/hotplug/disk-add-default-machine.conf.expected
new file mode 100644
index 00000000..1edb018f
--- /dev/null
+++ b/src/test/hotplug/disk-add-default-machine.conf.expected
@@ -0,0 +1,25 @@
+# recorded actions
+storage activate_volumes ["local:8006/vm-8006-disk-1.qcow2"]
+qmp object-add {"id":"iothread-virtio1","qom-type":"iothread"}
+qmp object-del {"id":"throttle-drive-virtio1"}
+qmp object-add {"id":"throttle-drive-virtio1","limits":{},"qom-type":"throttle-group"}
+qmp blockdev-add {"detect-zeroes":"on","discard":"ignore","driver":"throttle","file":{"cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"qcow2","file":{"aio":"io_uring","cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"file","filename":"/var/lib/vz/images/8006/vm-8006-disk-1.qcow2","node-name":"ef4c217a7f08cf59741edc1790023f5","read-only":false},"node-name":"ff4c217a7f08cf59741edc1790023f5","read-only":false},"node-name":"drive-virtio1","read-only":false,"throttle-group":"throttle-drive-virtio1"}
+hmp device_add driver=virtio-blk-pci,drive=drive-virtio1,id=virtio1,bus=pci.0,addr=0xb,iothread=iothread-virtio1,write-cache=on
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/disk-add-legacy-drive.conf b/src/test/hotplug/disk-add-legacy-drive.conf
new file mode 100644
index 00000000..e889bb71
--- /dev/null
+++ b/src/test/hotplug/disk-add-legacy-drive.conf
@@ -0,0 +1,16 @@
+# TEST: hotplug a disk with machine version < 10.0 uses the legacy -drive backend
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-9.2
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+virtio1: local:8006/vm-8006-disk-1.qcow2,size=32G
diff --git a/src/test/hotplug/disk-add-legacy-drive.conf.expected b/src/test/hotplug/disk-add-legacy-drive.conf.expected
new file mode 100644
index 00000000..9a949342
--- /dev/null
+++ b/src/test/hotplug/disk-add-legacy-drive.conf.expected
@@ -0,0 +1,23 @@
+# recorded actions
+storage activate_volumes ["local:8006/vm-8006-disk-1.qcow2"]
+hmp drive_add auto "file=/var/lib/vz/images/8006/vm-8006-disk-1.qcow2,if=none,id=drive-virtio1,format=qcow2,cache=none,aio=io_uring,detect-zeroes=on"
+hmp device_add driver=virtio-blk-pci,drive=drive-virtio1,id=virtio1,bus=pci.0,addr=0xb
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-9.2
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+virtio1: local:8006/vm-8006-disk-1.qcow2,size=32G
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/disk-add-scsi-single.conf b/src/test/hotplug/disk-add-scsi-single.conf
new file mode 100644
index 00000000..23b3f275
--- /dev/null
+++ b/src/test/hotplug/disk-add-scsi-single.conf
@@ -0,0 +1,16 @@
+# TEST: hotplug a SCSI disk with virtio-scsi-single adds a dedicated controller with iothread
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,iothread=1,size=32G
+scsihw: virtio-scsi-single
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+scsi1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
diff --git a/src/test/hotplug/disk-add-scsi-single.conf.expected b/src/test/hotplug/disk-add-scsi-single.conf.expected
new file mode 100644
index 00000000..e6d2a7b8
--- /dev/null
+++ b/src/test/hotplug/disk-add-scsi-single.conf.expected
@@ -0,0 +1,27 @@
+# recorded actions
+storage activate_volumes ["local:8006/vm-8006-disk-1.qcow2"]
+qmp object-add {"id":"iothread-virtioscsi1","qom-type":"iothread"}
+hmp device_add driver=virtio-scsi-pci,id=virtioscsi1,bus=pci.3,addr=0x2,iothread=iothread-virtioscsi1
+qmp object-del {"id":"throttle-drive-scsi1"}
+qmp object-add {"id":"throttle-drive-scsi1","limits":{},"qom-type":"throttle-group"}
+qmp blockdev-add {"detect-zeroes":"on","discard":"ignore","driver":"throttle","file":{"cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"qcow2","file":{"aio":"io_uring","cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"file","filename":"/var/lib/vz/images/8006/vm-8006-disk-1.qcow2","node-name":"e70a10e5fb0473d3fc55686f720db61","read-only":false},"node-name":"f70a10e5fb0473d3fc55686f720db61","read-only":false},"node-name":"drive-scsi1","read-only":false,"throttle-group":"throttle-drive-scsi1"}
+hmp device_add driver=scsi-hd,bus=virtioscsi1.0,channel=0,scsi-id=0,lun=1,drive=drive-scsi1,id=scsi1,device_id=drive-scsi1,write-cache=on
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,iothread=1,size=32G
+scsi1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
+scsihw: virtio-scsi-single
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/disk-add-virtio.conf b/src/test/hotplug/disk-add-virtio.conf
new file mode 100644
index 00000000..ca750fde
--- /dev/null
+++ b/src/test/hotplug/disk-add-virtio.conf
@@ -0,0 +1,16 @@
+# TEST: hotplug a new VirtIO block disk with iothread
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
diff --git a/src/test/hotplug/disk-add-virtio.conf.expected b/src/test/hotplug/disk-add-virtio.conf.expected
new file mode 100644
index 00000000..3b5999fb
--- /dev/null
+++ b/src/test/hotplug/disk-add-virtio.conf.expected
@@ -0,0 +1,26 @@
+# recorded actions
+storage activate_volumes ["local:8006/vm-8006-disk-1.qcow2"]
+qmp object-add {"id":"iothread-virtio1","qom-type":"iothread"}
+qmp object-del {"id":"throttle-drive-virtio1"}
+qmp object-add {"id":"throttle-drive-virtio1","limits":{},"qom-type":"throttle-group"}
+qmp blockdev-add {"detect-zeroes":"on","discard":"ignore","driver":"throttle","file":{"cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"qcow2","file":{"aio":"io_uring","cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"file","filename":"/var/lib/vz/images/8006/vm-8006-disk-1.qcow2","node-name":"ef4c217a7f08cf59741edc1790023f5","read-only":false},"node-name":"ff4c217a7f08cf59741edc1790023f5","read-only":false},"node-name":"drive-virtio1","read-only":false,"throttle-group":"throttle-drive-virtio1"}
+hmp device_add driver=virtio-blk-pci,drive=drive-virtio1,id=virtio1,bus=pci.0,addr=0xb,iothread=iothread-virtio1,write-cache=on
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/disk-remove-bootdisk.conf b/src/test/hotplug/disk-remove-bootdisk.conf
new file mode 100644
index 00000000..b44c9a94
--- /dev/null
+++ b/src/test/hotplug/disk-remove-bootdisk.conf
@@ -0,0 +1,16 @@
+# TEST: the boot disk cannot be hot-unplugged
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+delete: scsi0
diff --git a/src/test/hotplug/disk-remove-bootdisk.conf.expected b/src/test/hotplug/disk-remove-bootdisk.conf.expected
new file mode 100644
index 00000000..0d2a30d0
--- /dev/null
+++ b/src/test/hotplug/disk-remove-bootdisk.conf.expected
@@ -0,0 +1,23 @@
+# recorded actions
+
+# hotplug errors
+scsi0: hotplug problem - can't unplug bootdisk 'scsi0'
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+delete: scsi0
diff --git a/src/test/hotplug/disk-remove-busy.conf b/src/test/hotplug/disk-remove-busy.conf
new file mode 100644
index 00000000..9eddc8ba
--- /dev/null
+++ b/src/test/hotplug/disk-remove-busy.conf
@@ -0,0 +1,18 @@
+# TEST: a disk that is not released by the guest stays in the config
+# FAIL_DEVICE_DEL: virtio1
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+delete: virtio1
diff --git a/src/test/hotplug/disk-remove-busy.conf.expected b/src/test/hotplug/disk-remove-busy.conf.expected
new file mode 100644
index 00000000..3fc32469
--- /dev/null
+++ b/src/test/hotplug/disk-remove-busy.conf.expected
@@ -0,0 +1,25 @@
+# recorded actions
+hmp device_del virtio1
+
+# hotplug errors
+virtio1: hotplug problem - error on hot-unplugging device 'virtio1' - still busy in guest?
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+delete: virtio1
diff --git a/src/test/hotplug/disk-remove-scsi-single.conf b/src/test/hotplug/disk-remove-scsi-single.conf
new file mode 100644
index 00000000..7074eb79
--- /dev/null
+++ b/src/test/hotplug/disk-remove-scsi-single.conf
@@ -0,0 +1,17 @@
+# TEST: hot-unplug a SCSI disk with virtio-scsi-single also removes its controller and iothread
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,iothread=1,size=32G
+scsi1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
+scsihw: virtio-scsi-single
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+delete: scsi1
diff --git a/src/test/hotplug/disk-remove-scsi-single.conf.expected b/src/test/hotplug/disk-remove-scsi-single.conf.expected
new file mode 100644
index 00000000..c9c39d05
--- /dev/null
+++ b/src/test/hotplug/disk-remove-scsi-single.conf.expected
@@ -0,0 +1,25 @@
+# recorded actions
+hmp device_del scsi1
+qmp blockdev-del {"node-name":"drive-scsi1"}
+qmp object-del {"id":"throttle-drive-scsi1"}
+hmp device_del virtioscsi1
+qmp object-del {"id":"iothread-virtioscsi1"}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,iothread=1,size=32G
+scsihw: virtio-scsi-single
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+unused0: local:8006/vm-8006-disk-1.qcow2
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/disk-remove-virtio.conf b/src/test/hotplug/disk-remove-virtio.conf
new file mode 100644
index 00000000..f6f334a8
--- /dev/null
+++ b/src/test/hotplug/disk-remove-virtio.conf
@@ -0,0 +1,17 @@
+# TEST: hot-unplug a VirtIO block disk with iothread and keep the volume as unused
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+delete: virtio1
diff --git a/src/test/hotplug/disk-remove-virtio.conf.expected b/src/test/hotplug/disk-remove-virtio.conf.expected
new file mode 100644
index 00000000..c0f37833
--- /dev/null
+++ b/src/test/hotplug/disk-remove-virtio.conf.expected
@@ -0,0 +1,24 @@
+# recorded actions
+hmp device_del virtio1
+qmp blockdev-del {"node-name":"drive-virtio1"}
+qmp object-del {"id":"throttle-drive-virtio1"}
+qmp object-del {"id":"iothread-virtio1"}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+unused0: local:8006/vm-8006-disk-1.qcow2
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/disk-skip-ide.conf b/src/test/hotplug/disk-skip-ide.conf
new file mode 100644
index 00000000..11f2db6a
--- /dev/null
+++ b/src/test/hotplug/disk-skip-ide.conf
@@ -0,0 +1,16 @@
+# TEST: IDE disks cannot be hotplugged and stay pending
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+ide0: local:8006/vm-8006-disk-1.qcow2,size=32G
diff --git a/src/test/hotplug/disk-skip-ide.conf.expected b/src/test/hotplug/disk-skip-ide.conf.expected
new file mode 100644
index 00000000..bf63ca34
--- /dev/null
+++ b/src/test/hotplug/disk-skip-ide.conf.expected
@@ -0,0 +1,22 @@
+# recorded actions
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+ide0: local:8006/vm-8006-disk-1.qcow2,size=32G
diff --git a/src/test/hotplug/disk-throttle.conf b/src/test/hotplug/disk-throttle.conf
new file mode 100644
index 00000000..f1baf6df
--- /dev/null
+++ b/src/test/hotplug/disk-throttle.conf
@@ -0,0 +1,16 @@
+# TEST: changing the bandwidth limit of a disk is applied to the throttle group
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+scsi0: local:8006/vm-8006-disk-0.qcow2,mbps_rd=50,size=32G
diff --git a/src/test/hotplug/disk-throttle.conf.expected b/src/test/hotplug/disk-throttle.conf.expected
new file mode 100644
index 00000000..d0f3be00
--- /dev/null
+++ b/src/test/hotplug/disk-throttle.conf.expected
@@ -0,0 +1,20 @@
+# recorded actions
+qmp qom-set {"path":"throttle-drive-scsi0","property":"limits","value":{"bps-read":52428800,"bps-read-max":0,"bps-read-max-length":1,"bps-total":0,"bps-total-max":0,"bps-total-max-length":1,"bps-write":0,"bps-write-max":0,"bps-write-max-length":1,"iops-read":0,"iops-read-max":0,"iops-read-max-length":1,"iops-total":0,"iops-total-max":0,"iops-total-max-length":1,"iops-write":0,"iops-write-max":0,"iops-write-max-length":1}}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,mbps_rd=50,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/fast-plug.conf b/src/test/hotplug/fast-plug.conf
new file mode 100644
index 00000000..4746ade9
--- /dev/null
+++ b/src/test/hotplug/fast-plug.conf
@@ -0,0 +1,17 @@
+# TEST: options without impact on the running VM are applied without any action
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+name: renamed
+onboot: 1
diff --git a/src/test/hotplug/fast-plug.conf.expected b/src/test/hotplug/fast-plug.conf.expected
new file mode 100644
index 00000000..3abf55ae
--- /dev/null
+++ b/src/test/hotplug/fast-plug.conf.expected
@@ -0,0 +1,20 @@
+# recorded actions
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: renamed
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+onboot: 1
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/memory-add.conf b/src/test/hotplug/memory-add.conf
new file mode 100644
index 00000000..ac26ed68
--- /dev/null
+++ b/src/test/hotplug/memory-add.conf
@@ -0,0 +1,18 @@
+# TEST: hotplug memory adds DIMMs on top of the existing ones
+bootdisk: scsi0
+cores: 2
+hotplug: network,disk,usb,memory
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+numa: 1
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+memory: 3072
diff --git a/src/test/hotplug/memory-add.conf.expected b/src/test/hotplug/memory-add.conf.expected
new file mode 100644
index 00000000..45e80f02
--- /dev/null
+++ b/src/test/hotplug/memory-add.conf.expected
@@ -0,0 +1,25 @@
+# recorded actions
+qmp object-add {"id":"mem-dimm2","qom-type":"memory-backend-ram","size":536870912}
+qmp device_add {"driver":"pc-dimm","id":"dimm2","memdev":"mem-dimm2","node":0}
+qmp object-add {"id":"mem-dimm3","qom-type":"memory-backend-ram","size":536870912}
+qmp device_add {"driver":"pc-dimm","id":"dimm3","memdev":"mem-dimm3","node":0}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+hotplug: network,disk,usb,memory
+machine: pc-i440fx-10.1
+memory: 3072
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+numa: 1
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/memory-remove.conf b/src/test/hotplug/memory-remove.conf
new file mode 100644
index 00000000..1ac656c6
--- /dev/null
+++ b/src/test/hotplug/memory-remove.conf
@@ -0,0 +1,18 @@
+# TEST: hot-unplug memory removes DIMMs starting with the highest one
+bootdisk: scsi0
+cores: 2
+hotplug: network,disk,usb,memory
+machine: pc-i440fx-10.1
+memory: 3072
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+numa: 1
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+memory: 2048
diff --git a/src/test/hotplug/memory-remove.conf.expected b/src/test/hotplug/memory-remove.conf.expected
new file mode 100644
index 00000000..be3784c2
--- /dev/null
+++ b/src/test/hotplug/memory-remove.conf.expected
@@ -0,0 +1,25 @@
+# recorded actions
+hmp device_del dimm3
+qmp object-del {"id":"mem-dimm3"}
+hmp device_del dimm2
+qmp object-del {"id":"mem-dimm2"}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+hotplug: network,disk,usb,memory
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+numa: 1
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/net-add-fail.conf b/src/test/hotplug/net-add-fail.conf
new file mode 100644
index 00000000..49ddcdc7
--- /dev/null
+++ b/src/test/hotplug/net-add-fail.conf
@@ -0,0 +1,17 @@
+# TEST: a network device that does not show up after hotplug is cleaned up again
+# FAIL_DEVICE_ADD: net1
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+net1: virtio=BA:DC:0F:FE:E0:01,bridge=vmbr0,firewall=1
diff --git a/src/test/hotplug/net-add-fail.conf.expected b/src/test/hotplug/net-add-fail.conf.expected
new file mode 100644
index 00000000..53133dc8
--- /dev/null
+++ b/src/test/hotplug/net-add-fail.conf.expected
@@ -0,0 +1,28 @@
+# recorded actions
+sdn add_next_free_cidr ["vmbr0","hotplug","BA:DC:0F:FE:E0:01","8006",null,1]
+sdn add_dhcp_mapping ["vmbr0","BA:DC:0F:FE:E0:01","8006","hotplug"]
+qmp netdev_add {"downscript":"/usr/libexec/qemu-server/pve-bridgedown","id":"net1","ifname":"tap8006i1","script":"/usr/libexec/qemu-server/pve-bridge-hotplug","type":"tap","vhost":true}
+hmp device_add driver=virtio-net-pci,mac=BA:DC:0F:FE:E0:01,netdev=net1,bus=pci.0,addr=0x13,id=net1,rx_queue_size=1024,tx_queue_size=256,host_mtu=1500
+qmp netdev_del {"id":"net1"}
+
+# hotplug errors
+net1: hotplug problem - error on hotplug device 'net1'
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+net1: virtio=BA:DC:0F:FE:E0:01,bridge=vmbr0,firewall=1
diff --git a/src/test/hotplug/net-add-pci-bridge.conf b/src/test/hotplug/net-add-pci-bridge.conf
new file mode 100644
index 00000000..3d440d28
--- /dev/null
+++ b/src/test/hotplug/net-add-pci-bridge.conf
@@ -0,0 +1,16 @@
+# TEST: hotplug a network device on the second PCI bridge
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+net6: virtio=BA:DC:0F:FE:E0:06,bridge=vmbr0
diff --git a/src/test/hotplug/net-add-pci-bridge.conf.expected b/src/test/hotplug/net-add-pci-bridge.conf.expected
new file mode 100644
index 00000000..d38c03b8
--- /dev/null
+++ b/src/test/hotplug/net-add-pci-bridge.conf.expected
@@ -0,0 +1,25 @@
+# recorded actions
+sdn add_next_free_cidr ["vmbr0","hotplug","BA:DC:0F:FE:E0:06","8006",null,1]
+sdn add_dhcp_mapping ["vmbr0","BA:DC:0F:FE:E0:06","8006","hotplug"]
+qmp netdev_add {"downscript":"/usr/libexec/qemu-server/pve-bridgedown","id":"net6","ifname":"tap8006i6","script":"/usr/libexec/qemu-server/pve-bridge-hotplug","type":"tap","vhost":true}
+hmp device_add driver=virtio-net-pci,mac=BA:DC:0F:FE:E0:06,netdev=net6,bus=pci.1,addr=0x1,id=net6,rx_queue_size=1024,tx_queue_size=256,host_mtu=1500
+qmp set_link {"name":"net6","up":true}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+net6: virtio=BA:DC:0F:FE:E0:06,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/net-add.conf b/src/test/hotplug/net-add.conf
new file mode 100644
index 00000000..b4bb3568
--- /dev/null
+++ b/src/test/hotplug/net-add.conf
@@ -0,0 +1,16 @@
+# TEST: hotplug a new network device
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+net1: virtio=BA:DC:0F:FE:E0:01,bridge=vmbr0,firewall=1
diff --git a/src/test/hotplug/net-add.conf.expected b/src/test/hotplug/net-add.conf.expected
new file mode 100644
index 00000000..386e4e23
--- /dev/null
+++ b/src/test/hotplug/net-add.conf.expected
@@ -0,0 +1,25 @@
+# recorded actions
+sdn add_next_free_cidr ["vmbr0","hotplug","BA:DC:0F:FE:E0:01","8006",null,1]
+sdn add_dhcp_mapping ["vmbr0","BA:DC:0F:FE:E0:01","8006","hotplug"]
+qmp netdev_add {"downscript":"/usr/libexec/qemu-server/pve-bridgedown","id":"net1","ifname":"tap8006i1","script":"/usr/libexec/qemu-server/pve-bridge-hotplug","type":"tap","vhost":true}
+hmp device_add driver=virtio-net-pci,mac=BA:DC:0F:FE:E0:01,netdev=net1,bus=pci.0,addr=0x13,id=net1,rx_queue_size=1024,tx_queue_size=256,host_mtu=1500
+qmp set_link {"name":"net1","up":true}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+net1: virtio=BA:DC:0F:FE:E0:01,bridge=vmbr0,firewall=1
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/net-change-bridge.conf b/src/test/hotplug/net-change-bridge.conf
new file mode 100644
index 00000000..562d6dce
--- /dev/null
+++ b/src/test/hotplug/net-change-bridge.conf
@@ -0,0 +1,16 @@
+# TEST: changing bridge and VLAN tag re-plugs the tap device without replacing the device
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr1,tag=100
diff --git a/src/test/hotplug/net-change-bridge.conf.expected b/src/test/hotplug/net-change-bridge.conf.expected
new file mode 100644
index 00000000..f1b3acf1
--- /dev/null
+++ b/src/test/hotplug/net-change-bridge.conf.expected
@@ -0,0 +1,25 @@
+# recorded actions
+net tap_unplug ["tap8006i0"]
+qmp set_link {"name":"net0","up":false}
+sdn del_ips_from_mac ["vmbr0","A2:C0:43:77:08:A0","hotplug"]
+sdn add_next_free_cidr ["vmbr1","hotplug","A2:C0:43:77:08:A0","8006",null,1]
+net tap_plug ["tap8006i0","vmbr1","100",null,null,null]
+qmp set_link {"name":"net0","up":true}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr1,tag=100
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/net-change-model.conf b/src/test/hotplug/net-change-model.conf
new file mode 100644
index 00000000..3dccc1ff
--- /dev/null
+++ b/src/test/hotplug/net-change-model.conf
@@ -0,0 +1,16 @@
+# TEST: changing the model replaces the network device
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+net0: e1000=A2:C0:43:77:08:A0,bridge=vmbr0
diff --git a/src/test/hotplug/net-change-model.conf.expected b/src/test/hotplug/net-change-model.conf.expected
new file mode 100644
index 00000000..fc7b70bd
--- /dev/null
+++ b/src/test/hotplug/net-change-model.conf.expected
@@ -0,0 +1,27 @@
+# recorded actions
+hmp device_del net0
+qmp netdev_del {"id":"net0"}
+sdn del_ips_from_mac ["vmbr0","A2:C0:43:77:08:A0","hotplug"]
+sdn add_next_free_cidr ["vmbr0","hotplug","A2:C0:43:77:08:A0","8006",null,1]
+sdn add_dhcp_mapping ["vmbr0","A2:C0:43:77:08:A0","8006","hotplug"]
+qmp netdev_add {"downscript":"/usr/libexec/qemu-server/pve-bridgedown","id":"net0","ifname":"tap8006i0","script":"/usr/libexec/qemu-server/pve-bridge-hotplug","type":"tap"}
+hmp device_add driver=e1000,mac=A2:C0:43:77:08:A0,netdev=net0,bus=pci.0,addr=0x12,id=net0
+qmp set_link {"name":"net0","up":true}
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: e1000=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/net-change-rate.conf b/src/test/hotplug/net-change-rate.conf
new file mode 100644
index 00000000..09d91d04
--- /dev/null
+++ b/src/test/hotplug/net-change-rate.conf
@@ -0,0 +1,16 @@
+# TEST: changing only the rate limit is applied to the tap device
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0,rate=10
diff --git a/src/test/hotplug/net-change-rate.conf.expected b/src/test/hotplug/net-change-rate.conf.expected
new file mode 100644
index 00000000..321b00d3
--- /dev/null
+++ b/src/test/hotplug/net-change-rate.conf.expected
@@ -0,0 +1,20 @@
+# recorded actions
+net tap_rate_limit ["tap8006i0","10"]
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0,rate=10
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/net-hotplug-disabled.conf b/src/test/hotplug/net-hotplug-disabled.conf
new file mode 100644
index 00000000..63b902b9
--- /dev/null
+++ b/src/test/hotplug/net-hotplug-disabled.conf
@@ -0,0 +1,17 @@
+# TEST: a new network device stays pending if network hotplug is disabled
+bootdisk: scsi0
+cores: 2
+hotplug: disk
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+net1: virtio=BA:DC:0F:FE:E0:01,bridge=vmbr0
diff --git a/src/test/hotplug/net-hotplug-disabled.conf.expected b/src/test/hotplug/net-hotplug-disabled.conf.expected
new file mode 100644
index 00000000..2dcf7663
--- /dev/null
+++ b/src/test/hotplug/net-hotplug-disabled.conf.expected
@@ -0,0 +1,23 @@
+# recorded actions
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+hotplug: disk
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+net1: virtio=BA:DC:0F:FE:E0:01,bridge=vmbr0
diff --git a/src/test/hotplug/net-remove.conf b/src/test/hotplug/net-remove.conf
new file mode 100644
index 00000000..95da8dfd
--- /dev/null
+++ b/src/test/hotplug/net-remove.conf
@@ -0,0 +1,17 @@
+# TEST: hot-unplug a network device
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+net1: virtio=BA:DC:0F:FE:E0:01,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+delete: net1
diff --git a/src/test/hotplug/net-remove.conf.expected b/src/test/hotplug/net-remove.conf.expected
new file mode 100644
index 00000000..272cc599
--- /dev/null
+++ b/src/test/hotplug/net-remove.conf.expected
@@ -0,0 +1,22 @@
+# recorded actions
+hmp device_del net1
+qmp netdev_del {"id":"net1"}
+sdn del_ips_from_mac ["vmbr0","BA:DC:0F:FE:E0:01","hotplug"]
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/tablet-disable.conf b/src/test/hotplug/tablet-disable.conf
new file mode 100644
index 00000000..ef2821a2
--- /dev/null
+++ b/src/test/hotplug/tablet-disable.conf
@@ -0,0 +1,16 @@
+# TEST: disabling the tablet device hot-unplugs it
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+tablet: 0
diff --git a/src/test/hotplug/tablet-disable.conf.expected b/src/test/hotplug/tablet-disable.conf.expected
new file mode 100644
index 00000000..50f7ac74
--- /dev/null
+++ b/src/test/hotplug/tablet-disable.conf.expected
@@ -0,0 +1,21 @@
+# recorded actions
+hmp device_del tablet
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+tablet: 0
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/tablet-enable.conf b/src/test/hotplug/tablet-enable.conf
new file mode 100644
index 00000000..8a7dbf95
--- /dev/null
+++ b/src/test/hotplug/tablet-enable.conf
@@ -0,0 +1,17 @@
+# TEST: enabling the tablet device hotplugs it
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+tablet: 0
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+tablet: 1
diff --git a/src/test/hotplug/tablet-enable.conf.expected b/src/test/hotplug/tablet-enable.conf.expected
new file mode 100644
index 00000000..351ba809
--- /dev/null
+++ b/src/test/hotplug/tablet-enable.conf.expected
@@ -0,0 +1,21 @@
+# recorded actions
+hmp device_add driver=usb-tablet,id=tablet,bus=uhci.0,port=1
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+tablet: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/usb-add-spice.conf b/src/test/hotplug/usb-add-spice.conf
new file mode 100644
index 00000000..ead41bbd
--- /dev/null
+++ b/src/test/hotplug/usb-add-spice.conf
@@ -0,0 +1,17 @@
+# TEST: hotplug a SPICE USB redirection device
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+usb0: host=1234:5678
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+usb1: spice
diff --git a/src/test/hotplug/usb-add-spice.conf.expected b/src/test/hotplug/usb-add-spice.conf.expected
new file mode 100644
index 00000000..95741e24
--- /dev/null
+++ b/src/test/hotplug/usb-add-spice.conf.expected
@@ -0,0 +1,23 @@
+# recorded actions
+qmp chardev-add {"backend":{"data":{"type":"usbredir"},"type":"spicevmc"},"id":"usbredirchardev1"}
+hmp device_add driver=usb-redir,chardev=usbredirchardev1,id=usbredirdev1,bus=xhci.0,port=2
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+usb0: host=1234:5678
+usb1: spice
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/usb-add.conf b/src/test/hotplug/usb-add.conf
new file mode 100644
index 00000000..b3514037
--- /dev/null
+++ b/src/test/hotplug/usb-add.conf
@@ -0,0 +1,16 @@
+# TEST: hotplug the first USB device also adds the xhci controller
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+usb0: host=1234:5678
diff --git a/src/test/hotplug/usb-add.conf.expected b/src/test/hotplug/usb-add.conf.expected
new file mode 100644
index 00000000..1c0d1df3
--- /dev/null
+++ b/src/test/hotplug/usb-add.conf.expected
@@ -0,0 +1,22 @@
+# recorded actions
+hmp device_add driver=qemu-xhci,p2=15,p3=15,id=xhci,bus=pci.1,addr=0x1b
+hmp device_add driver=usb-host,bus=xhci.0,port=1,vendorid=0x1234,productid=0x5678,id=usb0
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+usb0: host=1234:5678
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
diff --git a/src/test/hotplug/usb-remove-last.conf b/src/test/hotplug/usb-remove-last.conf
new file mode 100644
index 00000000..e10e93fd
--- /dev/null
+++ b/src/test/hotplug/usb-remove-last.conf
@@ -0,0 +1,17 @@
+# TEST: hot-unplug the last USB device also removes the xhci controller
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+usb0: host=1234:5678
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+delete: usb0
diff --git a/src/test/hotplug/usb-remove-last.conf.expected b/src/test/hotplug/usb-remove-last.conf.expected
new file mode 100644
index 00000000..6092052a
--- /dev/null
+++ b/src/test/hotplug/usb-remove-last.conf.expected
@@ -0,0 +1,21 @@
+# recorded actions
+hmp device_del usb0
+hmp device_del xhci
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH qemu-server 3/6] tests: hotplug: add cases for known defects
2026-09-10 11:00 [PATCH qemu-server 0/6] add hotplug tests and fix uncovered bugs Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 1/6] tests: hotplug: add initial hotplug test harness Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 2/6] tests: hotplug: add some test cases Dominik Csapak
@ 2026-09-10 11:00 ` Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 4/6] tests: hotplug: add test case for adding scsi14 on qemu 11.1 Dominik Csapak
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2026-09-10 11:00 UTC (permalink / raw)
To: pve-devel
adds some test cases that highlight currently known defects:
* adding scsi14 with an lsi controller tries to hotplug a bridge and fails
* a failed disk add with iothread does not clean up an io thread
* hotplugging a tablet device into an aarch64 vm results in errors due
to wrong parameter use of a function.
The tests don't fail, but record the faulty behavior.
Fixes for these are sent as individual patches.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/test/hotplug/aarch64/tablet-enable.conf | 21 ++++++++++++
.../aarch64/tablet-enable.conf.expected | 28 ++++++++++++++++
src/test/hotplug/disk-add-fail.conf | 17 ++++++++++
src/test/hotplug/disk-add-fail.conf.expected | 32 +++++++++++++++++++
src/test/hotplug/disk-add-scsi14-old.conf | 15 +++++++++
.../hotplug/disk-add-scsi14-old.conf.expected | 23 +++++++++++++
6 files changed, 136 insertions(+)
create mode 100644 src/test/hotplug/aarch64/tablet-enable.conf
create mode 100644 src/test/hotplug/aarch64/tablet-enable.conf.expected
create mode 100644 src/test/hotplug/disk-add-fail.conf
create mode 100644 src/test/hotplug/disk-add-fail.conf.expected
create mode 100644 src/test/hotplug/disk-add-scsi14-old.conf
create mode 100644 src/test/hotplug/disk-add-scsi14-old.conf.expected
diff --git a/src/test/hotplug/aarch64/tablet-enable.conf b/src/test/hotplug/aarch64/tablet-enable.conf
new file mode 100644
index 00000000..bb523819
--- /dev/null
+++ b/src/test/hotplug/aarch64/tablet-enable.conf
@@ -0,0 +1,21 @@
+# TEST: enabling the tablet device on aarch64 hotplugs tablet and keyboard
+# HOST_ARCH: aarch64
+arch: aarch64
+bios: ovmf
+bootdisk: scsi0
+cores: 2
+efidisk0: local:8006/vm-8006-disk-2.qcow2,efitype=4m
+machine: virt-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+tablet: 0
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+tablet: 1
diff --git a/src/test/hotplug/aarch64/tablet-enable.conf.expected b/src/test/hotplug/aarch64/tablet-enable.conf.expected
new file mode 100644
index 00000000..708c5432
--- /dev/null
+++ b/src/test/hotplug/aarch64/tablet-enable.conf.expected
@@ -0,0 +1,28 @@
+# recorded actions
+hmp device_add driver=usb-tablet,id=tablet,bus=uhci.0,port=1
+
+# hotplug errors
+tablet: hotplug problem - Too few arguments for subroutine 'PVE::QemuServer::QMPHelpers::qemu_deviceadd' (got 1; expected 2)
+
+# differences to a freshly started VM
+
+# resulting config
+arch: aarch64
+bios: ovmf
+bootdisk: scsi0
+cores: 2
+efidisk0: local:8006/vm-8006-disk-2.qcow2,efitype=4m
+machine: virt-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+tablet: 0
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+tablet: 1
diff --git a/src/test/hotplug/disk-add-fail.conf b/src/test/hotplug/disk-add-fail.conf
new file mode 100644
index 00000000..89851b4f
--- /dev/null
+++ b/src/test/hotplug/disk-add-fail.conf
@@ -0,0 +1,17 @@
+# TEST: a disk that does not show up after hotplug is detached again
+# FAIL_DEVICE_ADD: virtio1
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
diff --git a/src/test/hotplug/disk-add-fail.conf.expected b/src/test/hotplug/disk-add-fail.conf.expected
new file mode 100644
index 00000000..8a36d951
--- /dev/null
+++ b/src/test/hotplug/disk-add-fail.conf.expected
@@ -0,0 +1,32 @@
+# recorded actions
+storage activate_volumes ["local:8006/vm-8006-disk-1.qcow2"]
+qmp object-add {"id":"iothread-virtio1","qom-type":"iothread"}
+qmp object-del {"id":"throttle-drive-virtio1"}
+qmp object-add {"id":"throttle-drive-virtio1","limits":{},"qom-type":"throttle-group"}
+qmp blockdev-add {"detect-zeroes":"on","discard":"ignore","driver":"throttle","file":{"cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"qcow2","file":{"aio":"io_uring","cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"file","filename":"/var/lib/vz/images/8006/vm-8006-disk-1.qcow2","node-name":"ef4c217a7f08cf59741edc1790023f5","read-only":false},"node-name":"ff4c217a7f08cf59741edc1790023f5","read-only":false},"node-name":"drive-virtio1","read-only":false,"throttle-group":"throttle-drive-virtio1"}
+hmp device_add driver=virtio-blk-pci,drive=drive-virtio1,id=virtio1,bus=pci.0,addr=0xb,iothread=iothread-virtio1,write-cache=on
+qmp blockdev-del {"node-name":"drive-virtio1"}
+qmp object-del {"id":"throttle-drive-virtio1"}
+
+# hotplug errors
+virtio1: hotplug problem - error on hotplug device 'virtio1'
+
+# differences to a freshly started VM
+object iothread-virtio1: only present in running VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-10.1
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsihw: virtio-scsi-pci
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+virtio1: local:8006/vm-8006-disk-1.qcow2,iothread=1,size=32G
diff --git a/src/test/hotplug/disk-add-scsi14-old.conf b/src/test/hotplug/disk-add-scsi14-old.conf
new file mode 100644
index 00000000..9f9e72b9
--- /dev/null
+++ b/src/test/hotplug/disk-add-scsi14-old.conf
@@ -0,0 +1,15 @@
+# TEST: hotplugging a SCSI disk whose new LSI controller needs a hotplugged PCI bridge fails on QEMU < 11.1
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-11.0
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+scsi14: local:8006/vm-8006-disk-1.qcow2,size=32G
diff --git a/src/test/hotplug/disk-add-scsi14-old.conf.expected b/src/test/hotplug/disk-add-scsi14-old.conf.expected
new file mode 100644
index 00000000..63c2678d
--- /dev/null
+++ b/src/test/hotplug/disk-add-scsi14-old.conf.expected
@@ -0,0 +1,23 @@
+# recorded actions
+storage activate_volumes ["local:8006/vm-8006-disk-1.qcow2"]
+
+# hotplug errors
+scsi14: hotplug problem - can't hotplug bridge necessary for 'scsihw2'
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+machine: pc-i440fx-11.0
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+scsi14: local:8006/vm-8006-disk-1.qcow2,size=32G
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH qemu-server 4/6] tests: hotplug: add test case for adding scsi14 on qemu 11.1
2026-09-10 11:00 [PATCH qemu-server 0/6] add hotplug tests and fix uncovered bugs Dominik Csapak
` (2 preceding siblings ...)
2026-09-10 11:00 ` [PATCH qemu-server 3/6] tests: hotplug: add cases for known defects Dominik Csapak
@ 2026-09-10 11:00 ` Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 5/6] hotplug: fix vm_deviceplug call for 'tablet' and 'keyboard' on aarch64 Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 6/6] hotplug: remove iothread if adding drive device failed Dominik Csapak
5 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2026-09-10 11:00 UTC (permalink / raw)
To: pve-devel
this logic changed recently so that we add the necessary bridge by
default, check this behavior.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
.../hotplug/disk-add-scsi14-qemu11.1.conf | 15 ++++++++++++
.../disk-add-scsi14-qemu11.1.conf.expected | 24 +++++++++++++++++++
2 files changed, 39 insertions(+)
create mode 100644 src/test/hotplug/disk-add-scsi14-qemu11.1.conf
create mode 100644 src/test/hotplug/disk-add-scsi14-qemu11.1.conf.expected
diff --git a/src/test/hotplug/disk-add-scsi14-qemu11.1.conf b/src/test/hotplug/disk-add-scsi14-qemu11.1.conf
new file mode 100644
index 00000000..76344b41
--- /dev/null
+++ b/src/test/hotplug/disk-add-scsi14-qemu11.1.conf
@@ -0,0 +1,15 @@
+# TEST: hotplugging a SCSI disk whose new LSI controller needs a hotplugged PCI bridge fails
+# QEMU_VERSION: 11.1.0
+bootdisk: scsi0
+cores: 2
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
+
+[PENDING]
+scsi14: local:8006/vm-8006-disk-1.qcow2,size=32G
diff --git a/src/test/hotplug/disk-add-scsi14-qemu11.1.conf.expected b/src/test/hotplug/disk-add-scsi14-qemu11.1.conf.expected
new file mode 100644
index 00000000..c4f81c7a
--- /dev/null
+++ b/src/test/hotplug/disk-add-scsi14-qemu11.1.conf.expected
@@ -0,0 +1,24 @@
+# recorded actions
+storage activate_volumes ["local:8006/vm-8006-disk-1.qcow2"]
+hmp device_add driver=lsi,id=scsihw2,bus=pci.4,addr=0x1
+qmp object-del {"id":"throttle-drive-scsi14"}
+qmp object-add {"id":"throttle-drive-scsi14","limits":{},"qom-type":"throttle-group"}
+qmp blockdev-add {"detect-zeroes":"on","discard":"ignore","driver":"throttle","file":{"cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"qcow2","file":{"aio":"io_uring","cache":{"direct":true,"no-flush":false},"detect-zeroes":"on","discard":"ignore","driver":"file","filename":"/var/lib/vz/images/8006/vm-8006-disk-1.qcow2","node-name":"efa063c24a1f5d262b573eeb1ba16a3","read-only":false},"node-name":"ffa063c24a1f5d262b573eeb1ba16a3","read-only":false},"node-name":"drive-scsi14","read-only":false,"throttle-group":"throttle-drive-scsi14"}
+hmp device_add driver=scsi-hd,bus=scsihw2.0,scsi-id=0,drive=drive-scsi14,id=scsi14,device_id=drive-scsi14,write-cache=on
+
+# hotplug errors
+
+# differences to a freshly started VM
+
+# resulting config
+bootdisk: scsi0
+cores: 2
+memory: 2048
+name: hotplug
+net0: virtio=A2:C0:43:77:08:A0,bridge=vmbr0
+ostype: l26
+scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
+scsi14: local:8006/vm-8006-disk-1.qcow2,size=32G
+smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
+sockets: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH qemu-server 5/6] hotplug: fix vm_deviceplug call for 'tablet' and 'keyboard' on aarch64
2026-09-10 11:00 [PATCH qemu-server 0/6] add hotplug tests and fix uncovered bugs Dominik Csapak
` (3 preceding siblings ...)
2026-09-10 11:00 ` [PATCH qemu-server 4/6] tests: hotplug: add test case for adding scsi14 on qemu 11.1 Dominik Csapak
@ 2026-09-10 11:00 ` Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 6/6] hotplug: remove iothread if adding drive device failed Dominik Csapak
5 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2026-09-10 11:00 UTC (permalink / raw)
To: pve-devel
vm_deviceplug has a 'device' parameter between 'deviceid' and 'arch',
but this was overlooked at these two callsites. This means that the
calls would give the 'machine_type' as 'arch'. On x86 this is harmless,
but on aarch64 systems this produced wrong behavior.
Fix this by simply adding 'undef' as device, since it's not used in
those cases anyway.
Modify the relevant test that highlighted this problem.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/PVE/QemuServer.pm | 18 ++++++++++++------
.../aarch64/tablet-enable.conf.expected | 9 +++------
2 files changed, 15 insertions(+), 12 deletions(-)
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index cf46f635..34073ee8 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -4689,9 +4689,12 @@ sub vmconfig_hotplug_pending {
} elsif ($opt eq 'tablet') {
die "skip\n" if !$hotplug_features->{usb};
if ($defaults->{tablet}) {
- vm_deviceplug($storecfg, $conf, $vmid, 'tablet', $arch, $machine_type);
- vm_deviceplug($storecfg, $conf, $vmid, 'keyboard', $arch, $machine_type)
- if $arch eq 'aarch64';
+ vm_deviceplug(
+ $storecfg, $conf, $vmid, 'tablet', undef, $arch, $machine_type,
+ );
+ vm_deviceplug(
+ $storecfg, $conf, $vmid, 'keyboard', undef, $arch, $machine_type,
+ ) if $arch eq 'aarch64';
} else {
vm_deviceunplug($vmid, $conf, 'tablet');
vm_deviceunplug($vmid, $conf, 'keyboard') if $arch eq 'aarch64';
@@ -4760,9 +4763,12 @@ sub vmconfig_hotplug_pending {
} elsif ($opt eq 'tablet') {
die "skip\n" if !$hotplug_features->{usb};
if ($value == 1) {
- vm_deviceplug($storecfg, $conf, $vmid, 'tablet', $arch, $machine_type);
- vm_deviceplug($storecfg, $conf, $vmid, 'keyboard', $arch, $machine_type)
- if $arch eq 'aarch64';
+ vm_deviceplug(
+ $storecfg, $conf, $vmid, 'tablet', undef, $arch, $machine_type,
+ );
+ vm_deviceplug(
+ $storecfg, $conf, $vmid, 'keyboard', undef, $arch, $machine_type,
+ ) if $arch eq 'aarch64';
} elsif ($value == 0) {
vm_deviceunplug($vmid, $conf, 'tablet');
vm_deviceunplug($vmid, $conf, 'keyboard') if $arch eq 'aarch64';
diff --git a/src/test/hotplug/aarch64/tablet-enable.conf.expected b/src/test/hotplug/aarch64/tablet-enable.conf.expected
index 708c5432..3a195c2d 100644
--- a/src/test/hotplug/aarch64/tablet-enable.conf.expected
+++ b/src/test/hotplug/aarch64/tablet-enable.conf.expected
@@ -1,8 +1,8 @@
# recorded actions
-hmp device_add driver=usb-tablet,id=tablet,bus=uhci.0,port=1
+hmp device_add driver=usb-tablet,id=tablet,bus=ehci.0,port=1
+hmp device_add driver=usb-kbd,id=keyboard,bus=ehci.0,port=2
# hotplug errors
-tablet: hotplug problem - Too few arguments for subroutine 'PVE::QemuServer::QMPHelpers::qemu_deviceadd' (got 1; expected 2)
# differences to a freshly started VM
@@ -21,8 +21,5 @@ scsi0: local:8006/vm-8006-disk-0.qcow2,size=32G
scsihw: virtio-scsi-pci
smbios1: uuid=7b10d7af-b932-4c66-b2c3-3996152ec465
sockets: 1
-tablet: 0
-vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
-
-[PENDING]
tablet: 1
+vmgenid: c773c261-d800-4348-9f5d-167fadd53cf8
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH qemu-server 6/6] hotplug: remove iothread if adding drive device failed
2026-09-10 11:00 [PATCH qemu-server 0/6] add hotplug tests and fix uncovered bugs Dominik Csapak
` (4 preceding siblings ...)
2026-09-10 11:00 ` [PATCH qemu-server 5/6] hotplug: fix vm_deviceplug call for 'tablet' and 'keyboard' on aarch64 Dominik Csapak
@ 2026-09-10 11:00 ` Dominik Csapak
5 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2026-09-10 11:00 UTC (permalink / raw)
To: pve-devel
Otherwise it is left over after a failed attempt to add a drive with
iothreads.
Modify the relevant test that highlighted this problem.
The failure can be tested by disabling hotplug, e.g. for a i440fx guest
one can use the following commandline switches:
-global PIIX4_PM.acpi-root-pci-hotplug=off
-global PIIX4_PM.acpi-pci-hotplug-with-bridge-support=off
-global pci-bridge.shpc=off
to disable hotplug on all pci bridges and the pci root bus.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/PVE/QemuServer.pm | 11 ++++++++++-
src/test/hotplug/disk-add-fail.conf.expected | 2 +-
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 34073ee8..8fade3fa 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -3906,6 +3906,8 @@ sub vm_deviceplug {
if (my $err = $@) {
eval { qemu_drivedel($vmid, $deviceid); };
warn $@ if $@;
+ eval { qemu_iothread_del($vmid, $deviceid, $device) };
+ warn $@ if $@;
die $err;
}
} elsif ($deviceid =~ m/^(virtioscsi|scsihw)(\d+)$/) {
@@ -3925,7 +3927,14 @@ sub vm_deviceplug {
}
qemu_deviceadd($vmid, $devicefull);
- qemu_deviceaddverify($vmid, $deviceid);
+ eval { qemu_deviceaddverify($vmid, $deviceid) };
+ if (my $err = $@) {
+ if ($deviceid =~ m/^virtioscsi(\d+)$/ && $device->{iothread}) {
+ eval { qemu_iothread_del($vmid, $deviceid, $device) };
+ warn $@ if $@;
+ }
+ die $err;
+ }
} elsif ($deviceid =~ m/^(scsi)(\d+)$/) {
qemu_findorcreatescsihw($storecfg, $conf, $vmid, $device, $arch, $machine_type);
qemu_driveadd($storecfg, $vmid, $device);
diff --git a/src/test/hotplug/disk-add-fail.conf.expected b/src/test/hotplug/disk-add-fail.conf.expected
index 8a36d951..28ec8e89 100644
--- a/src/test/hotplug/disk-add-fail.conf.expected
+++ b/src/test/hotplug/disk-add-fail.conf.expected
@@ -7,12 +7,12 @@ qmp blockdev-add {"detect-zeroes":"on","discard":"ignore","driver":"throttle","f
hmp device_add driver=virtio-blk-pci,drive=drive-virtio1,id=virtio1,bus=pci.0,addr=0xb,iothread=iothread-virtio1,write-cache=on
qmp blockdev-del {"node-name":"drive-virtio1"}
qmp object-del {"id":"throttle-drive-virtio1"}
+qmp object-del {"id":"iothread-virtio1"}
# hotplug errors
virtio1: hotplug problem - error on hotplug device 'virtio1'
# differences to a freshly started VM
-object iothread-virtio1: only present in running VM
# resulting config
bootdisk: scsi0
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-09-10 11:29 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-10 11:00 [PATCH qemu-server 0/6] add hotplug tests and fix uncovered bugs Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 1/6] tests: hotplug: add initial hotplug test harness Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 2/6] tests: hotplug: add some test cases Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 3/6] tests: hotplug: add cases for known defects Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 4/6] tests: hotplug: add test case for adding scsi14 on qemu 11.1 Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 5/6] hotplug: fix vm_deviceplug call for 'tablet' and 'keyboard' on aarch64 Dominik Csapak
2026-09-10 11:00 ` [PATCH qemu-server 6/6] hotplug: remove iothread if adding drive device failed Dominik Csapak
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox