]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
add new 'boot' property format and introduce legacy conversion helpers
[qemu-server.git] / PVE / QemuServer.pm
index 7b225da77019095d158a01722d21ca15fc2f9950..cfac03a92858236763b8f9637efd04af2f94f8b8 100644 (file)
@@ -24,27 +24,39 @@ use POSIX;
 use Storable qw(dclone);
 use Time::HiRes qw(gettimeofday);
 use URI::Escape;
+use UUID;
 
-use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file);
+use PVE::DataCenterConfig;
 use PVE::Exception qw(raise raise_param_exc);
-use PVE::GuestHelpers;
+use PVE::GuestHelpers qw(safe_string_ne safe_num_ne safe_boolean_ne);
 use PVE::INotify;
-use PVE::JSONSchema qw(get_standard_option);
+use PVE::JSONSchema qw(get_standard_option parse_property_string);
 use PVE::ProcFSTools;
 use PVE::RPCEnvironment;
-use PVE::SafeSyslog;
 use PVE::Storage;
 use PVE::SysFSTools;
 use PVE::Systemd;
-use PVE::Tools qw(run_command lock_file lock_file_full file_read_firstline dir_glob_foreach $IPV6RE);
+use PVE::Tools qw(run_command file_read_firstline file_get_contents dir_glob_foreach get_host_arch $IPV6RE);
 
 use PVE::QMPClient;
 use PVE::QemuConfig;
+use PVE::QemuServer::Helpers qw(min_version config_aware_timeout);
 use PVE::QemuServer::Cloudinit;
+use PVE::QemuServer::CPUConfig qw(print_cpu_device get_cpu_options);
+use PVE::QemuServer::Drive qw(is_valid_drivename drive_is_cloudinit drive_is_cdrom parse_drive print_drive);
+use PVE::QemuServer::Machine;
 use PVE::QemuServer::Memory;
-use PVE::QemuServer::PCI qw(print_pci_addr print_pcie_addr print_pcie_root_port);
+use PVE::QemuServer::Monitor qw(mon_cmd);
+use PVE::QemuServer::PCI qw(print_pci_addr print_pcie_addr print_pcie_root_port parse_hostpci);
 use PVE::QemuServer::USB qw(parse_usb_device);
 
+my $have_sdn;
+eval {
+    require PVE::Network::SDN::Zones;
+    $have_sdn = 1;
+};
+
 my $EDK2_FW_BASE = '/usr/share/pve-edk2-firmware/';
 my $OVMF = {
     x86_64 => [
@@ -57,12 +69,8 @@ my $OVMF = {
     ],
 };
 
-my $qemu_snap_storage = { rbd => 1 };
-
 my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
 
-my $QEMU_FORMAT_RE = qr/raw|cow|qcow|qcow2|qed|vmdk|cloop/;
-
 # Note about locking: we use flock on the config file protect
 # against concurent actions.
 # Aditionaly, we have a 'lock' setting in the config file. This
@@ -81,21 +89,36 @@ PVE::JSONSchema::register_standard_option('pve-qm-stateuri', {
     optional => 1,
 });
 
-PVE::JSONSchema::register_standard_option('pve-qm-image-format', {
-    type => 'string',
-    enum => [qw(raw cow qcow qed qcow2 vmdk cloop)],
-    description => "The drive's backing file's data format.",
-    optional => 1,
-});
-
 PVE::JSONSchema::register_standard_option('pve-qemu-machine', {
        description => "Specifies the Qemu machine type.",
        type => 'string',
-       pattern => '(pc|pc(-i440fx)?-\d+\.\d+(\.pxe)?|q35|pc-q35-\d+\.\d+(\.pxe)?|virt(?:-\d+\.\d+)?)',
+       pattern => '(pc|pc(-i440fx)?-\d+(\.\d+)+(\+pve\d+)?(\.pxe)?|q35|pc-q35-\d+(\.\d+)+(\+pve\d+)?(\.pxe)?|virt(?:-\d+(\.\d+)+)?(\+pve\d+)?)',
        maxLength => 40,
        optional => 1,
 });
 
+
+sub map_storage {
+    my ($map, $source) = @_;
+
+    return $source if !defined($map);
+
+    return $map->{entries}->{$source}
+       if $map->{entries} && defined($map->{entries}->{$source});
+
+    return $map->{default} if $map->{default};
+
+    # identity (fallback)
+    return $source;
+}
+
+PVE::JSONSchema::register_standard_option('pve-targetstorage', {
+    description => "Mapping from source to target storages. Providing only a single storage ID maps all source storages to that storage. Providing the special value '1' will map each source storage to itself.",
+    type => 'string',
+    format => 'storagepair-list',
+    optional => 1,
+});
+
 #no warnings 'redefine';
 
 sub cgroups_write {
@@ -106,119 +129,11 @@ sub cgroups_write {
 
 }
 
-my $nodename = PVE::INotify::nodename();
-
-mkdir "/etc/pve/nodes/$nodename";
-my $confdir = "/etc/pve/nodes/$nodename/qemu-server";
-mkdir $confdir;
-
-my $var_run_tmpdir = "/var/run/qemu-server";
-mkdir $var_run_tmpdir;
-
-my $lock_dir = "/var/lock/qemu-server";
-mkdir $lock_dir;
-
-my $cpu_vendor_list = {
-    # Intel CPUs
-    486 => 'GenuineIntel',
-    pentium => 'GenuineIntel',
-    pentium2  => 'GenuineIntel',
-    pentium3  => 'GenuineIntel',
-    coreduo => 'GenuineIntel',
-    core2duo => 'GenuineIntel',
-    Conroe  => 'GenuineIntel',
-    Penryn  => 'GenuineIntel',
-    Nehalem  => 'GenuineIntel',
-    'Nehalem-IBRS'  => 'GenuineIntel',
-    Westmere => 'GenuineIntel',
-    'Westmere-IBRS' => 'GenuineIntel',
-    SandyBridge => 'GenuineIntel',
-    'SandyBridge-IBRS' => 'GenuineIntel',
-    IvyBridge => 'GenuineIntel',
-    'IvyBridge-IBRS' => 'GenuineIntel',
-    Haswell => 'GenuineIntel',
-    'Haswell-IBRS' => 'GenuineIntel',
-    'Haswell-noTSX' => 'GenuineIntel',
-    'Haswell-noTSX-IBRS' => 'GenuineIntel',
-    Broadwell => 'GenuineIntel',
-    'Broadwell-IBRS' => 'GenuineIntel',
-    'Broadwell-noTSX' => 'GenuineIntel',
-    'Broadwell-noTSX-IBRS' => 'GenuineIntel',
-    'Skylake-Client' => 'GenuineIntel',
-    'Skylake-Client-IBRS' => 'GenuineIntel',
-    'Skylake-Server' => 'GenuineIntel',
-    'Skylake-Server-IBRS' => 'GenuineIntel',
-    'Cascadelake-Server' => 'GenuineIntel',
-    KnightsMill => 'GenuineIntel',
-
-
-    # AMD CPUs
-    athlon => 'AuthenticAMD',
-    phenom  => 'AuthenticAMD',
-    Opteron_G1  => 'AuthenticAMD',
-    Opteron_G2  => 'AuthenticAMD',
-    Opteron_G3  => 'AuthenticAMD',
-    Opteron_G4  => 'AuthenticAMD',
-    Opteron_G5  => 'AuthenticAMD',
-    EPYC => 'AuthenticAMD',
-    'EPYC-IBPB' => 'AuthenticAMD',
-
-    # generic types, use vendor from host node
-    host => 'default',
-    kvm32 => 'default',
-    kvm64 => 'default',
-    qemu32 => 'default',
-    qemu64 => 'default',
-    max => 'default',
-};
-
-my @supported_cpu_flags = (
-    'pcid',
-    'spec-ctrl',
-    'ibpb',
-    'ssbd',
-    'virt-ssbd',
-    'amd-ssbd',
-    'amd-no-ssb',
-    'pdpe1gb',
-    'md-clear',
-    'hv-tlbflush',
-    'hv-evmcs',
-    'aes'
-);
-my $cpu_flag = qr/[+-](@{[join('|', @supported_cpu_flags)]})/;
-
-my $cpu_fmt = {
-    cputype => {
-       description => "Emulated CPU type.",
-       type => 'string',
-       enum => [ sort { "\L$a" cmp "\L$b" } keys %$cpu_vendor_list ],
-       default => 'kvm64',
-       default_key => 1,
-    },
-    hidden => {
-       description => "Do not identify as a KVM virtual machine.",
-       type => 'boolean',
-       optional => 1,
-       default => 0
-    },
-    'hv-vendor-id' => {
-       type => 'string',
-       pattern => qr/[a-zA-Z0-9]{1,12}/,
-       format_description => 'vendor-id',
-       description => 'The Hyper-V vendor ID. Some drivers or programs inside Windows guests need a specific ID.',
-       optional => 1,
-    },
-    flags => {
-       description => "List of additional CPU flags separated by ';'."
-                    . " Use '+FLAG' to enable, '-FLAG' to disable a flag."
-                    . " Currently supported flags: @{[join(', ', @supported_cpu_flags)]}.",
-       format_description => '+FLAG[;-FLAG...]',
-       type => 'string',
-       pattern => qr/$cpu_flag(;$cpu_flag)*/,
-       optional => 1,
-    },
-};
+my $nodename_cache;
+sub nodename {
+    $nodename_cache //= PVE::INotify::nodename();
+    return $nodename_cache;
+}
 
 my $watchdog_fmt = {
     model => {
@@ -251,6 +166,13 @@ my $agent_fmt = {
        optional => 1,
        default => 0
     },
+    type => {
+       description => "Select the agent type",
+       type => 'string',
+       default => 'virtio',
+       optional => 1,
+       enum => [qw(virtio isa)],
+    },
 };
 
 my $vga_fmt = {
@@ -317,6 +239,43 @@ my $spice_enhancements_fmt = {
     },
 };
 
+my $rng_fmt = {
+    source => {
+       type => 'string',
+       enum => ['/dev/urandom', '/dev/random', '/dev/hwrng'],
+       default_key => 1,
+       description => "The file on the host to gather entropy from. In most"
+                    . " cases /dev/urandom should be preferred over /dev/random"
+                    . " to avoid entropy-starvation issues on the host. Using"
+                    . " urandom does *not* decrease security in any meaningful"
+                    . " way, as it's still seeded from real entropy, and the"
+                    . " bytes provided will most likely be mixed with real"
+                    . " entropy on the guest as well. /dev/hwrng can be used"
+                    . " to pass through a hardware RNG from the host.",
+    },
+    max_bytes => {
+       type => 'integer',
+       description => "Maximum bytes of entropy injected into the guest every"
+                    . " 'period' milliseconds. Prefer a lower value when using"
+                    . " /dev/random as source. Use 0 to disable limiting"
+                    . " (potentially dangerous!).",
+       optional => 1,
+
+       # default is 1 KiB/s, provides enough entropy to the guest to avoid
+       # boot-starvation issues (e.g. systemd etc...) while allowing no chance
+       # of overwhelming the host, provided we're reading from /dev/urandom
+       default => 1024,
+    },
+    period => {
+       type => 'integer',
+       description => "Every 'period' milliseconds the entropy-injection quota"
+                    . " is reset, allowing the guest to retrieve another"
+                    . " 'max_bytes' of entropy.",
+       optional => 1,
+       default => 1000,
+    },
+};
+
 my $confdesc = {
     onboot => {
        optional => 1,
@@ -432,7 +391,7 @@ win7;; Microsoft Windows 7
 win8;; Microsoft Windows 8/2012/2012r2
 win10;; Microsoft Windows 10/2016
 l24;; Linux 2.4 Kernel
-l26;; Linux 2.6/3.X Kernel
+l26;; Linux 2.6 - 5.X Kernel
 solaris;; Solaris/OpenSolaris/OpenIndiania kernel
 EODESC
     },
@@ -482,6 +441,13 @@ EODESC
        description => "Enable/disable hugepages memory.",
        enum => [qw(any 2 1024)],
     },
+    keephugepages => {
+       optional => 1,
+       type => 'boolean',
+       default => 0,
+       description => "Use together with hugepages. If enabled, hugepages will not not be deleted"
+           ." after VM shutdown and can be used for subsequent starts.",
+    },
     vcpus => {
        optional => 1,
        type => 'integer',
@@ -516,7 +482,8 @@ EODESC
     localtime => {
        optional => 1,
        type => 'boolean',
-       description => "Set the real time clock to local time. This is enabled by default if ostype indicates a Microsoft OS.",
+       description => "Set the real time clock to local time. This is enabled by default if ostype"
+           ." indicates a Microsoft OS.",
     },
     freeze => {
        optional => 1,
@@ -527,29 +494,28 @@ EODESC
        optional => 1,
        type => 'string', format => $vga_fmt,
        description => "Configure the VGA hardware.",
-       verbose_description => "Configure the VGA Hardware. If you want to use ".
-           "high resolution modes (>= 1280x1024x16) you may need to increase " .
-           "the vga memory option. Since QEMU 2.9 the default VGA display type " .
-           "is 'std' for all OS types besides some Windows versions (XP and " .
-           "older) which use 'cirrus'. The 'qxl' option enables the SPICE " .
-           "display server. For win* OS you can select how many independent " .
-           "displays you want, Linux guests can add displays them self.\n".
-           "You can also run without any graphic card, using a serial device as terminal.",
+       verbose_description => "Configure the VGA Hardware. If you want to use high resolution"
+           ." modes (>= 1280x1024x16) you may need to increase the vga memory option. Since QEMU"
+           ." 2.9 the default VGA display type is 'std' for all OS types besides some Windows"
+           ." versions (XP and older) which use 'cirrus'. The 'qxl' option enables the SPICE"
+           ." display server. For win* OS you can select how many independent displays you want,"
+           ." Linux guests can add displays them self.\nYou can also run without any graphic card,"
+           ." using a serial device as terminal.",
     },
     watchdog => {
        optional => 1,
        type => 'string', format => 'pve-qm-watchdog',
        description => "Create a virtual hardware watchdog device.",
-       verbose_description => "Create a virtual hardware watchdog device. Once enabled" .
-           " (by a guest action), the watchdog must be periodically polled " .
-           "by an agent inside the guest or else the watchdog will reset " .
-           "the guest (or execute the respective action specified)",
+       verbose_description => "Create a virtual hardware watchdog device. Once enabled (by a guest"
+           ." action), the watchdog must be periodically polled by an agent inside the guest or"
+           ." else the watchdog will reset the guest (or execute the respective action specified)",
     },
     startdate => {
        optional => 1,
        type => 'string',
        typetext => "(now | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS)",
-       description => "Set the initial date of the real time clock. Valid format for date are: 'now' or '2006-06-17T16:01:21' or '2006-06-17'.",
+       description => "Set the initial date of the real time clock. Valid format for date are:"
+           ."'now' or '2006-06-17T16:01:21' or '2006-06-17'.",
        pattern => '(now|\d{4}-\d{1,2}-\d{1,2}(T\d{1,2}:\d{1,2}:\d{1,2})?)',
        default => 'now',
     },
@@ -577,12 +543,11 @@ EODESCR
        type => 'boolean',
        default => 1,
        description => "Enable/disable the USB tablet device.",
-       verbose_description => "Enable/disable the USB tablet device. This device is " .
-           "usually needed to allow absolute mouse positioning with VNC. " .
-           "Else the mouse runs out of sync with normal VNC clients. " .
-           "If you're running lots of console-only guests on one host, " .
-           "you may consider disabling this to save some context switches. " .
-           "This is turned off by default if you use spice (-vga=qxl).",
+       verbose_description => "Enable/disable the USB tablet device. This device is usually needed"
+           ." to allow absolute mouse positioning with VNC. Else the mouse runs out of sync with"
+           ." normal VNC clients. If you're running lots of console-only guests on one host, you"
+           ." may consider disabling this to save some context switches. This is turned off by"
+           ." default if you use spice (`qm set <vmid> --vga qxl`).",
     },
     migrate_speed => {
        optional => 1,
@@ -608,7 +573,7 @@ EODESCR
        optional => 1,
        description => "Emulated CPU type.",
        type => 'string',
-       format => $cpu_fmt,
+       format => 'pve-vm-cpu-conf',
     },
     parent => get_standard_option('pve-snapshot-name', {
        optional => 1,
@@ -623,15 +588,25 @@ EODESCR
     vmstate => {
        optional => 1,
        type => 'string', format => 'pve-volume-id',
-       description => "Reference to a volume which stores the VM state. This is used internally for snapshots.",
+       description => "Reference to a volume which stores the VM state. This is used internally"
+           ." for snapshots.",
     },
     vmstatestorage => get_standard_option('pve-storage-id', {
        description => "Default storage for VM state volumes/files.",
        optional => 1,
     }),
     runningmachine => get_standard_option('pve-qemu-machine', {
-       description => "Specifies the Qemu machine type of the running vm. This is used internally for snapshots.",
+       description => "Specifies the QEMU machine type of the running vm. This is used internally"
+           ." for snapshots.",
     }),
+    runningcpu => {
+       description => "Specifies the QEMU '-cpu' parameter of the running vm. This is used"
+           ." internally for snapshots.",
+       optional => 1,
+       type => 'string',
+       pattern => $PVE::QemuServer::CPUConfig::qemu_cmdline_cpu_re,
+       format_description => 'QEMU -cpu parameter'
+    },
     machine => get_standard_option('pve-qemu-machine'),
     arch => {
        description => "Virtual processor architecture. Defaults to the host.",
@@ -648,7 +623,8 @@ EODESCR
     protection => {
        optional => 1,
        type => 'boolean',
-       description => "Sets the protection flag of the VM. This will disable the remove VM and remove disk operations.",
+       description => "Sets the protection flag of the VM. This will disable the remove VM and"
+           ." remove disk operations.",
        default => 0,
     },
     bios => {
@@ -662,17 +638,16 @@ EODESCR
        type => 'string',
        pattern => '(?:[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}|[01])',
        format_description => 'UUID',
-       description => "Set VM Generation ID. Use '1' to autogenerate on create or update, pass '0' to disable explicitly.",
-       verbose_description => "The VM generation ID (vmgenid) device exposes a".
-           " 128-bit integer value identifier to the guest OS. This allows to".
-           " notify the guest operating system when the virtual machine is".
-           " executed with a different configuration (e.g. snapshot execution".
-           " or creation from a template). The guest operating system notices".
-           " the change, and is then able to react as appropriate by marking".
-           " its copies of distributed databases as dirty, re-initializing its".
-           " random number generator, etc.\n".
-           "Note that auto-creation only works when done throug API/CLI create".
-           " or update methods, but not when manually editing the config file.",
+       description => "Set VM Generation ID. Use '1' to autogenerate on create or update, pass '0'"
+           ." to disable explicitly.",
+       verbose_description => "The VM generation ID (vmgenid) device exposes a 128-bit integer"
+           ." value identifier to the guest OS. This allows to notify the guest operating system"
+           ." when the virtual machine is executed with a different configuration (e.g. snapshot"
+           ." execution or creation from a template). The guest operating system notices the"
+           ." change, and is then able to react as appropriate by marking its copies of"
+           ." distributed databases as dirty, re-initializing its random number generator, etc.\n"
+           ."Note that auto-creation only works when done through API/CLI create or update methods"
+           .", but not when manually editing the config file.",
        default => "1 (autogenerated)",
        optional => 1,
     },
@@ -685,7 +660,8 @@ EODESCR
     ivshmem => {
        type => 'string',
        format => $ivshmem_fmt,
-       description => "Inter-VM shared memory. Useful for direct communication between VMs, or to the host.",
+       description => "Inter-VM shared memory. Useful for direct communication between VMs, or to"
+           ." the host.",
        optional => 1,
     },
     audio0 => {
@@ -700,27 +676,41 @@ EODESCR
        description => "Configure additional enhancements for SPICE.",
        optional => 1
     },
+    tags => {
+       type => 'string', format => 'pve-tag-list',
+       description => 'Tags of the VM. This is only meta information.',
+       optional => 1,
+    },
+    rng0 => {
+       type => 'string',
+       format => $rng_fmt,
+       description => "Configure a VirtIO-based Random Number Generator.",
+       optional => 1,
+    },
 };
 
 my $cicustom_fmt = {
     meta => {
        type => 'string',
        optional => 1,
-       description => 'Specify a custom file containing all meta data passed to the VM via cloud-init. This is provider specific meaning configdrive2 and nocloud differ.',
+       description => 'Specify a custom file containing all meta data passed to the VM via"
+           ." cloud-init. This is provider specific meaning configdrive2 and nocloud differ.',
        format => 'pve-volume-id',
        format_description => 'volume',
     },
     network => {
        type => 'string',
        optional => 1,
-       description => 'Specify a custom file containing all network data passed to the VM via cloud-init.',
+       description => 'Specify a custom file containing all network data passed to the VM via'
+           .' cloud-init.',
        format => 'pve-volume-id',
        format_description => 'volume',
     },
     user => {
        type => 'string',
        optional => 1,
-       description => 'Specify a custom file containing all user data passed to the VM via cloud-init.',
+       description => 'Specify a custom file containing all user data passed to the VM via'
+           .' cloud-init.',
        format => 'pve-volume-id',
        format_description => 'volume',
     },
@@ -731,34 +721,44 @@ my $confdesc_cloudinit = {
     citype => {
        optional => 1,
        type => 'string',
-       description => 'Specifies the cloud-init configuration format. The default depends on the configured operating system type (`ostype`. We use the `nocloud` format for Linux, and `configdrive2` for windows.',
+       description => 'Specifies the cloud-init configuration format. The default depends on the'
+           .' configured operating system type (`ostype`. We use the `nocloud` format for Linux,'
+           .' and `configdrive2` for windows.',
        enum => ['configdrive2', 'nocloud'],
     },
     ciuser => {
        optional => 1,
        type => 'string',
-       description => "cloud-init: User name to change ssh keys and password for instead of the image's configured default user.",
+       description => "cloud-init: User name to change ssh keys and password for instead of the"
+           ." image's configured default user.",
     },
     cipassword => {
        optional => 1,
        type => 'string',
-       description => 'cloud-init: Password to assign the user. Using this is generally not recommended. Use ssh keys instead. Also note that older cloud-init versions do not support hashed passwords.',
+       description => 'cloud-init: Password to assign the user. Using this is generally not'
+           .' recommended. Use ssh keys instead. Also note that older cloud-init versions do not'
+           .' support hashed passwords.',
     },
     cicustom => {
        optional => 1,
        type => 'string',
-       description => 'cloud-init: Specify custom files to replace the automatically generated ones at start.',
+       description => 'cloud-init: Specify custom files to replace the automatically generated'
+           .' ones at start.',
        format => 'pve-qm-cicustom',
     },
     searchdomain => {
        optional => 1,
        type => 'string',
-       description => "cloud-init: Sets DNS search domains for a container. Create will automatically use the setting from the host if neither searchdomain nor nameserver are set.",
+       description => "cloud-init: Sets DNS search domains for a container. Create will'
+           .' automatically use the setting from the host if neither searchdomain nor nameserver'
+           .' are set.",
     },
     nameserver => {
        optional => 1,
        type => 'string', format => 'address-list',
-       description => "cloud-init: Sets DNS server IP address for a container. Create will automatically use the setting from the host if neither searchdomain nor nameserver are set.",
+       description => "cloud-init: Sets DNS server IP address for a container. Create will'
+           .' automatically use the setting from the host if neither searchdomain nor nameserver'
+           .' are set.",
     },
     sshkeys => {
        optional => 1,
@@ -789,14 +789,8 @@ while (my ($k, $v) = each %$confdesc) {
     PVE::JSONSchema::register_standard_option("pve-qm-$k", $v);
 }
 
-my $MAX_IDE_DISKS = 4;
-my $MAX_SCSI_DISKS = 14;
-my $MAX_VIRTIO_DISKS = 16;
-my $MAX_SATA_DISKS = 6;
 my $MAX_USB_DEVICES = 5;
 my $MAX_NETS = 32;
-my $MAX_UNUSED_DISKS = 256;
-my $MAX_HOSTPCI_DEVICES = 16;
 my $MAX_SERIAL_PORTS = 4;
 my $MAX_PARALLEL_PORTS = 3;
 my $MAX_NUMA = 8;
@@ -861,11 +855,14 @@ __EOD__
 
 my $net_fmt = {
     macaddr  => get_standard_option('mac-addr', {
-       description => "MAC address. That address must be unique withing your network. This is automatically generated if not specified.",
+       description => "MAC address. That address must be unique withing your network. This is"
+           ." automatically generated if not specified.",
     }),
     model => {
        type => 'string',
-       description => "Network Card Model. The 'virtio' model provides the best performance with very low CPU overhead. If your guest does not support this driver, it is usually best to use 'e1000'.",
+       description => "Network Card Model. The 'virtio' model provides the best performance with"
+           ." very low CPU overhead. If your guest does not support this driver, it is usually"
+           ." best to use 'e1000'.",
         enum => $nic_model_list,
         default_key => 1,
     },
@@ -874,6 +871,7 @@ my $net_fmt = {
        type => 'string',
        description => $net_fmt_bridge_descr,
        format_description => 'bridge',
+       pattern => '[-_.\w\d]+',
        optional => 1,
     },
     queues => {
@@ -911,6 +909,12 @@ my $net_fmt = {
        description => 'Whether this interface should be disconnected (like pulling the plug).',
        optional => 1,
     },
+    mtu => {
+       type => 'integer',
+       minimum => 1, maximum => 65520,
+       description => "Force MTU, for VirtIO only. Set to '1' to use the bridge MTU",
+       optional => 1,
+    },
 };
 
 my $netdesc = {
@@ -964,10 +968,12 @@ cloud-init: Specify IP addresses and gateways for the corresponding interface.
 
 IP addresses use CIDR notation, gateways are optional but need an IP of the same type specified.
 
-The special string 'dhcp' can be used for IP addresses to use DHCP, in which case no explicit gateway should be provided.
+The special string 'dhcp' can be used for IP addresses to use DHCP, in which case no explicit
+gateway should be provided.
 For IPv6 the special string 'auto' can be used to use stateless autoconfiguration.
 
-If cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using dhcp on IPv4.
+If cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using
+dhcp on IPv4.
 EODESCR
 };
 PVE::JSONSchema::register_standard_option("pve-qm-ipconfig", $netdesc);
@@ -998,310 +1004,6 @@ sub verify_volume_id_or_qm_path {
     return $volid;
 }
 
-my $drivename_hash;
-
-my %drivedesc_base = (
-    volume => { alias => 'file' },
-    file => {
-       type => 'string',
-       format => 'pve-volume-id-or-qm-path',
-       default_key => 1,
-       format_description => 'volume',
-       description => "The drive's backing volume.",
-    },
-    media => {
-       type => 'string',
-       enum => [qw(cdrom disk)],
-       description => "The drive's media type.",
-       default => 'disk',
-       optional => 1
-    },
-    cyls => {
-       type => 'integer',
-       description => "Force the drive's physical geometry to have a specific cylinder count.",
-       optional => 1
-    },
-    heads => {
-       type => 'integer',
-       description => "Force the drive's physical geometry to have a specific head count.",
-       optional => 1
-    },
-    secs => {
-       type => 'integer',
-       description => "Force the drive's physical geometry to have a specific sector count.",
-       optional => 1
-    },
-    trans => {
-       type => 'string',
-       enum => [qw(none lba auto)],
-       description => "Force disk geometry bios translation mode.",
-       optional => 1,
-    },
-    snapshot => {
-       type => 'boolean',
-       description => "Controls qemu's snapshot mode feature."
-           . " If activated, changes made to the disk are temporary and will"
-           . " be discarded when the VM is shutdown.",
-       optional => 1,
-    },
-    cache => {
-       type => 'string',
-       enum => [qw(none writethrough writeback unsafe directsync)],
-       description => "The drive's cache mode",
-       optional => 1,
-    },
-    format => get_standard_option('pve-qm-image-format'),
-    size => {
-       type => 'string',
-       format => 'disk-size',
-       format_description => 'DiskSize',
-       description => "Disk size. This is purely informational and has no effect.",
-       optional => 1,
-    },
-    backup => {
-       type => 'boolean',
-       description => "Whether the drive should be included when making backups.",
-       optional => 1,
-    },
-    replicate => {
-       type => 'boolean',
-       description => 'Whether the drive should considered for replication jobs.',
-       optional => 1,
-       default => 1,
-    },
-    rerror => {
-       type => 'string',
-       enum => [qw(ignore report stop)],
-       description => 'Read error action.',
-       optional => 1,
-    },
-    werror => {
-       type => 'string',
-       enum => [qw(enospc ignore report stop)],
-       description => 'Write error action.',
-       optional => 1,
-    },
-    aio => {
-       type => 'string',
-       enum => [qw(native threads)],
-       description => 'AIO type to use.',
-       optional => 1,
-    },
-    discard => {
-       type => 'string',
-       enum => [qw(ignore on)],
-       description => 'Controls whether to pass discard/trim requests to the underlying storage.',
-       optional => 1,
-    },
-    detect_zeroes => {
-       type => 'boolean',
-       description => 'Controls whether to detect and try to optimize writes of zeroes.',
-       optional => 1,
-    },
-    serial => {
-       type => 'string',
-       format => 'urlencoded',
-       format_description => 'serial',
-       maxLength => 20*3, # *3 since it's %xx url enoded
-       description => "The drive's reported serial number, url-encoded, up to 20 bytes long.",
-       optional => 1,
-    },
-    shared => {
-       type => 'boolean',
-       description => 'Mark this locally-managed volume as available on all nodes',
-       verbose_description => "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!",
-       optional => 1,
-       default => 0,
-    }
-);
-
-my %iothread_fmt = ( iothread => {
-       type => 'boolean',
-       description => "Whether to use iothreads for this drive",
-       optional => 1,
-});
-
-my %model_fmt = (
-    model => {
-       type => 'string',
-       format => 'urlencoded',
-       format_description => 'model',
-       maxLength => 40*3, # *3 since it's %xx url enoded
-       description => "The drive's reported model name, url-encoded, up to 40 bytes long.",
-       optional => 1,
-    },
-);
-
-my %queues_fmt = (
-    queues => {
-       type => 'integer',
-       description => "Number of queues.",
-       minimum => 2,
-       optional => 1
-    }
-);
-
-my %scsiblock_fmt = (
-    scsiblock => {
-       type => 'boolean',
-       description => "whether to use scsi-block for full passthrough of host block device\n\nWARNING: can lead to I/O errors in combination with low memory or high memory fragmentation on host",
-       optional => 1,
-       default => 0,
-    },
-);
-
-my %ssd_fmt = (
-    ssd => {
-       type => 'boolean',
-       description => "Whether to expose this drive as an SSD, rather than a rotational hard disk.",
-       optional => 1,
-    },
-);
-
-my %wwn_fmt = (
-    wwn => {
-       type => 'string',
-       pattern => qr/^(0x)[0-9a-fA-F]{16}/,
-       format_description => 'wwn',
-       description => "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.",
-       optional => 1,
-    },
-);
-
-my $add_throttle_desc = sub {
-    my ($key, $type, $what, $unit, $longunit, $minimum) = @_;
-    my $d = {
-       type => $type,
-       format_description => $unit,
-       description => "Maximum $what in $longunit.",
-       optional => 1,
-    };
-    $d->{minimum} = $minimum if defined($minimum);
-    $drivedesc_base{$key} = $d;
-};
-# throughput: (leaky bucket)
-$add_throttle_desc->('bps',     'integer', 'r/w speed',   'bps',  'bytes per second');
-$add_throttle_desc->('bps_rd',  'integer', 'read speed',  'bps',  'bytes per second');
-$add_throttle_desc->('bps_wr',  'integer', 'write speed', 'bps',  'bytes per second');
-$add_throttle_desc->('mbps',    'number',  'r/w speed',   'mbps', 'megabytes per second');
-$add_throttle_desc->('mbps_rd', 'number',  'read speed',  'mbps', 'megabytes per second');
-$add_throttle_desc->('mbps_wr', 'number',  'write speed', 'mbps', 'megabytes per second');
-$add_throttle_desc->('iops',    'integer', 'r/w I/O',     'iops', 'operations per second');
-$add_throttle_desc->('iops_rd', 'integer', 'read I/O',    'iops', 'operations per second');
-$add_throttle_desc->('iops_wr', 'integer', 'write I/O',   'iops', 'operations per second');
-
-# pools: (pool of IO before throttling starts taking effect)
-$add_throttle_desc->('mbps_max',    'number',  'unthrottled r/w pool',       'mbps', 'megabytes per second');
-$add_throttle_desc->('mbps_rd_max', 'number',  'unthrottled read pool',      'mbps', 'megabytes per second');
-$add_throttle_desc->('mbps_wr_max', 'number',  'unthrottled write pool',     'mbps', 'megabytes per second');
-$add_throttle_desc->('iops_max',    'integer', 'unthrottled r/w I/O pool',   'iops', 'operations per second');
-$add_throttle_desc->('iops_rd_max', 'integer', 'unthrottled read I/O pool',  'iops', 'operations per second');
-$add_throttle_desc->('iops_wr_max', 'integer', 'unthrottled write I/O pool', 'iops', 'operations per second');
-
-# burst lengths
-$add_throttle_desc->('bps_max_length',     'integer', 'length of I/O bursts',       'seconds', 'seconds', 1);
-$add_throttle_desc->('bps_rd_max_length',  'integer', 'length of read I/O bursts',  'seconds', 'seconds', 1);
-$add_throttle_desc->('bps_wr_max_length',  'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1);
-$add_throttle_desc->('iops_max_length',    'integer', 'length of I/O bursts',       'seconds', 'seconds', 1);
-$add_throttle_desc->('iops_rd_max_length', 'integer', 'length of read I/O bursts',  'seconds', 'seconds', 1);
-$add_throttle_desc->('iops_wr_max_length', 'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1);
-
-# legacy support
-$drivedesc_base{'bps_rd_length'} = { alias => 'bps_rd_max_length' };
-$drivedesc_base{'bps_wr_length'} = { alias => 'bps_wr_max_length' };
-$drivedesc_base{'iops_rd_length'} = { alias => 'iops_rd_max_length' };
-$drivedesc_base{'iops_wr_length'} = { alias => 'iops_wr_max_length' };
-
-my $ide_fmt = {
-    %drivedesc_base,
-    %model_fmt,
-    %ssd_fmt,
-    %wwn_fmt,
-};
-PVE::JSONSchema::register_format("pve-qm-ide", $ide_fmt);
-
-my $idedesc = {
-    optional => 1,
-    type => 'string', format => $ide_fmt,
-    description => "Use volume as IDE hard disk or CD-ROM (n is 0 to " .($MAX_IDE_DISKS -1) . ").",
-};
-PVE::JSONSchema::register_standard_option("pve-qm-ide", $idedesc);
-
-my $scsi_fmt = {
-    %drivedesc_base,
-    %iothread_fmt,
-    %queues_fmt,
-    %scsiblock_fmt,
-    %ssd_fmt,
-    %wwn_fmt,
-};
-my $scsidesc = {
-    optional => 1,
-    type => 'string', format => $scsi_fmt,
-    description => "Use volume as SCSI hard disk or CD-ROM (n is 0 to " . ($MAX_SCSI_DISKS - 1) . ").",
-};
-PVE::JSONSchema::register_standard_option("pve-qm-scsi", $scsidesc);
-
-my $sata_fmt = {
-    %drivedesc_base,
-    %ssd_fmt,
-    %wwn_fmt,
-};
-my $satadesc = {
-    optional => 1,
-    type => 'string', format => $sata_fmt,
-    description => "Use volume as SATA hard disk or CD-ROM (n is 0 to " . ($MAX_SATA_DISKS - 1). ").",
-};
-PVE::JSONSchema::register_standard_option("pve-qm-sata", $satadesc);
-
-my $virtio_fmt = {
-    %drivedesc_base,
-    %iothread_fmt,
-};
-my $virtiodesc = {
-    optional => 1,
-    type => 'string', format => $virtio_fmt,
-    description => "Use volume as VIRTIO hard disk (n is 0 to " . ($MAX_VIRTIO_DISKS - 1) . ").",
-};
-PVE::JSONSchema::register_standard_option("pve-qm-virtio", $virtiodesc);
-
-my $alldrive_fmt = {
-    %drivedesc_base,
-    %iothread_fmt,
-    %model_fmt,
-    %queues_fmt,
-    %scsiblock_fmt,
-    %ssd_fmt,
-    %wwn_fmt,
-};
-
-my $efidisk_fmt = {
-    volume => { alias => 'file' },
-    file => {
-       type => 'string',
-       format => 'pve-volume-id-or-qm-path',
-       default_key => 1,
-       format_description => 'volume',
-       description => "The drive's backing volume.",
-    },
-    format => get_standard_option('pve-qm-image-format'),
-    size => {
-       type => 'string',
-       format => 'disk-size',
-       format_description => 'DiskSize',
-       description => "Disk size. This is purely informational and has no effect.",
-       optional => 1,
-    },
-};
-
-my $efidisk_desc = {
-    optional => 1,
-    type => 'string', format => $efidisk_fmt,
-    description => "Configure a Disk for storing EFI vars",
-};
-
-PVE::JSONSchema::register_standard_option("pve-qm-efidisk", $efidisk_desc);
-
 my $usb_fmt = {
     host => {
        default_key => 1,
@@ -1316,7 +1018,8 @@ The Host USB device or port or the value 'spice'. HOSTUSBDEVICE syntax is:
 
 You can use the 'lsusb -t' command to list existing usb devices.
 
-NOTE: This option allows direct access to host hardware. So it is no longer possible to migrate such machines - use with special care.
+NOTE: This option allows direct access to host hardware. So it is no longer possible to migrate such
+machines - use with special care.
 
 The value 'spice' can be used to add a usb redirection devices for spice.
 EODESCR
@@ -1336,76 +1039,6 @@ my $usbdesc = {
 };
 PVE::JSONSchema::register_standard_option("pve-qm-usb", $usbdesc);
 
-my $PCIRE = qr/[a-f0-9]{2}:[a-f0-9]{2}(?:\.[a-f0-9])?/;
-my $hostpci_fmt = {
-    host => {
-       default_key => 1,
-       type => 'string',
-       pattern => qr/$PCIRE(;$PCIRE)*/,
-       format_description => 'HOSTPCIID[;HOSTPCIID2...]',
-       description => <<EODESCR,
-Host PCI device pass through. The PCI ID of a host's PCI device or a list
-of PCI virtual functions of the host. HOSTPCIID syntax is:
-
-'bus:dev.func' (hexadecimal numbers)
-
-You can us the 'lspci' command to list existing PCI devices.
-EODESCR
-    },
-    rombar => {
-       type => 'boolean',
-        description =>  "Specify whether or not the device's ROM will be visible in the guest's memory map.",
-       optional => 1,
-       default => 1,
-    },
-    romfile => {
-        type => 'string',
-        pattern => '[^,;]+',
-        format_description => 'string',
-        description => "Custom pci device rom filename (must be located in /usr/share/kvm/).",
-        optional => 1,
-    },
-    pcie => {
-       type => 'boolean',
-        description =>  "Choose the PCI-express bus (needs the 'q35' machine model).",
-       optional => 1,
-       default => 0,
-    },
-    'x-vga' => {
-       type => 'boolean',
-        description =>  "Enable vfio-vga device support.",
-       optional => 1,
-       default => 0,
-    },
-    'mdev' => {
-       type => 'string',
-        format_description => 'string',
-       pattern => '[^/\.:]+',
-       optional => 1,
-       description => <<EODESCR
-The type of mediated device to use.
-An instance of this type will be created on startup of the VM and
-will be cleaned up when the VM stops.
-EODESCR
-    }
-};
-PVE::JSONSchema::register_format('pve-qm-hostpci', $hostpci_fmt);
-
-my $hostpcidesc = {
-        optional => 1,
-        type => 'string', format => 'pve-qm-hostpci',
-        description => "Map host PCI devices into guest.",
-       verbose_description =>  <<EODESCR,
-Map host PCI devices into guest.
-
-NOTE: This option allows direct access to host hardware. So it is no longer
-possible to migrate such machines - use with special care.
-
-CAUTION: Experimental! User reported problems with this option.
-EODESCR
-};
-PVE::JSONSchema::register_standard_option("pve-qm-hostpci", $hostpcidesc);
-
 my $serialdesc = {
        optional => 1,
        type => 'string',
@@ -1416,7 +1049,8 @@ Create a serial device inside the VM (n is 0 to 3), and pass through a
 host serial device (i.e. /dev/ttyS0), or create a unix socket on the
 host side (use 'qm terminal' to open a terminal connection).
 
-NOTE: If you pass through a host serial device, it is no longer possible to migrate such machines - use with special care.
+NOTE: If you pass through a host serial device, it is no longer possible to migrate such machines -
+use with special care.
 
 CAUTION: Experimental! User reported problems with this option.
 EODESCR
@@ -1430,7 +1064,8 @@ my $paralleldesc= {
        verbose_description =>  <<EODESCR,
 Map host parallel devices (n is 0 to 2).
 
-NOTE: This option allows direct access to host hardware. So it is no longer possible to migrate such machines - use with special care.
+NOTE: This option allows direct access to host hardware. So it is no longer possible to migrate such
+machines - use with special care.
 
 CAUTION: Experimental! User reported problems with this option.
 EODESCR
@@ -1444,45 +1079,78 @@ for (my $i = 0; $i < $MAX_SERIAL_PORTS; $i++)  {
     $confdesc->{"serial$i"} = $serialdesc;
 }
 
-for (my $i = 0; $i < $MAX_HOSTPCI_DEVICES; $i++)  {
-    $confdesc->{"hostpci$i"} = $hostpcidesc;
+for (my $i = 0; $i < $PVE::QemuServer::PCI::MAX_HOSTPCI_DEVICES; $i++)  {
+    $confdesc->{"hostpci$i"} = $PVE::QemuServer::PCI::hostpcidesc;
 }
 
-for (my $i = 0; $i < $MAX_IDE_DISKS; $i++)  {
-    $drivename_hash->{"ide$i"} = 1;
-    $confdesc->{"ide$i"} = $idedesc;
+for my $key (keys %{$PVE::QemuServer::Drive::drivedesc_hash}) {
+    $confdesc->{$key} = $PVE::QemuServer::Drive::drivedesc_hash->{$key};
 }
 
-for (my $i = 0; $i < $MAX_SATA_DISKS; $i++)  {
-    $drivename_hash->{"sata$i"} = 1;
-    $confdesc->{"sata$i"} = $satadesc;
+for (my $i = 0; $i < $MAX_USB_DEVICES; $i++)  {
+    $confdesc->{"usb$i"} = $usbdesc;
 }
 
-for (my $i = 0; $i < $MAX_SCSI_DISKS; $i++)  {
-    $drivename_hash->{"scsi$i"} = 1;
-    $confdesc->{"scsi$i"} = $scsidesc ;
-}
+my $boot_fmt = {
+    legacy => {
+       optional => 1,
+       default_key => 1,
+       type => 'string',
+       description => "Boot on floppy (a), hard disk (c), CD-ROM (d), or network (n)."
+                    . " Deprecated, use 'order=' instead.",
+       pattern => '[acdn]{1,4}',
+       format_description => "[acdn]{1,4}",
 
-for (my $i = 0; $i < $MAX_VIRTIO_DISKS; $i++)  {
-    $drivename_hash->{"virtio$i"} = 1;
-    $confdesc->{"virtio$i"} = $virtiodesc;
-}
+       # note: this is also the fallback if boot: is not given at all
+       default => 'cdn',
+    },
+    order => {
+       optional => 1,
+       type => 'string',
+       format => 'pve-qm-bootdev-list',
+       format_description => "device[;device...]",
+       description => <<EODESC,
+The guest will attempt to boot from devices in the order they appear here.
 
-$drivename_hash->{efidisk0} = 1;
-$confdesc->{efidisk0} = $efidisk_desc;
+Disks, optical drives and passed-through storage USB devices will be directly
+booted from, NICs will load PXE, and PCIe devices will either behave like disks
+(e.g. NVMe) or load an option ROM (e.g. RAID controller, hardware NIC).
 
-for (my $i = 0; $i < $MAX_USB_DEVICES; $i++)  {
-    $confdesc->{"usb$i"} = $usbdesc;
-}
+Note that only devices in this list will be marked as bootable and thus loaded
+by the guest firmware (BIOS/UEFI). If you require multiple disks for booting
+(e.g. software-raid), you need to specify all of them here.
 
-my $unuseddesc = {
-    optional => 1,
-    type => 'string', format => 'pve-volume-id',
-    description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
+Overrides the deprecated 'legacy=[acdn]*' value when given.
+EODESC
+    },
 };
+PVE::JSONSchema::register_format('pve-qm-boot', $boot_fmt);
+
+PVE::JSONSchema::register_format('pve-qm-bootdev', \&verify_bootdev);
+sub verify_bootdev {
+    my ($dev, $noerr) = @_;
+
+    return $dev if PVE::QemuServer::Drive::is_valid_drivename($dev) && $dev !~ m/^efidisk/;
 
-for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++)  {
-    $confdesc->{"unused$i"} = $unuseddesc;
+    my $check = sub {
+       my ($base) = @_;
+       return 0 if $dev !~ m/^$base\d+$/;
+       return 0 if !$confdesc->{$dev};
+       return 1;
+    };
+
+    return $dev if $check->("net");
+    return $dev if $check->("usb");
+    return $dev if $check->("hostpci");
+
+    return undef if $noerr;
+    die "invalid boot device '$dev'\n";
+}
+
+sub print_bootorder {
+    my ($devs) = @_;
+    my $data = { order => join(';', @$devs) };
+    return PVE::JSONSchema::print_property_string($data, $boot_fmt);
 }
 
 my $kvm_api_version = 0;
@@ -1528,54 +1196,22 @@ sub kvm_user_version {
     return $kvm_user_version->{$binary};
 
 }
+my sub extract_version {
+    my ($machine_type, $version) = @_;
+    $version = kvm_user_version() if !defined($version);
+    PVE::QemuServer::Machine::extract_version($machine_type, $version)
+}
 
 sub kernel_has_vhost_net {
     return -c '/dev/vhost-net';
 }
 
-sub valid_drive_names {
-    # order is important - used to autoselect boot disk
-    return ((map { "ide$_" } (0 .. ($MAX_IDE_DISKS - 1))),
-            (map { "scsi$_" } (0 .. ($MAX_SCSI_DISKS - 1))),
-            (map { "virtio$_" } (0 .. ($MAX_VIRTIO_DISKS - 1))),
-            (map { "sata$_" } (0 .. ($MAX_SATA_DISKS - 1))),
-            'efidisk0');
-}
-
-sub is_valid_drivename {
-    my $dev = shift;
-
-    return defined($drivename_hash->{$dev});
-}
-
 sub option_exists {
     my $key = shift;
     return defined($confdesc->{$key});
 }
 
-sub nic_models {
-    return $nic_model_list;
-}
-
-sub os_list_description {
-
-    return {
-       other => 'Other',
-       wxp => 'Windows XP',
-       w2k => 'Windows 2000',
-       w2k3 =>, 'Windows 2003',
-       w2k8 => 'Windows 2008',
-       wvista => 'Windows Vista',
-       win7 => 'Windows 7',
-       win8 => 'Windows 8/2012',
-       win10 => 'Windows 10/2016',
-       l24 => 'Linux 2.4',
-       l26 => 'Linux 2.6',
-    };
-}
-
 my $cdrom_path;
-
 sub get_cdrom_path {
 
     return  $cdrom_path if $cdrom_path;
@@ -1647,7 +1283,8 @@ sub cleanup_drive_path {
        ($drive->{file} !~ m/^([^:]+):(.+)$/) &&
        ($drive->{file} !~ m/^\d+$/)) {
        my ($vtype, $volid) = PVE::Storage::path_to_volume_id($storecfg, $drive->{file});
-       raise_param_exc({ $opt => "unable to associate path '$drive->{file}' to any storage"}) if !$vtype;
+       raise_param_exc({ $opt => "unable to associate path '$drive->{file}' to any storage"})
+           if !$vtype;
        $drive->{media} = 'cdrom' if !$drive->{media} && $vtype eq 'iso';
        verify_media_type($opt, $vtype, $drive->{media});
        $drive->{file} = $volid;
@@ -1686,95 +1323,6 @@ sub pve_verify_hotplug_features {
     die "unable to parse hotplug option\n";
 }
 
-# ideX = [volume=]volume-id[,media=d][,cyls=c,heads=h,secs=s[,trans=t]]
-#        [,snapshot=on|off][,cache=on|off][,format=f][,backup=yes|no]
-#        [,rerror=ignore|report|stop][,werror=enospc|ignore|report|stop]
-#        [,aio=native|threads][,discard=ignore|on][,detect_zeroes=on|off]
-#        [,iothread=on][,serial=serial][,model=model]
-
-sub parse_drive {
-    my ($key, $data) = @_;
-
-    my ($interface, $index);
-
-    if ($key =~ m/^([^\d]+)(\d+)$/) {
-       $interface = $1;
-       $index = $2;
-    } else {
-       return undef;
-    }
-
-    my $desc = $key =~ /^unused\d+$/ ? $alldrive_fmt
-                                     : $confdesc->{$key}->{format};
-    if (!$desc) {
-       warn "invalid drive key: $key\n";
-       return undef;
-    }
-    my $res = eval { PVE::JSONSchema::parse_property_string($desc, $data) };
-    return undef if !$res;
-    $res->{interface} = $interface;
-    $res->{index} = $index;
-
-    my $error = 0;
-    foreach my $opt (qw(bps bps_rd bps_wr)) {
-       if (my $bps = defined(delete $res->{$opt})) {
-           if (defined($res->{"m$opt"})) {
-               warn "both $opt and m$opt specified\n";
-               ++$error;
-               next;
-           }
-           $res->{"m$opt"} = sprintf("%.3f", $bps / (1024*1024.0));
-       }
-    }
-
-    # can't use the schema's 'requires' because of the mbps* => bps* "transforming aliases"
-    for my $requirement (
-       [mbps_max => 'mbps'],
-       [mbps_rd_max => 'mbps_rd'],
-       [mbps_wr_max => 'mbps_wr'],
-       [miops_max => 'miops'],
-       [miops_rd_max => 'miops_rd'],
-       [miops_wr_max => 'miops_wr'],
-       [bps_max_length => 'mbps_max'],
-       [bps_rd_max_length => 'mbps_rd_max'],
-       [bps_wr_max_length => 'mbps_wr_max'],
-       [iops_max_length => 'iops_max'],
-       [iops_rd_max_length => 'iops_rd_max'],
-       [iops_wr_max_length => 'iops_wr_max']) {
-       my ($option, $requires) = @$requirement;
-       if ($res->{$option} && !$res->{$requires}) {
-           warn "$option requires $requires\n";
-           ++$error;
-       }
-    }
-
-    return undef if $error;
-
-    return undef if $res->{mbps_rd} && $res->{mbps};
-    return undef if $res->{mbps_wr} && $res->{mbps};
-    return undef if $res->{iops_rd} && $res->{iops};
-    return undef if $res->{iops_wr} && $res->{iops};
-
-    if ($res->{media} && ($res->{media} eq 'cdrom')) {
-       return undef if $res->{snapshot} || $res->{trans} || $res->{format};
-       return undef if $res->{heads} || $res->{secs} || $res->{cyls};
-       return undef if $res->{interface} eq 'virtio';
-    }
-
-    if (my $size = $res->{size}) {
-       return undef if !defined($res->{size} = PVE::JSONSchema::parse_size($size));
-    }
-
-    return $res;
-}
-
-sub print_drive {
-    my ($vmid, $drive) = @_;
-    my $data = { %$drive };
-    delete $data->{$_} for qw(index interface);
-    return PVE::JSONSchema::print_property_string($data, $alldrive_fmt);
-}
-
 sub scsi_inquiry {
     my($fh, $noerr) = @_;
 
@@ -1836,20 +1384,14 @@ sub path_is_scsi {
     return $res;
 }
 
-sub machine_type_is_q35 {
-    my ($conf) = @_;
-
-    return $conf->{machine} && ($conf->{machine} =~ m/q35/) ? 1 : 0;
-}
-
 sub print_tabletdevice_full {
     my ($conf, $arch) = @_;
 
-    my $q35 = machine_type_is_q35($conf);
+    my $q35 = PVE::QemuServer::Machine::machine_type_is_q35($conf);
 
     # we use uhci for old VMs because tablet driver was buggy in older qemu
     my $usbbus;
-    if (machine_type_is_q35($conf) || $arch eq 'aarch64') {
+    if (PVE::QemuServer::Machine::machine_type_is_q35($conf) || $arch eq 'aarch64') {
        $usbbus = 'ehci';
     } else {
        $usbbus = 'uhci';
@@ -1872,10 +1414,11 @@ sub print_drivedevice_full {
     my $device = '';
     my $maxdev = 0;
 
+    my $drive_id = "$drive->{interface}$drive->{index}";
     if ($drive->{interface} eq 'virtio') {
-       my $pciaddr = print_pci_addr("$drive->{interface}$drive->{index}", $bridges, $arch, $machine_type);
-       $device = "virtio-blk-pci,drive=drive-$drive->{interface}$drive->{index},id=$drive->{interface}$drive->{index}$pciaddr";
-       $device .= ",iothread=iothread-$drive->{interface}$drive->{index}" if $drive->{iothread};
+       my $pciaddr = print_pci_addr("$drive_id", $bridges, $arch, $machine_type);
+       $device = "virtio-blk-pci,drive=drive-$drive_id,id=${drive_id}${pciaddr}";
+       $device .= ",iothread=iothread-$drive_id" if $drive->{iothread};
     } elsif ($drive->{interface} eq 'scsi') {
 
        my ($maxdev, $controller, $controller_prefix) = scsihw_infos($conf, $drive);
@@ -1898,16 +1441,21 @@ sub print_drivedevice_full {
                 $path = PVE::Storage::path($storecfg, $drive->{file});
            }
 
-           if($path =~ m/^iscsi\:\/\//){
+           # for compatibility only, we prefer scsi-hd (#2408, #2355, #2380)
+           my $version = extract_version($machine_type, kvm_user_version());
+           if ($path =~ m/^iscsi\:\/\// &&
+              !min_version($version, 4, 1)) {
                $devicetype = 'generic';
            }
        }
 
        if (!$conf->{scsihw} || ($conf->{scsihw} =~ m/^lsi/)){
-           $device = "scsi-$devicetype,bus=$controller_prefix$controller.0,scsi-id=$unit,drive=drive-$drive->{interface}$drive->{index},id=$drive->{interface}$drive->{index}";
+           $device = "scsi-$devicetype,bus=$controller_prefix$controller.0,scsi-id=$unit";
        } else {
-           $device = "scsi-$devicetype,bus=$controller_prefix$controller.0,channel=0,scsi-id=0,lun=$drive->{index},drive=drive-$drive->{interface}$drive->{index},id=$drive->{interface}$drive->{index}";
+           $device = "scsi-$devicetype,bus=$controller_prefix$controller.0,channel=0,scsi-id=0"
+               .",lun=$drive->{index}";
        }
+       $device .= ",drive=drive-$drive_id,id=$drive_id";
 
        if ($drive->{ssd} && ($devicetype eq 'block' || $devicetype eq 'hd')) {
            $device .= ",rotation_rate=1";
@@ -1915,7 +1463,7 @@ sub print_drivedevice_full {
        $device .= ",wwn=$drive->{wwn}" if $drive->{wwn};
 
     } elsif ($drive->{interface} eq 'ide' || $drive->{interface} eq 'sata') {
-       my $maxdev = ($drive->{interface} eq 'sata') ? $MAX_SATA_DISKS : 2;
+       my $maxdev = ($drive->{interface} eq 'sata') ? $PVE::QemuServer::Drive::MAX_SATA_DISKS : 2;
        my $controller = int($drive->{index} / $maxdev);
        my $unit = $drive->{index} % $maxdev;
        my $devicetype = ($drive->{media} && $drive->{media} eq 'cdrom') ? "cd" : "hd";
@@ -1926,7 +1474,7 @@ sub print_drivedevice_full {
        } else {
            $device .= ",bus=ahci$controller.$unit";
        }
-       $device .= ",drive=drive-$drive->{interface}$drive->{index},id=$drive->{interface}$drive->{index}";
+       $device .= ",drive=drive-$drive_id,id=$drive_id";
 
        if ($devicetype eq 'hd') {
            if (my $model = $drive->{model}) {
@@ -1970,7 +1518,7 @@ sub get_initiator_name {
     return $initiator;
 }
 
-sub print_drive_full {
+sub print_drive_commandline_full {
     my ($storecfg, $vmid, $drive) = @_;
 
     my $path;
@@ -2076,12 +1624,29 @@ sub print_netdevice_full {
     my $pciaddr = print_pci_addr("$netid", $bridges, $arch, $machine_type);
     my $tmpstr = "$device,mac=$net->{macaddr},netdev=$netid$pciaddr,id=$netid";
     if ($net->{queues} && $net->{queues} > 1 && $net->{model} eq 'virtio'){
-       #Consider we have N queues, the number of vectors needed is 2*N + 2 (plus one config interrupt and control vq)
+       # Consider we have N queues, the number of vectors needed is 2 * N + 2, i.e., one per in
+       # and out of each queue plus one config interrupt and control vector queue
        my $vectors = $net->{queues} * 2 + 2;
        $tmpstr .= ",vectors=$vectors,mq=on";
     }
     $tmpstr .= ",bootindex=$net->{bootindex}" if $net->{bootindex} ;
 
+    if (my $mtu = $net->{mtu}) {
+       if ($net->{model} eq 'virtio' && $net->{bridge}) {
+           my $bridge_mtu = PVE::Network::read_bridge_mtu($net->{bridge});
+           if ($mtu == 1) {
+                $mtu = $bridge_mtu;
+           } elsif ($mtu < 576) {
+               die "netdev $netid: MTU '$mtu' is smaller than the IP minimum MTU '576'\n";
+           } elsif ($mtu > $bridge_mtu) {
+               die "netdev $netid: MTU '$mtu' is bigger than the bridge MTU '$bridge_mtu'\n";
+           }
+           $tmpstr .= ",host_mtu=$mtu";
+       } else {
+           warn "WARN: netdev $netid: ignoring MTU '$mtu', not using VirtIO or no bridge configured.\n";
+       }
+    }
+
     if ($use_old_bios_files) {
        my $romfile;
        if ($device eq 'virtio-net-pci') {
@@ -2128,7 +1693,8 @@ sub print_netdev_full {
     my $script = $hotplug ? "pve-bridge-hotplug" : "pve-bridge";
 
     if ($net->{bridge}) {
-        $netdev = "type=tap,id=$netid,ifname=${ifname},script=/var/lib/qemu-server/$script,downscript=/var/lib/qemu-server/pve-bridgedown$vhostparam";
+       $netdev = "type=tap,id=$netid,ifname=${ifname},script=/var/lib/qemu-server/$script"
+           .",downscript=/var/lib/qemu-server/pve-bridgedown$vhostparam";
     } else {
         $netdev = "type=user,id=$netid,hostname=$vmname";
     }
@@ -2138,26 +1704,6 @@ sub print_netdev_full {
     return $netdev;
 }
 
-
-sub print_cpu_device {
-    my ($conf, $id) = @_;
-
-    my $kvm = $conf->{kvm} // 1;
-    my $cpu = $kvm ? "kvm64" : "qemu64";
-    if (my $cputype = $conf->{cpu}) {
-       my $cpuconf = PVE::JSONSchema::parse_property_string($cpu_fmt, $cputype)
-           or die "Cannot parse cpu description: $cputype\n";
-       $cpu = $cpuconf->{cputype};
-    }
-
-    my $cores = $conf->{cores} || 1;
-
-    my $current_core = ($id - 1) % $cores;
-    my $current_socket = int(($id - 1 - $current_core)/$cores);
-
-    return "$cpu-x86_64-cpu,id=cpu$id,socket-id=$current_socket,core-id=$current_core,thread-id=0";
-}
-
 my $vga_map = {
     'cirrus' => 'cirrus-vga',
     'std' => 'VGA',
@@ -2166,16 +1712,26 @@ my $vga_map = {
 };
 
 sub print_vga_device {
-    my ($conf, $vga, $arch, $machine, $id, $qxlnum, $bridges) = @_;
+    my ($conf, $vga, $arch, $machine_version, $machine, $id, $qxlnum, $bridges) = @_;
 
     my $type = $vga_map->{$vga->{type}};
     if ($arch eq 'aarch64' && defined($type) && $type eq 'virtio-vga') {
        $type = 'virtio-gpu';
     }
     my $vgamem_mb = $vga->{memory};
+
+    my $max_outputs = '';
     if ($qxlnum) {
        $type = $id ? 'qxl' : 'qxl-vga';
+
+       if (!$conf->{ostype} || $conf->{ostype} =~ m/^(?:l\d\d)|(?:other)$/) {
+           # set max outputs so linux can have up to 4 qxl displays with one device
+           if (min_version($machine_version, 4, 1)) {
+               $max_outputs = ",max_outputs=4";
+           }
+       }
     }
+
     die "no devicetype for $vga->{type}\n" if !$type;
 
     my $memory = "";
@@ -2196,7 +1752,12 @@ sub print_vga_device {
        $memory = ",ram_size=67108864,vram_size=33554432";
     }
 
-    my $q35 = machine_type_is_q35($conf);
+    my $edidoff = "";
+    if ($type eq 'VGA' && windows_version($conf->{ostype})) {
+       $edidoff=",edid=off" if (!defined($conf->{bios}) || $conf->{bios} ne 'ovmf');
+    }
+
+    my $q35 = PVE::QemuServer::Machine::machine_type_is_q35($conf);
     my $vgaid = "vga" . ($id // '');
     my $pciaddr;
 
@@ -2207,21 +1768,7 @@ sub print_vga_device {
        $pciaddr = print_pci_addr($vgaid, $bridges, $arch, $machine);
     }
 
-    return "$type,id=${vgaid}${memory}${pciaddr}";
-}
-
-sub drive_is_cloudinit {
-    my ($drive) = @_;
-    return $drive->{file} =~ m@[:/]vm-\d+-cloudinit(?:\.$QEMU_FORMAT_RE)?$@;
-}
-
-sub drive_is_cdrom {
-    my ($drive, $exclude_cloudinit) = @_;
-
-    return 0 if $exclude_cloudinit && drive_is_cloudinit($drive);
-
-    return $drive && $drive->{media} && ($drive->{media} eq 'cdrom');
-
+    return "$type,id=${vgaid}${memory}${max_outputs}${pciaddr}${edidoff}";
 }
 
 sub parse_number_sets {
@@ -2241,38 +1788,17 @@ sub parse_number_sets {
 sub parse_numa {
     my ($data) = @_;
 
-    my $res = PVE::JSONSchema::parse_property_string($numa_fmt, $data);
+    my $res = parse_property_string($numa_fmt, $data);
     $res->{cpus} = parse_number_sets($res->{cpus}) if defined($res->{cpus});
     $res->{hostnodes} = parse_number_sets($res->{hostnodes}) if defined($res->{hostnodes});
     return $res;
 }
 
-sub parse_hostpci {
-    my ($value) = @_;
-
-    return undef if !$value;
-
-    my $res = PVE::JSONSchema::parse_property_string($hostpci_fmt, $value);
-
-    my @idlist = split(/;/, $res->{host});
-    delete $res->{host};
-    foreach my $id (@idlist) {
-       if ($id =~ m/\./) { # full id 00:00.1
-           push @{$res->{pciid}}, {
-               id => $id,
-           };
-       } else { # partial id 00:00
-           $res->{pciid} = PVE::SysFSTools::lspci($id);
-       }
-    }
-    return $res;
-}
-
 # netX: e1000=XX:XX:XX:XX:XX:XX,bridge=vmbr0,rate=<mbps>
 sub parse_net {
     my ($data) = @_;
 
-    my $res = eval { PVE::JSONSchema::parse_property_string($net_fmt, $data) };
+    my $res = eval { parse_property_string($net_fmt, $data) };
     if ($@) {
        warn $@;
        return undef;
@@ -2288,7 +1814,7 @@ sub parse_net {
 sub parse_ipconfig {
     my ($data) = @_;
 
-    my $res = eval { PVE::JSONSchema::parse_property_string($ipconfig_fmt, $data) };
+    my $res = eval { parse_property_string($ipconfig_fmt, $data) };
     if ($@) {
        warn $@;
        return undef;
@@ -2425,7 +1951,7 @@ my $smbios1_fmt = {
 sub parse_smbios1 {
     my ($data) = @_;
 
-    my $res = eval { PVE::JSONSchema::parse_property_string($smbios1_fmt, $data) };
+    my $res = eval { parse_property_string($smbios1_fmt, $data) };
     warn $@ if $@;
     return $res;
 }
@@ -2437,23 +1963,12 @@ sub print_smbios1 {
 
 PVE::JSONSchema::register_format('pve-qm-smbios1', $smbios1_fmt);
 
-PVE::JSONSchema::register_format('pve-qm-bootdisk', \&verify_bootdisk);
-sub verify_bootdisk {
-    my ($value, $noerr) = @_;
-
-    return $value if is_valid_drivename($value);
-
-    return undef if $noerr;
-
-    die "invalid boot disk '$value'\n";
-}
-
 sub parse_watchdog {
     my ($value) = @_;
 
     return undef if !$value;
 
-    my $res = eval { PVE::JSONSchema::parse_property_string($watchdog_fmt, $value) };
+    my $res = eval { parse_property_string($watchdog_fmt, $value) };
     warn $@ if $@;
     return $res;
 }
@@ -2463,7 +1978,7 @@ sub parse_guest_agent {
 
     return {} if !defined($value->{agent});
 
-    my $res = eval { PVE::JSONSchema::parse_property_string($agent_fmt, $value->{agent}) };
+    my $res = eval { parse_property_string($agent_fmt, $value->{agent}) };
     warn $@ if $@;
 
     # if the agent is disabled ignore the other potentially set properties
@@ -2475,7 +1990,17 @@ sub parse_vga {
     my ($value) = @_;
 
     return {} if !$value;
-    my $res = eval { PVE::JSONSchema::parse_property_string($vga_fmt, $value) };
+    my $res = eval { parse_property_string($vga_fmt, $value) };
+    warn $@ if $@;
+    return $res;
+}
+
+sub parse_rng {
+    my ($value) = @_;
+
+    return undef if !$value;
+
+    my $res = eval { parse_property_string($rng_fmt, $value) };
     warn $@ if $@;
     return $res;
 }
@@ -2496,7 +2021,8 @@ sub json_config_properties {
     my $prop = shift;
 
     foreach my $opt (keys %$confdesc) {
-       next if $opt eq 'parent' || $opt eq 'snaptime' || $opt eq 'vmstate' || $opt eq 'runningmachine';
+       next if $opt eq 'parent' || $opt eq 'snaptime' || $opt eq 'vmstate' ||
+           $opt eq 'runningmachine' || $opt eq 'runningcpu';
        $prop->{$opt} = $confdesc->{$opt};
     }
 
@@ -2546,15 +2072,8 @@ sub check_type {
     }
 }
 
-sub touch_config {
-    my ($vmid) = @_;
-
-    my $conf = PVE::QemuConfig->config_file($vmid);
-    utime undef, undef, $conf;
-}
-
 sub destroy_vm {
-    my ($storecfg, $vmid, $keep_empty_config, $skiplock) = @_;
+    my ($storecfg, $vmid, $skiplock, $replacement_conf) = @_;
 
     my $conf = PVE::QemuConfig->load_config($vmid);
 
@@ -2562,13 +2081,11 @@ sub destroy_vm {
 
     if ($conf->{template}) {
        # check if any base image is still used by a linked clone
-       foreach_drive($conf, sub {
+       PVE::QemuConfig->foreach_volume($conf, sub {
                my ($ds, $drive) = @_;
-
                return if drive_is_cdrom($drive);
 
                my $volid = $drive->{file};
-
                return if !$volid || $volid =~ m|^/|;
 
                die "base volume '$volid' is still in use by linked cloned\n"
@@ -2578,45 +2095,33 @@ sub destroy_vm {
     }
 
     # only remove disks owned by this VM
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
-
        return if drive_is_cdrom($drive, 1);
 
        my $volid = $drive->{file};
-
        return if !$volid || $volid =~ m|^/|;
 
        my ($path, $owner) = PVE::Storage::path($storecfg, $volid);
        return if !$path || !$owner || ($owner != $vmid);
 
-       eval {
-           PVE::Storage::vdisk_free($storecfg, $volid);
-       };
+       eval { PVE::Storage::vdisk_free($storecfg, $volid) };
        warn "Could not remove disk '$volid', check manually: $@" if $@;
+    });
 
+    # also remove unused disk
+    my $vmdisks = PVE::Storage::vdisk_list($storecfg, undef, $vmid);
+    PVE::Storage::foreach_volid($vmdisks, sub {
+       my ($volid, $sid, $volname, $d) = @_;
+       eval { PVE::Storage::vdisk_free($storecfg, $volid) };
+       warn $@ if $@;
     });
 
-    if ($keep_empty_config) {
-       PVE::QemuConfig->write_config($vmid, { memory => 128 });
+    if (defined $replacement_conf) {
+       PVE::QemuConfig->write_config($vmid, $replacement_conf);
     } else {
        PVE::QemuConfig->destroy_config($vmid);
     }
-
-    # also remove unused disk
-    eval {
-       my $dl = PVE::Storage::vdisk_list($storecfg, undef, $vmid);
-
-       eval {
-           PVE::Storage::foreach_volid($dl, sub {
-               my ($volid, $sid, $volname, $d) = @_;
-               PVE::Storage::vdisk_free($storecfg, $volid);
-           });
-       };
-       warn $@ if $@;
-
-    };
-    warn $@ if $@;
 }
 
 sub parse_vm_config {
@@ -2699,7 +2204,7 @@ sub parse_vm_config {
                    my $v = parse_drive($key, $value);
                    if (my $volid = filename_to_volume_id($vmid, $v->{file}, $v->{media})) {
                        $v->{file} = $volid;
-                       $value = print_drive($vmid, $v);
+                       $value = print_drive($v);
                    } else {
                        warn "vm $vmid - unable to parse value of '$key'\n";
                        next;
@@ -2772,7 +2277,7 @@ sub write_vm_config {
     &$cleanup_config($conf->{pending}, 1);
 
     foreach my $snapname (keys %{$conf->{snapshots}}) {
-       die "internal error" if $snapname eq 'pending';
+       die "internal error: snapshot name '$snapname' is forbidden" if lc($snapname) eq 'pending';
        &$cleanup_config($conf->{snapshots}->{$snapname}, undef, $snapname);
     }
 
@@ -2801,7 +2306,7 @@ sub write_vm_config {
        }
 
        foreach my $key (sort keys %$conf) {
-           next if $key eq 'digest' || $key eq 'description' || $key eq 'pending' || $key eq 'snapshots';
+           next if $key =~ /^(digest|description|pending|snapshots)$/;
            $raw .= "$key: $conf->{$key}\n";
        }
        return $raw;
@@ -2841,6 +2346,7 @@ sub config_list {
     my $res = {};
     return $res if !$vmlist || !$vmlist->{ids};
     my $ids = $vmlist->{ids};
+    my $nodename = nodename();
 
     foreach my $vmid (keys %$ids) {
        my $d = $ids->{$vmid};
@@ -2878,7 +2384,7 @@ sub check_local_resources {
 sub check_storage_availability {
     my ($storecfg, $conf, $node) = @_;
 
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
 
        my $volid = $drive->{file};
@@ -2899,9 +2405,9 @@ sub shared_nodes {
 
     my $nodelist = PVE::Cluster::get_nodelist();
     my $nodehash = { map { $_ => 1 } @$nodelist };
-    my $nodename = PVE::INotify::nodename();
+    my $nodename = nodename();
 
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
 
        my $volid = $drive->{file};
@@ -2933,7 +2439,7 @@ sub check_local_storage_availability {
     my $nodelist = PVE::Cluster::get_nodelist();
     my $nodehash = { map { $_ => {} } @$nodelist };
 
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
 
        my $volid = $drive->{file};
@@ -2966,70 +2472,19 @@ sub check_local_storage_availability {
     return $nodehash
 }
 
-sub check_cmdline {
-    my ($pidfile, $pid) = @_;
-
-    my $fh = IO::File->new("/proc/$pid/cmdline", "r");
-    if (defined($fh)) {
-       my $line = <$fh>;
-       $fh->close;
-       return undef if !$line;
-       my @param = split(/\0/, $line);
-
-       my $cmd = $param[0];
-       return if !$cmd || ($cmd !~ m|kvm$| && $cmd !~ m@(?:^|/)qemu-system-[^/]+$@);
-
-       for (my $i = 0; $i < scalar (@param); $i++) {
-           my $p = $param[$i];
-           next if !$p;
-           if (($p eq '-pidfile') || ($p eq '--pidfile')) {
-               my $p = $param[$i+1];
-               return 1 if $p && ($p eq $pidfile);
-               return undef;
-           }
-       }
-    }
-    return undef;
-}
-
+# Compat only, use assert_config_exists_on_node and vm_running_locally where possible
 sub check_running {
     my ($vmid, $nocheck, $node) = @_;
 
-    my $filename = PVE::QemuConfig->config_file($vmid, $node);
-
-    die "unable to find configuration file for VM $vmid - no such machine\n"
-       if !$nocheck && ! -f $filename;
-
-    my $pidfile = pidfile_name($vmid);
-
-    if (my $fd = IO::File->new("<$pidfile")) {
-       my $st = stat($fd);
-       my $line = <$fd>;
-       close($fd);
-
-       my $mtime = $st->mtime;
-       if ($mtime > time()) {
-           warn "file '$filename' modified in future\n";
-       }
-
-       if ($line =~ m/^(\d+)$/) {
-           my $pid = $1;
-           if (check_cmdline($pidfile, $pid)) {
-               if (my $pinfo = PVE::ProcFSTools::check_process_running($pid)) {
-                   return $pid;
-               }
-           }
-       }
-    }
-
-    return undef;
+    PVE::QemuConfig::assert_config_exists_on_node($vmid, $node) if !$nocheck;
+    return PVE::QemuServer::Helpers::vm_running_locally($vmid);
 }
 
 sub vzlist {
 
     my $vzlist = config_list();
 
-    my $fd = IO::Dir->new($var_run_tmpdir) || return $vzlist;
+    my $fd = IO::Dir->new($PVE::QemuServer::Helpers::var_run_tmpdir) || return $vzlist;
 
     while (defined(my $de = $fd->read)) {
        next if $de !~ m/^(\d+)\.pid$/;
@@ -3043,26 +2498,6 @@ sub vzlist {
     return $vzlist;
 }
 
-sub disksize {
-    my ($storecfg, $conf) = @_;
-
-    my $bootdisk = $conf->{bootdisk};
-    return undef if !$bootdisk;
-    return undef if !is_valid_drivename($bootdisk);
-
-    return undef if !$conf->{$bootdisk};
-
-    my $drive = parse_drive($bootdisk, $conf->{$bootdisk});
-    return undef if !defined($drive);
-
-    return undef if drive_is_cdrom($drive);
-
-    my $volid = $drive->{file};
-    return undef if !$volid;
-
-    return $drive->{size};
-}
-
 our $vmstatus_return_properties = {
     vmid => get_standard_option('pve-vmid'),
     status => {
@@ -3112,7 +2547,12 @@ our $vmstatus_return_properties = {
        description => "The current config lock, if any.",
        type => 'string',
        optional => 1,
-    }
+    },
+    tags => {
+       description  => "The current configured tags, if any",
+       type => 'string',
+       optional => 1,
+    },
 };
 
 my $last_proc_pid_stat;
@@ -3137,8 +2577,7 @@ sub vmstatus {
     foreach my $vmid (keys %$list) {
        next if $opt_vmid && ($vmid ne $opt_vmid);
 
-       my $cfspath = PVE::QemuConfig->cfs_config_path($vmid);
-       my $conf = PVE::Cluster::cfs_read_file($cfspath) || {};
+       my $conf = PVE::QemuConfig->load_config($vmid);
 
        my $d = { vmid => $vmid };
        $d->{pid} = $list->{$vmid}->{pid};
@@ -3146,7 +2585,7 @@ sub vmstatus {
        # fixme: better status?
        $d->{status} = $list->{$vmid}->{pid} ? 'running' : 'stopped';
 
-       my $size = disksize($storecfg, $conf);
+       my $size = PVE::QemuServer::Drive::bootdisk_size($storecfg, $conf);
        if (defined($size)) {
            $d->{disk} = 0; # no info available
            $d->{maxdisk} = $size;
@@ -3184,6 +2623,7 @@ sub vmstatus {
 
        $d->{serial} = 1 if conf_has_serial($conf);
        $d->{lock} = $conf->{lock} if $conf->{lock};
+       $d->{tags} = $conf->{tags} if defined($conf->{tags});
 
        $res->{$vmid} = $d;
     }
@@ -3324,65 +2764,6 @@ sub vmstatus {
     return $res;
 }
 
-sub foreach_drive {
-    my ($conf, $func, @param) = @_;
-
-    foreach my $ds (valid_drive_names()) {
-       next if !defined($conf->{$ds});
-
-       my $drive = parse_drive($ds, $conf->{$ds});
-       next if !$drive;
-
-       &$func($ds, $drive, @param);
-    }
-}
-
-sub foreach_volid {
-    my ($conf, $func, @param) = @_;
-
-    my $volhash = {};
-
-    my $test_volid = sub {
-       my ($volid, $is_cdrom, $replicate, $shared, $snapname, $size) = @_;
-
-       return if !$volid;
-
-       $volhash->{$volid}->{cdrom} //= 1;
-       $volhash->{$volid}->{cdrom} = 0 if !$is_cdrom;
-
-       $volhash->{$volid}->{replicate} //= 0;
-       $volhash->{$volid}->{replicate} = 1 if $replicate;
-
-       $volhash->{$volid}->{shared} //= 0;
-       $volhash->{$volid}->{shared} = 1 if $shared;
-
-       $volhash->{$volid}->{referenced_in_config} //= 0;
-       $volhash->{$volid}->{referenced_in_config} = 1 if !defined($snapname);
-
-       $volhash->{$volid}->{referenced_in_snapshot}->{$snapname} = 1
-           if defined($snapname);
-       $volhash->{$volid}->{size} = $size if $size;
-    };
-
-    foreach_drive($conf, sub {
-       my ($ds, $drive) = @_;
-       $test_volid->($drive->{file}, drive_is_cdrom($drive), $drive->{replicate} // 1, $drive->{shared}, undef, $drive->{size});
-    });
-
-    foreach my $snapname (keys %{$conf->{snapshots}}) {
-       my $snap = $conf->{snapshots}->{$snapname};
-       $test_volid->($snap->{vmstate}, 0, 1, $snapname);
-       foreach_drive($snap, sub {
-           my ($ds, $drive) = @_;
-           $test_volid->($drive->{file}, drive_is_cdrom($drive), $drive->{replicate} // 1, $drive->{shared}, $snapname);
-        });
-    }
-
-    foreach my $volid (keys %$volhash) {
-       &$func($volid, $volhash->{$volid}, @param);
-    }
-}
-
 sub conf_has_serial {
     my ($conf) = @_;
 
@@ -3402,7 +2783,7 @@ sub conf_has_audio {
     my $audio = $conf->{"audio$id"};
     return undef if !defined($audio);
 
-    my $audioproperties = PVE::JSONSchema::parse_property_string($audio_fmt, $audio);
+    my $audioproperties = parse_property_string($audio_fmt, $audio);
     my $audiodriver = $audioproperties->{driver} // 'spice';
 
     return {
@@ -3413,6 +2794,32 @@ sub conf_has_audio {
     };
 }
 
+sub audio_devs {
+    my ($audio, $audiopciaddr, $machine_version) = @_;
+
+    my $devs = [];
+
+    my $id = $audio->{dev_id};
+    my $audiodev = "";
+    if (min_version($machine_version, 4, 2)) {
+       $audiodev = ",audiodev=$audio->{backend_id}";
+    }
+
+    if ($audio->{dev} eq 'AC97') {
+       push @$devs, '-device', "AC97,id=${id}${audiopciaddr}$audiodev";
+    } elsif ($audio->{dev} =~ /intel\-hda$/) {
+       push @$devs, '-device', "$audio->{dev},id=${id}${audiopciaddr}";
+       push @$devs, '-device', "hda-micro,id=${id}-codec0,bus=${id}.0,cad=0$audiodev";
+       push @$devs, '-device', "hda-duplex,id=${id}-codec1,bus=${id}.0,cad=1$audiodev";
+    } else {
+       die "unkown audio device '$audio->{dev}', implement me!";
+    }
+
+    push @$devs, '-audiodev', "$audio->{backend},id=$audio->{backend_id}";
+
+    return $devs;
+}
+
 sub vga_conf_has_spice {
     my ($vga) = @_;
 
@@ -3423,28 +2830,43 @@ sub vga_conf_has_spice {
     return $1 || 1;
 }
 
-my $host_arch; # FIXME: fix PVE::Tools::get_host_arch
-sub get_host_arch() {
-    $host_arch = (POSIX::uname())[4] if !$host_arch;
-    return $host_arch;
-}
-
 sub is_native($) {
     my ($arch) = @_;
     return get_host_arch() eq $arch;
 }
 
+sub get_vm_arch {
+    my ($conf) = @_;
+    return $conf->{arch} // get_host_arch();
+}
+
 my $default_machines = {
     x86_64 => 'pc',
     aarch64 => 'virt',
 };
 
-sub get_basic_machine_info {
-    my ($conf, $forcemachine) = @_;
+sub get_vm_machine {
+    my ($conf, $forcemachine, $arch, $add_pve_version, $kvmversion) = @_;
+
+    my $machine = $forcemachine || $conf->{machine};
+
+    if (!$machine || $machine =~ m/^(?:pc|q35|virt)$/) {
+       $arch //= 'x86_64';
+       $machine ||= $default_machines->{$arch};
+       if ($add_pve_version) {
+           $kvmversion //= kvm_user_version();
+           my $pvever = PVE::QemuServer::Machine::get_pve_version($kvmversion);
+           $machine .= "+pve$pvever";
+       }
+    }
+
+    if ($add_pve_version && $machine !~ m/\+pve\d+$/) {
+       # for version-pinned machines that do not include a pve-version (e.g.
+       # pc-q35-4.1), we assume 0 to keep them stable in case we bump
+       $machine .= '+pve0';
+    }
 
-    my $arch = $conf->{arch} // get_host_arch();
-    my $machine = $forcemachine || $conf->{machine} || $default_machines->{$arch};
-    return ($arch, $machine);
+    return $machine;
 }
 
 sub get_ovmf_files($) {
@@ -3469,64 +2891,127 @@ sub get_command_for_arch($) {
     return $cmd;
 }
 
-sub get_cpu_options {
-    my ($conf, $arch, $kvm, $machine_type, $kvm_off, $kvmver, $winversion, $gpu_passthrough) = @_;
+# To use query_supported_cpu_flags and query_understood_cpu_flags to get flags
+# to use in a QEMU command line (-cpu element), first array_intersect the result
+# of query_supported_ with query_understood_. This is necessary because:
+#
+# a) query_understood_ returns flags the host cannot use and
+# b) query_supported_ (rather the QMP call) doesn't actually return CPU
+#    flags, but CPU settings - with most of them being flags. Those settings
+#    (and some flags, curiously) cannot be specified as a "-cpu" argument.
+#
+# query_supported_ needs to start up to 2 temporary VMs and is therefore rather
+# expensive. If you need the value returned from this, you can get it much
+# cheaper from pmxcfs using PVE::Cluster::get_node_kv('cpuflags-$accel') with
+# $accel being 'kvm' or 'tcg'.
+#
+# pvestatd calls this function on startup and whenever the QEMU/KVM version
+# changes, automatically populating pmxcfs.
+#
+# Returns: { kvm => [ flagX, flagY, ... ], tcg => [ flag1, flag2, ... ] }
+# since kvm and tcg machines support different flags
+#
+sub query_supported_cpu_flags {
+    my ($arch) = @_;
+
+    $arch //= get_host_arch();
+    my $default_machine = $default_machines->{$arch};
 
-    my $cpuFlags = [];
-    my $ostype = $conf->{ostype};
+    my $flags = {};
 
-    my $cpu = $kvm ? "kvm64" : "qemu64";
-    if ($arch eq 'aarch64') {
-       $cpu = 'cortex-a57';
-    }
-    my $hv_vendor_id;
-    if (my $cputype = $conf->{cpu}) {
-       my $cpuconf = PVE::JSONSchema::parse_property_string($cpu_fmt, $cputype)
-           or die "Cannot parse cpu description: $cputype\n";
-       $cpu = $cpuconf->{cputype};
-       $kvm_off = 1 if $cpuconf->{hidden};
-       $hv_vendor_id = $cpuconf->{'hv-vendor-id'};
+    # FIXME: Once this is merged, the code below should work for ARM as well:
+    # https://lists.nongnu.org/archive/html/qemu-devel/2019-06/msg04947.html
+    die "QEMU/KVM cannot detect CPU flags on ARM (aarch64)\n" if
+       $arch eq "aarch64";
+
+    my $kvm_supported = defined(kvm_version());
+    my $qemu_cmd = get_command_for_arch($arch);
+    my $fakevmid = -1;
+    my $pidfile = PVE::QemuServer::Helpers::pidfile_name($fakevmid);
 
-       if (defined(my $flags = $cpuconf->{flags})) {
-           push @$cpuFlags, split(";", $flags);
+    # Start a temporary (frozen) VM with vmid -1 to allow sending a QMP command
+    my $query_supported_run_qemu = sub {
+       my ($kvm) = @_;
+
+       my $flags = {};
+       my $cmd = [
+           $qemu_cmd,
+           '-machine', $default_machine,
+           '-display', 'none',
+           '-chardev', "socket,id=qmp,path=/var/run/qemu-server/$fakevmid.qmp,server,nowait",
+           '-mon', 'chardev=qmp,mode=control',
+           '-pidfile', $pidfile,
+           '-S', '-daemonize'
+       ];
+
+       if (!$kvm) {
+           push @$cmd, '-accel', 'tcg';
        }
-    }
 
-    push @$cpuFlags , '+lahf_lm' if $cpu eq 'kvm64' && $arch eq 'x86_64';
+       my $rc = run_command($cmd, noerr => 1, quiet => 0);
+       die "QEMU flag querying VM exited with code " . $rc if $rc;
 
-    push @$cpuFlags , '-x2apic'
-       if $conf->{ostype} && $conf->{ostype} eq 'solaris';
+       eval {
+           my $cmd_result = mon_cmd(
+               $fakevmid,
+               'query-cpu-model-expansion',
+               type => 'full',
+               model => { name => 'host' }
+           );
+
+           my $props = $cmd_result->{model}->{props};
+           foreach my $prop (keys %$props) {
+               next if $props->{$prop} ne '1';
+               # QEMU returns some flags multiple times, with '_', '.' or '-'
+               # (e.g. lahf_lm and lahf-lm; sse4.2, sse4-2 and sse4_2; ...).
+               # We only keep those with underscores, to match /proc/cpuinfo
+               $prop =~ s/\.|-/_/g;
+               $flags->{$prop} = 1;
+           }
+       };
+       my $err = $@;
 
-    push @$cpuFlags, '+sep' if $cpu eq 'kvm64' || $cpu eq 'kvm32';
+       # force stop with 10 sec timeout and 'nocheck'
+       # always stop, even if QMP failed
+       vm_stop(undef, $fakevmid, 1, 1, 10, 0, 1);
 
-    push @$cpuFlags, '-rdtscp' if $cpu =~ m/^Opteron/;
+       die $err if $err;
 
-    if (qemu_machine_feature_enabled ($machine_type, $kvmver, 2, 3) && $arch eq 'x86_64') {
+       return [ sort keys %$flags ];
+    };
 
-       push @$cpuFlags , '+kvm_pv_unhalt' if $kvm;
-       push @$cpuFlags , '+kvm_pv_eoi' if $kvm;
-    }
+    # We need to query QEMU twice, since KVM and TCG have different supported flags
+    PVE::QemuConfig->lock_config($fakevmid, sub {
+       $flags->{tcg} = eval { $query_supported_run_qemu->(0) };
+       warn "warning: failed querying supported tcg flags: $@\n" if $@;
 
-    add_hyperv_enlightenments($cpuFlags, $winversion, $machine_type, $kvmver, $conf->{bios}, $gpu_passthrough, $hv_vendor_id) if $kvm;
+       if ($kvm_supported) {
+           $flags->{kvm} = eval { $query_supported_run_qemu->(1) };
+           warn "warning: failed querying supported kvm flags: $@\n" if $@;
+       }
+    });
 
-    push @$cpuFlags, 'enforce' if $cpu ne 'host' && $kvm && $arch eq 'x86_64';
+    return $flags;
+}
 
-    push @$cpuFlags, 'kvm=off' if $kvm_off;
+# Understood CPU flags are written to a file at 'pve-qemu' compile time
+my $understood_cpu_flag_dir = "/usr/share/kvm";
+sub query_understood_cpu_flags {
+    my $arch = get_host_arch();
+    my $filepath = "$understood_cpu_flag_dir/recognized-CPUID-flags-$arch";
 
-    if (my $cpu_vendor = $cpu_vendor_list->{$cpu}) {
-       push @$cpuFlags, "vendor=${cpu_vendor}"
-           if $cpu_vendor ne 'default';
-    } elsif ($arch ne 'aarch64') {
-       die "internal error"; # should not happen
-    }
+    die "Cannot query understood QEMU CPU flags for architecture: $arch (file not found)\n"
+       if ! -e $filepath;
 
-    $cpu .= "," . join(',', @$cpuFlags) if scalar(@$cpuFlags);
+    my $raw = file_get_contents($filepath);
+    $raw =~ s/^\s+|\s+$//g;
+    my @flags = split(/\s+/, $raw);
 
-    return ('-cpu', $cpu);
+    return \@flags;
 }
 
 sub config_to_command {
-    my ($storecfg, $vmid, $conf, $defaults, $forcemachine) = @_;
+    my ($storecfg, $vmid, $conf, $defaults, $forcemachine, $forcecpu) = @_;
 
     my $cmd = [];
     my $globalFlags = [];
@@ -3535,30 +3020,60 @@ sub config_to_command {
     my $devices = [];
     my $pciaddr = '';
     my $bridges = {};
-    my $vernum = 0; # unknown
     my $ostype = $conf->{ostype};
     my $winversion = windows_version($ostype);
     my $kvm = $conf->{kvm};
+    my $nodename = nodename();
 
-    my ($arch, $machine_type) = get_basic_machine_info($conf, $forcemachine);
+    my $arch = get_vm_arch($conf);
     my $kvm_binary = get_command_for_arch($arch);
     my $kvmver = kvm_user_version($kvm_binary);
-    $kvm //= 1 if is_native($arch);
 
-    if ($kvm) {
-       die "KVM virtualisation configured, but not available. Either disable in VM configuration or enable in BIOS.\n"
-           if !defined kvm_version();
+    if (!$kvmver || $kvmver !~ m/^(\d+)\.(\d+)/ || $1 < 3) {
+       $kvmver //= "undefined";
+       die "Detected old QEMU binary ('$kvmver', at least 3.0 is required)\n";
     }
 
-    if ($kvmver =~ m/^(\d+)\.(\d+)$/) {
-       $vernum = $1*1000000+$2*1000;
-    } elsif ($kvmver =~ m/^(\d+)\.(\d+)\.(\d+)$/) {
-       $vernum = $1*1000000+$2*1000+$3;
-    }
+    my $add_pve_version = min_version($kvmver, 4, 1);
+
+    my $machine_type = get_vm_machine($conf, $forcemachine, $arch, $add_pve_version);
+    my $machine_version = extract_version($machine_type, $kvmver);
+    $kvm //= 1 if is_native($arch);
+
+    $machine_version =~ m/(\d+)\.(\d+)/;
+    my ($machine_major, $machine_minor) = ($1, $2);
+
+    if ($kvmver =~ m/^\d+\.\d+\.(\d+)/ && $1 >= 90) {
+       warn "warning: Installed QEMU version ($kvmver) is a release candidate, ignoring version checks\n";
+    } elsif (!min_version($kvmver, $machine_major, $machine_minor)) {
+       die "Installed QEMU version '$kvmver' is too old to run machine type '$machine_type',"
+           ." please upgrade node '$nodename'\n"
+    } elsif (!PVE::QemuServer::Machine::can_run_pve_machine_version($machine_version, $kvmver)) {
+       my $max_pve_version = PVE::QemuServer::Machine::get_pve_version($machine_version);
+       die "Installed qemu-server (max feature level for $machine_major.$machine_minor is"
+           ." pve$max_pve_version) is too old to run machine type '$machine_type', please upgrade"
+           ." node '$nodename'\n";
+    }
+
+    # if a specific +pve version is required for a feature, use $version_guard
+    # instead of min_version to allow machines to be run with the minimum
+    # required version
+    my $required_pve_version = 0;
+    my $version_guard = sub {
+       my ($major, $minor, $pve) = @_;
+       return 0 if !min_version($machine_version, $major, $minor, $pve);
+       my $max_pve = PVE::QemuServer::Machine::get_pve_version("$major.$minor");
+       return 1 if min_version($machine_version, $major, $minor, $max_pve+1);
+       $required_pve_version = $pve if $pve && $pve > $required_pve_version;
+       return 1;
+    };
 
-    die "detected old qemu-kvm binary ($kvmver)\n" if $vernum < 15000;
+    if ($kvm && !defined kvm_version()) {
+       die "KVM virtualisation configured, but not available. Either disable in VM configuration"
+           ." or enable in BIOS.\n";
+    }
 
-    my $q35 = machine_type_is_q35($conf);
+    my $q35 = PVE::QemuServer::Machine::machine_type_is_q35($conf);
     my $hotplug_features = parse_hotplug_features(defined($conf->{hotplug}) ? $conf->{hotplug} : '1');
     my $use_old_bios_files = undef;
     ($use_old_bios_files, $machine_type) = qemu_use_old_bios_files($machine_type);
@@ -3576,16 +3091,16 @@ sub config_to_command {
 
     my $use_virtio = 0;
 
-    my $qmpsocket = qmp_socket($vmid);
+    my $qmpsocket = PVE::QemuServer::Helpers::qmp_socket($vmid);
     push @$cmd, '-chardev', "socket,id=qmp,path=$qmpsocket,server,nowait";
     push @$cmd, '-mon', "chardev=qmp,mode=control";
 
-    if (qemu_machine_feature_enabled($machine_type, $kvmver, 2, 12)) {
+    if (min_version($machine_version, 2, 12)) {
        push @$cmd, '-chardev', "socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect=5";
        push @$cmd, '-mon', "chardev=qmp-event,mode=control";
     }
 
-    push @$cmd, '-pidfile' , pidfile_name($vmid);
+    push @$cmd, '-pidfile' , PVE::QemuServer::Helpers::pidfile_name($vmid);
 
     push @$cmd, '-daemonize';
 
@@ -3612,18 +3127,13 @@ sub config_to_command {
        }
     }
 
-    if ($conf->{vmgenid}) {
-       push @$devices, '-device', 'vmgenid,guid='.$conf->{vmgenid};
-    }
-
-    my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch);
     if ($conf->{bios} && $conf->{bios} eq 'ovmf') {
-       die "uefi base image not found\n" if ! -f $ovmf_code;
+       my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch);
+       die "uefi base image '$ovmf_code' not found\n" if ! -f $ovmf_code;
 
-       my $path;
-       my $format;
+       my ($path, $format);
        if (my $efidisk = $conf->{efidisk0}) {
-           my $d = PVE::JSONSchema::parse_property_string($efidisk_fmt, $efidisk);
+           my $d = parse_drive('efidisk0', $efidisk);
            my ($storeid, $volname) = PVE::Storage::parse_volume_id($d->{file}, 1);
            $format = $d->{format};
            if ($storeid) {
@@ -3644,22 +3154,33 @@ sub config_to_command {
            $format = 'raw';
        }
 
+       my $size_str = "";
+
+       if ($format eq 'raw' && $version_guard->(4, 1, 2)) {
+           $size_str = ",size=" . (-s $ovmf_vars);
+       }
+
        push @$cmd, '-drive', "if=pflash,unit=0,format=raw,readonly,file=$ovmf_code";
-       push @$cmd, '-drive', "if=pflash,unit=1,format=$format,id=drive-efidisk0,file=$path";
+       push @$cmd, '-drive', "if=pflash,unit=1,format=$format,id=drive-efidisk0$size_str,file=$path";
     }
 
     # load q35 config
     if ($q35) {
        # we use different pcie-port hardware for qemu >= 4.0 for passthrough
-       if (qemu_machine_feature_enabled($machine_type, $kvmver, 4, 0)) {
+       if (min_version($machine_version, 4, 0)) {
            push @$devices, '-readconfig', '/usr/share/qemu-server/pve-q35-4.0.cfg';
        } else {
            push @$devices, '-readconfig', '/usr/share/qemu-server/pve-q35.cfg';
        }
     }
 
+    if ($conf->{vmgenid}) {
+       push @$devices, '-device', 'vmgenid,guid='.$conf->{vmgenid};
+    }
+
     # add usb controllers
-    my @usbcontrollers = PVE::QemuServer::USB::get_usb_controllers($conf, $bridges, $arch, $machine_type, $usbdesc->{format}, $MAX_USB_DEVICES);
+    my @usbcontrollers = PVE::QemuServer::USB::get_usb_controllers(
+        $conf, $bridges, $arch, $machine_type, $usbdesc->{format}, $MAX_USB_DEVICES);
     push @$devices, @usbcontrollers if @usbcontrollers;
     my $vga = parse_vga($conf->{vga});
 
@@ -3669,7 +3190,7 @@ sub config_to_command {
     if (!$vga->{type}) {
        if ($arch eq 'aarch64') {
            $vga->{type} = 'virtio';
-       } elsif (qemu_machine_feature_enabled($machine_type, $kvmver, 2, 9)) {
+       } elsif (min_version($machine_version, 2, 9)) {
            $vga->{type} = (!$winversion || $winversion >= 6) ? 'std' : 'cirrus';
        } else {
            $vga->{type} = ($winversion >= 6) ? 'std' : 'cirrus';
@@ -3692,83 +3213,16 @@ sub config_to_command {
        push @$devices, '-device', $kbd if defined($kbd);
     }
 
-    my $kvm_off = 0;
-    my $gpu_passthrough;
-
-    # host pci devices
-    for (my $i = 0; $i < $MAX_HOSTPCI_DEVICES; $i++)  {
-       my $id = "hostpci$i";
-       my $d = parse_hostpci($conf->{$id});
-       next if !$d;
-
-       if (my $pcie = $d->{pcie}) {
-           die "q35 machine model is not enabled" if !$q35;
-           # win7 wants to have the pcie devices directly on the pcie bus
-           # instead of in the root port
-           if ($winversion == 7) {
-               $pciaddr = print_pcie_addr("${id}bus0");
-           } else {
-               # add more root ports if needed, 4 are present by default
-               # by pve-q35 cfgs, rest added here on demand.
-               if ($i > 3) {
-                   push @$devices, '-device', print_pcie_root_port($i);
-               }
-               $pciaddr = print_pcie_addr($id);
-           }
-       } else {
-           $pciaddr = print_pci_addr($id, $bridges, $arch, $machine_type);
-       }
-
-       my $xvga = '';
-       if ($d->{'x-vga'}) {
-           $xvga = ',x-vga=on' if !($conf->{bios} && $conf->{bios} eq 'ovmf');
-           $kvm_off = 1;
-           $vga->{type} = 'none' if !defined($conf->{vga});
-           $gpu_passthrough = 1;
-       }
-
-       my $pcidevices = $d->{pciid};
-       my $multifunction = 1 if @$pcidevices > 1;
-
-       my $sysfspath;
-       if ($d->{mdev} && scalar(@$pcidevices) == 1) {
-           my $pci_id = $pcidevices->[0]->{id};
-           my $uuid = PVE::SysFSTools::generate_mdev_uuid($vmid, $i);
-           $sysfspath = "/sys/bus/pci/devices/0000:$pci_id/$uuid";
-       } elsif ($d->{mdev}) {
-           warn "ignoring mediated device '$id' with multifunction device\n";
-       }
-
-       my $j=0;
-       foreach my $pcidevice (@$pcidevices) {
-           my $devicestr = "vfio-pci";
-
-           if ($sysfspath) {
-               $devicestr .= ",sysfsdev=$sysfspath";
-           } else {
-               $devicestr .= ",host=$pcidevice->{id}";
-           }
-
-           my $mf_addr = $multifunction ? ".$j" : '';
-           $devicestr .= ",id=${id}${mf_addr}${pciaddr}${mf_addr}";
-
-           if ($j == 0) {
-               $devicestr .= ',rombar=0' if defined($d->{rombar}) && !$d->{rombar};
-               $devicestr .= "$xvga";
-               $devicestr .= ",multifunction=on" if $multifunction;
-               $devicestr .= ",romfile=/usr/share/kvm/$d->{romfile}" if $d->{romfile};
-           }
-
-           push @$devices, '-device', $devicestr;
-           $j++;
-       }
-    }
+    # host pci device passthrough
+    my ($kvm_off, $gpu_passthrough, $legacy_igd) = PVE::QemuServer::PCI::print_hostpci_devices(
+       $vmid, $conf, $devices, $winversion, $q35, $bridges, $arch, $machine_type);
 
     # usb devices
     my $usb_dev_features = {};
-    $usb_dev_features->{spice_usb3} = 1 if qemu_machine_feature_enabled($machine_type, $kvmver, 4, 0);
+    $usb_dev_features->{spice_usb3} = 1 if min_version($machine_version, 4, 0);
 
-    my @usbdevices = PVE::QemuServer::USB::get_usb_devices($conf, $usbdesc->{format}, $MAX_USB_DEVICES, $usb_dev_features);
+    my @usbdevices = PVE::QemuServer::USB::get_usb_devices(
+        $conf, $usbdesc->{format}, $MAX_USB_DEVICES, $usb_dev_features);
     push @$devices, @usbdevices if @usbdevices;
     # serial devices
     for (my $i = 0; $i < $MAX_SERIAL_PORTS; $i++)  {
@@ -3802,22 +3256,10 @@ sub config_to_command {
        }
     }
 
-    if (my $audio = conf_has_audio($conf)) {
-
+    if (min_version($machine_version, 4, 0) && (my $audio = conf_has_audio($conf))) {
        my $audiopciaddr = print_pci_addr("audio0", $bridges, $arch, $machine_type);
-
-       my $id = $audio->{dev_id};
-       if ($audio->{dev} eq 'AC97') {
-           push @$devices, '-device', "AC97,id=${id}${audiopciaddr}";
-       } elsif ($audio->{dev} =~ /intel\-hda$/) {
-           push @$devices, '-device', "$audio->{dev},id=${id}${audiopciaddr}";
-           push @$devices, '-device', "hda-micro,id=${id}-codec0,bus=${id}.0,cad=0";
-           push @$devices, '-device', "hda-duplex,id=${id}-codec1,bus=${id}.0,cad=1";
-       } else {
-           die "unkown audio device '$audio->{dev}', implement me!";
-       }
-
-       push @$devices, '-audiodev', "$audio->{backend},id=$audio->{backend_id}";
+       my $audio_devs = audio_devs($audio, $audiopciaddr, $machine_version);
+       push @$devices, @$audio_devs;
     }
 
     my $sockets = 1;
@@ -3835,7 +3277,7 @@ sub config_to_command {
     die "MAX $allowed_vcpus vcpus allowed per VM on this node\n"
        if ($allowed_vcpus < $maxcpus);
 
-    if($hotplug_features->{cpu} && qemu_machine_feature_enabled ($machine_type, $kvmver, 2, 7)) {
+    if($hotplug_features->{cpu} && min_version($machine_version, 2, 7)) {
 
        push @$cmd, '-smp', "1,sockets=$sockets,cores=$cores,maxcpus=$maxcpus";
         for (my $i = 2; $i <= $vcpus; $i++)  {
@@ -3865,8 +3307,9 @@ sub config_to_command {
     push @$cmd, '-no-reboot' if  defined($conf->{reboot}) && $conf->{reboot} == 0;
 
     if ($vga->{type} && $vga->{type} !~ m/^serial\d+$/ && $vga->{type} ne 'none'){
-       push @$devices, '-device', print_vga_device($conf, $vga, $arch, $machine_type, undef, $qxlnum, $bridges);
-       my $socket = vnc_socket($vmid);
+       push @$devices, '-device', print_vga_device(
+           $conf, $vga, $arch, $machine_version, $machine_type, undef, $qxlnum, $bridges);
+       my $socket = PVE::QemuServer::Helpers::vnc_socket($vmid);
        push @$cmd,  '-vnc', "unix:$socket,password";
     } else {
        push @$cmd, '-vga', 'none' if $vga->{type} eq 'none';
@@ -3875,7 +3318,6 @@ sub config_to_command {
 
     # time drift fix
     my $tdf = defined($conf->{tdf}) ? $conf->{tdf} : $defaults->{tdf};
-
     my $useLocaltime = $conf->{localtime};
 
     if ($winversion >= 5) { # windows
@@ -3894,21 +3336,17 @@ sub config_to_command {
 
     push @$rtcFlags, 'driftfix=slew' if $tdf;
 
-    if (!$kvm) {
-       push @$machineFlags, 'accel=tcg';
-    }
-
-    if ($machine_type) {
-       push @$machineFlags, "type=${machine_type}";
-    }
-
-    if (($conf->{startdate}) && ($conf->{startdate} ne 'now')) {
+    if ($conf->{startdate} && $conf->{startdate} ne 'now') {
        push @$rtcFlags, "base=$conf->{startdate}";
     } elsif ($useLocaltime) {
        push @$rtcFlags, 'base=localtime';
     }
 
-    push @$cmd, get_cpu_options($conf, $arch, $kvm, $machine_type, $kvm_off, $kvmver, $winversion, $gpu_passthrough);
+    if ($forcecpu) {
+       push @$cmd, '-cpu', $forcecpu;
+    } else {
+       push @$cmd, get_cpu_options($conf, $arch, $kvm, $kvm_off, $machine_version, $winversion, $gpu_passthrough);
+    }
 
     PVE::QemuServer::Memory::config($conf, $vmid, $sockets, $cores, $defaults, $hotplug_features, $cmd);
 
@@ -3916,12 +3354,35 @@ sub config_to_command {
 
     push @$cmd, '-k', $conf->{keyboard} if defined($conf->{keyboard});
 
-    if (parse_guest_agent($conf)->{enabled}) {
-       my $qgasocket = qmp_socket($vmid, 1);
-       my $pciaddr = print_pci_addr("qga0", $bridges, $arch, $machine_type);
+    my $guest_agent = parse_guest_agent($conf);
+
+    if ($guest_agent->{enabled}) {
+       my $qgasocket = PVE::QemuServer::Helpers::qmp_socket($vmid, 1);
        push @$devices, '-chardev', "socket,path=$qgasocket,server,nowait,id=qga0";
-       push @$devices, '-device', "virtio-serial,id=qga0$pciaddr";
-       push @$devices, '-device', 'virtserialport,chardev=qga0,name=org.qemu.guest_agent.0';
+
+       if (!$guest_agent->{type} || $guest_agent->{type} eq 'virtio') {
+           my $pciaddr = print_pci_addr("qga0", $bridges, $arch, $machine_type);
+           push @$devices, '-device', "virtio-serial,id=qga0$pciaddr";
+           push @$devices, '-device', 'virtserialport,chardev=qga0,name=org.qemu.guest_agent.0';
+       } elsif ($guest_agent->{type} eq 'isa') {
+           push @$devices, '-device', "isa-serial,chardev=qga0";
+       }
+    }
+
+    my $rng = parse_rng($conf->{rng0}) if $conf->{rng0};
+    if ($rng && &$version_guard(4, 1, 2)) {
+       check_rng_source($rng->{source});
+
+       my $max_bytes = $rng->{max_bytes} // $rng_fmt->{max_bytes}->{default};
+       my $period = $rng->{period} // $rng_fmt->{period}->{default};
+       my $limiter_str = "";
+       if ($max_bytes) {
+           $limiter_str = ",max-bytes=$max_bytes,period=$period";
+       }
+
+       my $rng_addr = print_pci_addr("rng0", $bridges, $arch, $machine_type);
+       push @$devices, '-object', "rng-random,filename=$rng->{source},id=rng0";
+       push @$devices, '-device', "virtio-rng-pci,rng=rng0$limiter_str$rng_addr";
     }
 
     my $spice_port;
@@ -3929,8 +3390,9 @@ sub config_to_command {
     if ($qxlnum) {
        if ($qxlnum > 1) {
            if ($winversion){
-               for(my $i = 1; $i < $qxlnum; $i++){
-                   push @$devices, '-device', print_vga_device($conf, $vga, $arch, $machine_type, $i, $qxlnum, $bridges);
+               for (my $i = 1; $i < $qxlnum; $i++){
+                   push @$devices, '-device', print_vga_device(
+                       $conf, $vga, $arch, $machine_version, $machine_type, $i, $qxlnum, $bridges);
                }
            } else {
                # assume other OS works like Linux
@@ -3946,7 +3408,6 @@ sub config_to_command {
 
        my $pciaddr = print_pci_addr("spice", $bridges, $arch, $machine_type);
 
-       my $nodename = PVE::INotify::nodename();
        my $pfamily = PVE::Tools::get_host_address_family($nodename);
        my @nodeaddrs = PVE::Tools::getaddrinfo_all('localhost', family => $pfamily);
        die "failed to get an ip address of type $pfamily for 'localhost'\n" if !@nodeaddrs;
@@ -3958,14 +3419,17 @@ sub config_to_command {
        my $localhost = PVE::Network::addr_to_ip($nodeaddrs[0]->{addr});
        $spice_port = PVE::Tools::next_spice_port($pfamily, $localhost);
 
-       my $spice_enhancement = PVE::JSONSchema::parse_property_string($spice_enhancements_fmt, $conf->{spice_enhancements} // '');
+       my $spice_enhancement_str = $conf->{spice_enhancements} // '';
+       my $spice_enhancement = parse_property_string($spice_enhancements_fmt, $spice_enhancement_str);
        if ($spice_enhancement->{foldersharing}) {
            push @$devices, '-chardev', "spiceport,id=foldershare,name=org.spice-space.webdav.0";
            push @$devices, '-device', "virtserialport,chardev=foldershare,name=org.spice-space.webdav.0";
        }
 
        my $spice_opts = "tls-port=${spice_port},addr=$localhost,tls-ciphers=HIGH,seamless-migration=on";
-       $spice_opts .= ",streaming-video=$spice_enhancement->{videostreaming}" if $spice_enhancement->{videostreaming};
+       $spice_opts .= ",streaming-video=$spice_enhancement->{videostreaming}"
+           if $spice_enhancement->{videostreaming};
+
        push @$devices, '-spice', "$spice_opts";
     }
 
@@ -3993,7 +3457,7 @@ sub config_to_command {
        push @$devices, '-iscsi', "initiator-name=$initiator";
     }
 
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
 
        if (PVE::Storage::parse_volume_id($drive->{file}, 1)) {
@@ -4017,14 +3481,17 @@ sub config_to_command {
            }
        }
 
-       if($drive->{interface} eq 'virtio'){
+       if ($drive->{interface} eq 'virtio'){
            push @$cmd, '-object', "iothread,id=iothread-$ds" if $drive->{iothread};
        }
 
-        if ($drive->{interface} eq 'scsi') {
+       if ($drive->{interface} eq 'scsi') {
 
            my ($maxdev, $controller, $controller_prefix) = scsihw_infos($conf, $drive);
 
+           die "scsi$drive->{index}: machine version 4.1~pve2 or higher is required to use more than 14 SCSI disks\n"
+               if $drive->{index} > 13 && !&$version_guard(4, 1, 2);
+
            $pciaddr = print_pci_addr("$controller_prefix$controller", $bridges, $arch, $machine_type);
            my $scsihw_type = $scsihw =~ m/^virtio-scsi-single/ ? "virtio-scsi-pci" : $scsihw;
 
@@ -4041,43 +3508,50 @@ sub config_to_command {
                $queues = ",num_queues=$drive->{queues}";
            }
 
-           push @$devices, '-device', "$scsihw_type,id=$controller_prefix$controller$pciaddr$iothread$queues" if !$scsicontroller->{$controller};
+           push @$devices, '-device', "$scsihw_type,id=$controller_prefix$controller$pciaddr$iothread$queues"
+               if !$scsicontroller->{$controller};
            $scsicontroller->{$controller}=1;
-        }
+       }
 
         if ($drive->{interface} eq 'sata') {
-           my $controller = int($drive->{index} / $MAX_SATA_DISKS);
-           $pciaddr = print_pci_addr("ahci$controller", $bridges, $arch, $machine_type);
-           push @$devices, '-device', "ahci,id=ahci$controller,multifunction=on$pciaddr" if !$ahcicontroller->{$controller};
-           $ahcicontroller->{$controller}=1;
+           my $controller = int($drive->{index} / $PVE::QemuServer::Drive::MAX_SATA_DISKS);
+           $pciaddr = print_pci_addr("ahci$controller", $bridges, $arch, $machine_type);
+           push @$devices, '-device', "ahci,id=ahci$controller,multifunction=on$pciaddr"
+               if !$ahcicontroller->{$controller};
+           $ahcicontroller->{$controller}=1;
         }
 
-       my $drive_cmd = print_drive_full($storecfg, $vmid, $drive);
+       my $drive_cmd = print_drive_commandline_full($storecfg, $vmid, $drive);
+       $drive_cmd .= ',readonly' if PVE::QemuConfig->is_template($conf);
+
        push @$devices, '-drive',$drive_cmd;
-       push @$devices, '-device', print_drivedevice_full($storecfg, $conf, $vmid, $drive, $bridges, $arch, $machine_type);
+       push @$devices, '-device', print_drivedevice_full(
+           $storecfg, $conf, $vmid, $drive, $bridges, $arch, $machine_type);
     });
 
     for (my $i = 0; $i < $MAX_NETS; $i++) {
-         next if !$conf->{"net$i"};
-         my $d = parse_net($conf->{"net$i"});
-         next if !$d;
+       next if !$conf->{"net$i"};
+       my $d = parse_net($conf->{"net$i"});
+       next if !$d;
 
-         $use_virtio = 1 if $d->{model} eq 'virtio';
+       $use_virtio = 1 if $d->{model} eq 'virtio';
 
-         if ($bootindex_hash->{n}) {
-            $d->{bootindex} = $bootindex_hash->{n};
-            $bootindex_hash->{n} += 1;
-         }
+       if ($bootindex_hash->{n}) {
+           $d->{bootindex} = $bootindex_hash->{n};
+           $bootindex_hash->{n} += 1;
+       }
+
+       my $netdevfull = print_netdev_full($vmid, $conf, $arch, $d, "net$i");
+       push @$devices, '-netdev', $netdevfull;
 
-         my $netdevfull = print_netdev_full($vmid, $conf, $arch, $d, "net$i");
-         push @$devices, '-netdev', $netdevfull;
+       my $netdevicefull = print_netdevice_full(
+           $vmid, $conf, $d, "net$i", $bridges, $use_old_bios_files, $arch, $machine_type);
 
-         my $netdevicefull = print_netdevice_full($vmid, $conf, $d, "net$i", $bridges, $use_old_bios_files, $arch, $machine_type);
-         push @$devices, '-device', $netdevicefull;
+       push @$devices, '-device', $netdevicefull;
     }
 
     if ($conf->{ivshmem}) {
-       my $ivshmem = PVE::JSONSchema::parse_property_string($ivshmem_fmt, $conf->{ivshmem});
+       my $ivshmem = parse_property_string($ivshmem_fmt, $conf->{ivshmem});
 
        my $bus;
        if ($q35) {
@@ -4090,36 +3564,63 @@ sub config_to_command {
        my $path = '/dev/shm/pve-shm-' . $ivshmem_name;
 
        push @$devices, '-device', "ivshmem-plain,memdev=ivshmem$bus,";
-       push @$devices, '-object', "memory-backend-file,id=ivshmem,share=on,mem-path=$path,size=$ivshmem->{size}M";
+       push @$devices, '-object', "memory-backend-file,id=ivshmem,share=on,mem-path=$path"
+           .",size=$ivshmem->{size}M";
     }
 
+    # pci.4 is nested in pci.1
+    $bridges->{1} = 1 if $bridges->{4};
+
     if (!$q35) {
        # add pci bridges
-        if (qemu_machine_feature_enabled ($machine_type, $kvmver, 2, 3)) {
+        if (min_version($machine_version, 2, 3)) {
           $bridges->{1} = 1;
           $bridges->{2} = 1;
        }
 
        $bridges->{3} = 1 if $scsihw =~ m/^virtio-scsi-single/;
 
-       for my $k (sort {$b cmp $a} keys %$bridges) {
-           $pciaddr = print_pci_addr("pci.$k", undef, $arch, $machine_type);
-           unshift @$devices, '-device', "pci-bridge,id=pci.$k,chassis_nr=$k$pciaddr" if $k > 0;
+    }
+
+    for my $k (sort {$b cmp $a} keys %$bridges) {
+       next if $q35 && $k < 4; # q35.cfg already includes bridges up to 3
+
+       my $k_name = $k;
+       if ($k == 2 && $legacy_igd) {
+           $k_name = "$k-igd";
+       }
+       $pciaddr = print_pci_addr("pci.$k_name", undef, $arch, $machine_type);
+
+       my $devstr = "pci-bridge,id=pci.$k,chassis_nr=$k$pciaddr";
+       if ($q35) {
+           # add after -readconfig pve-q35.cfg
+           splice @$devices, 2, 0, '-device', $devstr;
+       } else {
+           unshift @$devices, '-device', $devstr if $k > 0;
        }
     }
 
+    if (!$kvm) {
+       push @$machineFlags, 'accel=tcg';
+    }
+
+    my $machine_type_min = $machine_type;
+    if ($add_pve_version) {
+       $machine_type_min =~ s/\+pve\d+$//;
+       $machine_type_min .= "+pve$required_pve_version";
+    }
+    push @$machineFlags, "type=${machine_type_min}";
+
     push @$cmd, @$devices;
-    push @$cmd, '-rtc', join(',', @$rtcFlags)
-       if scalar(@$rtcFlags);
-    push @$cmd, '-machine', join(',', @$machineFlags)
-       if scalar(@$machineFlags);
-    push @$cmd, '-global', join(',', @$globalFlags)
-       if scalar(@$globalFlags);
+    push @$cmd, '-rtc', join(',', @$rtcFlags) if scalar(@$rtcFlags);
+    push @$cmd, '-machine', join(',', @$machineFlags) if scalar(@$machineFlags);
+    push @$cmd, '-global', join(',', @$globalFlags) if scalar(@$globalFlags);
 
     if (my $vmstate = $conf->{vmstate}) {
        my $statepath = PVE::Storage::path($storecfg, $vmstate);
-       push @$vollist, $statepath;
+       push @$vollist, $vmstate;
        push @$cmd, '-loadstate', $statepath;
+       print "activating and using '$vmstate' as vmstate\n";
     }
 
     # add custom args
@@ -4131,35 +3632,35 @@ sub config_to_command {
     return wantarray ? ($cmd, $vollist, $spice_port) : $cmd;
 }
 
-sub vnc_socket {
-    my ($vmid) = @_;
-    return "${var_run_tmpdir}/$vmid.vnc";
+sub check_rng_source {
+    my ($source) = @_;
+
+    # mostly relevant for /dev/hwrng, but doesn't hurt to check others too
+    die "cannot create VirtIO RNG device: source file '$source' doesn't exist\n"
+       if ! -e $source;
+
+    my $rng_current = '/sys/devices/virtual/misc/hw_random/rng_current';
+    if ($source eq '/dev/hwrng' && file_read_firstline($rng_current) eq 'none') {
+       # Needs to abort, otherwise QEMU crashes on first rng access. Note that rng_current cannot
+       # be changed to 'none' manually, so once the VM is past this point, it's no longer an issue.
+       die "Cannot start VM with passed-through RNG device: '/dev/hwrng' exists, but"
+           ." '$rng_current' is set to 'none'. Ensure that a compatible hardware-RNG is attached"
+           ." to the host.\n";
+    }
 }
 
 sub spice_port {
     my ($vmid) = @_;
 
-    my $res = vm_mon_cmd($vmid, 'query-spice');
+    my $res = mon_cmd($vmid, 'query-spice');
 
     return $res->{'tls-port'} || $res->{'port'} || die "no spice port\n";
 }
 
-sub qmp_socket {
-    my ($vmid, $qga, $name) = @_;
-    my $sockettype = $qga ? 'qga' : 'qmp';
-    my $ext = $name ? '-'.$name : '';
-    return "${var_run_tmpdir}/$vmid$ext.$sockettype";
-}
-
-sub pidfile_name {
-    my ($vmid) = @_;
-    return "${var_run_tmpdir}/$vmid.pid";
-}
-
 sub vm_devices_list {
     my ($vmid) = @_;
 
-    my $res = vm_mon_cmd($vmid, 'query-pci');
+    my $res = mon_cmd($vmid, 'query-pci');
     my $devices_to_check = [];
     my $devices = {};
     foreach my $pcibus (@$res) {
@@ -4178,14 +3679,14 @@ sub vm_devices_list {
        $devices_to_check = $to_check;
     }
 
-    my $resblock = vm_mon_cmd($vmid, 'query-block');
+    my $resblock = mon_cmd($vmid, 'query-block');
     foreach my $block (@$resblock) {
        if($block->{device} =~ m/^drive-(\S+)/){
                $devices->{$1} = 1;
        }
     }
 
-    my $resmice = vm_mon_cmd($vmid, 'query-mice');
+    my $resmice = mon_cmd($vmid, 'query-mice');
     foreach my $mice (@$resmice) {
        if ($mice->{name} eq 'QEMU HID Tablet') {
            $devices->{tablet} = 1;
@@ -4196,7 +3697,7 @@ sub vm_devices_list {
     # for usb devices there is no query-usb
     # but we can iterate over the entries in
     # qom-list path=/machine/peripheral
-    my $resperipheral = vm_mon_cmd($vmid, 'qom-list', path => '/machine/peripheral');
+    my $resperipheral = mon_cmd($vmid, 'qom-list', path => '/machine/peripheral');
     foreach my $per (@$resperipheral) {
        if ($per->{name} =~ m/^usb\d+$/) {
            $devices->{$per->{name}} = 1;
@@ -4209,12 +3710,13 @@ sub vm_devices_list {
 sub vm_deviceplug {
     my ($storecfg, $conf, $vmid, $deviceid, $device, $arch, $machine_type) = @_;
 
-    my $q35 = machine_type_is_q35($conf);
+    my $q35 = PVE::QemuServer::Machine::machine_type_is_q35($conf);
 
     my $devices_list = vm_devices_list($vmid);
     return 1 if defined($devices_list->{$deviceid});
 
-    qemu_add_pci_bridge($storecfg, $conf, $vmid, $deviceid, $arch, $machine_type); # add PCI bridge if we need it for the device
+    # add PCI bridge if we need it for the device
+    qemu_add_pci_bridge($storecfg, $conf, $vmid, $deviceid, $arch, $machine_type);
 
     if ($deviceid eq 'tablet') {
 
@@ -4285,11 +3787,12 @@ sub vm_deviceplug {
 
        return undef if !qemu_netdevadd($vmid, $conf, $arch, $device, $deviceid);
 
-       my $machine_type = PVE::QemuServer::qemu_machine_pxe($vmid, $conf);
+       my $machine_type = PVE::QemuServer::Machine::qemu_machine_pxe($vmid, $conf);
        my $use_old_bios_files = undef;
        ($use_old_bios_files, $machine_type) = qemu_use_old_bios_files($machine_type);
 
-       my $netdevicefull = print_netdevice_full($vmid, $conf, $device, $deviceid, undef, $use_old_bios_files, $arch, $machine_type);
+       my $netdevicefull = print_netdevice_full(
+           $vmid, $conf, $device, $deviceid, undef, $use_old_bios_files, $arch, $machine_type);
        qemu_deviceadd($vmid, $netdevicefull);
        eval {
            qemu_deviceaddverify($vmid, $deviceid);
@@ -4377,13 +3880,13 @@ sub qemu_deviceadd {
     $devicefull = "driver=".$devicefull;
     my %options =  split(/[=,]/, $devicefull);
 
-    vm_mon_cmd($vmid, "device_add" , %options);
+    mon_cmd($vmid, "device_add" , %options);
 }
 
 sub qemu_devicedel {
     my ($vmid, $deviceid) = @_;
 
-    my $ret = vm_mon_cmd($vmid, "device_del", id => $deviceid);
+    my $ret = mon_cmd($vmid, "device_del", id => $deviceid);
 }
 
 sub qemu_iothread_add {
@@ -4412,7 +3915,7 @@ sub qemu_iothread_del {
 sub qemu_objectadd {
     my($vmid, $objectid, $qomtype) = @_;
 
-    vm_mon_cmd($vmid, "object-add", id => $objectid, "qom-type" => $qomtype);
+    mon_cmd($vmid, "object-add", id => $objectid, "qom-type" => $qomtype);
 
     return 1;
 }
@@ -4420,7 +3923,7 @@ sub qemu_objectadd {
 sub qemu_objectdel {
     my($vmid, $objectid) = @_;
 
-    vm_mon_cmd($vmid, "object-del", id => $objectid);
+    mon_cmd($vmid, "object-del", id => $objectid);
 
     return 1;
 }
@@ -4428,9 +3931,9 @@ sub qemu_objectdel {
 sub qemu_driveadd {
     my ($storecfg, $vmid, $device) = @_;
 
-    my $drive = print_drive_full($storecfg, $vmid, $device);
+    my $drive = print_drive_commandline_full($storecfg, $vmid, $device);
     $drive =~ s/\\/\\\\/g;
-    my $ret = vm_human_monitor_command($vmid, "drive_add auto \"$drive\"");
+    my $ret = PVE::QemuServer::Monitor::hmp_cmd($vmid, "drive_add auto \"$drive\"");
 
     # If the command succeeds qemu prints: "OK"
     return 1 if $ret =~ m/OK/s;
@@ -4441,7 +3944,7 @@ sub qemu_driveadd {
 sub qemu_drivedel {
     my($vmid, $deviceid) = @_;
 
-    my $ret = vm_human_monitor_command($vmid, "drive_del drive-$deviceid");
+    my $ret = PVE::QemuServer::Monitor::hmp_cmd($vmid, "drive_del drive-$deviceid");
     $ret =~ s/^\s+//;
 
     return 1 if $ret eq "";
@@ -4509,8 +4012,8 @@ sub qemu_deletescsihw {
 
     my $devices_list = vm_devices_list($vmid);
     foreach my $opt (keys %{$devices_list}) {
-       if (PVE::QemuServer::is_valid_drivename($opt)) {
-           my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
+       if (is_valid_drivename($opt)) {
+           my $drive = parse_drive($opt, $conf->{$opt});
            if($drive->{interface} eq 'scsi' && $drive->{index} < (($maxdev-1)*($controller+1))) {
                return 1;
            }
@@ -4551,7 +4054,7 @@ sub qemu_add_pci_bridge {
 sub qemu_set_link_status {
     my ($vmid, $device, $up) = @_;
 
-    vm_mon_cmd($vmid, "set_link", name => $device,
+    mon_cmd($vmid, "set_link", name => $device,
               up => $up ? JSON::true : JSON::false);
 }
 
@@ -4561,14 +4064,22 @@ sub qemu_netdevadd {
     my $netdev = print_netdev_full($vmid, $conf, $arch, $device, $deviceid, 1);
     my %options =  split(/[=,]/, $netdev);
 
-    vm_mon_cmd($vmid, "netdev_add",  %options);
+    if (defined(my $vhost = $options{vhost})) {
+       $options{vhost} = JSON::boolean(PVE::JSONSchema::parse_boolean($vhost));
+    }
+
+    if (defined(my $queues = $options{queues})) {
+       $options{queues} = $queues + 0;
+    }
+
+    mon_cmd($vmid, "netdev_add",  %options);
     return 1;
 }
 
 sub qemu_netdevdel {
     my ($vmid, $deviceid) = @_;
 
-    vm_mon_cmd($vmid, "netdev_del", id => $deviceid);
+    mon_cmd($vmid, "netdev_del", id => $deviceid);
 }
 
 sub qemu_usb_hotplug {
@@ -4599,7 +4110,7 @@ sub qemu_usb_hotplug {
 sub qemu_cpu_hotplug {
     my ($vmid, $conf, $vcpus) = @_;
 
-    my $machine_type = PVE::QemuServer::get_current_qemu_machine($vmid);
+    my $machine_type = PVE::QemuServer::Machine::get_current_qemu_machine($vmid);
 
     my $sockets = 1;
     $sockets = $conf->{smp} if $conf->{smp}; # old style - no longer iused
@@ -4616,14 +4127,14 @@ sub qemu_cpu_hotplug {
 
     if ($vcpus < $currentvcpus) {
 
-       if (qemu_machine_feature_enabled ($machine_type, undef, 2, 7)) {
+       if (PVE::QemuServer::Machine::machine_version($machine_type, 2, 7)) {
 
            for (my $i = $currentvcpus; $i > $vcpus; $i--) {
                qemu_devicedel($vmid, "cpu$i");
                my $retry = 0;
                my $currentrunningvcpus = undef;
                while (1) {
-                   $currentrunningvcpus = vm_mon_cmd($vmid, "query-cpus");
+                   $currentrunningvcpus = mon_cmd($vmid, "query-cpus-fast");
                    last if scalar(@{$currentrunningvcpus}) == $i-1;
                    raise_param_exc({ vcpus => "error unplugging cpu$i" }) if $retry > 5;
                    $retry++;
@@ -4640,11 +4151,11 @@ sub qemu_cpu_hotplug {
        return;
     }
 
-    my $currentrunningvcpus = vm_mon_cmd($vmid, "query-cpus");
+    my $currentrunningvcpus = mon_cmd($vmid, "query-cpus-fast");
     die "vcpus in running vm does not match its configuration\n"
        if scalar(@{$currentrunningvcpus}) != $currentvcpus;
 
-    if (qemu_machine_feature_enabled ($machine_type, undef, 2, 7)) {
+    if (PVE::QemuServer::Machine::machine_version($machine_type, 2, 7)) {
 
        for (my $i = $currentvcpus+1; $i <= $vcpus; $i++) {
            my $cpustr = print_cpu_device($conf, $i);
@@ -4653,7 +4164,7 @@ sub qemu_cpu_hotplug {
            my $retry = 0;
            my $currentrunningvcpus = undef;
            while (1) {
-               $currentrunningvcpus = vm_mon_cmd($vmid, "query-cpus");
+               $currentrunningvcpus = mon_cmd($vmid, "query-cpus-fast");
                last if scalar(@{$currentrunningvcpus}) == $i;
                raise_param_exc({ vcpus => "error hotplugging cpu$i" }) if $retry > 10;
                sleep 1;
@@ -4666,7 +4177,7 @@ sub qemu_cpu_hotplug {
     } else {
 
        for (my $i = $currentvcpus; $i < $vcpus; $i++) {
-           vm_mon_cmd($vmid, "cpu-add", id => int($i));
+           mon_cmd($vmid, "cpu-add", id => int($i));
        }
     }
 }
@@ -4680,7 +4191,7 @@ sub qemu_block_set_io_throttle {
 
     return if !check_running($vmid) ;
 
-    vm_mon_cmd($vmid, "block_set_io_throttle", device => $deviceid,
+    mon_cmd($vmid, "block_set_io_throttle", device => $deviceid,
        bps => int($bps),
        bps_rd => int($bps_rd),
        bps_wr => int($bps_wr),
@@ -4745,7 +4256,10 @@ sub qemu_block_resize {
 
     return if !$running;
 
-    vm_mon_cmd($vmid, "block_resize", device => $deviceid, size => int($size));
+    my $padding = (1024 - $size % 1024) % 1024;
+    $size = $size + $padding;
+
+    mon_cmd($vmid, "block_resize", device => $deviceid, size => int($size));
 
 }
 
@@ -4755,7 +4269,7 @@ sub qemu_volume_snapshot {
     my $running = check_running($vmid);
 
     if ($running && do_snapshots_with_qemu($storecfg, $volid)){
-       vm_mon_cmd($vmid, 'blockdev-snapshot-internal-sync', device => $deviceid, name => $snap);
+       mon_cmd($vmid, 'blockdev-snapshot-internal-sync', device => $deviceid, name => $snap);
     } else {
        PVE::Storage::volume_snapshot($storecfg, $volid, $snap);
     }
@@ -4770,14 +4284,14 @@ sub qemu_volume_snapshot_delete {
 
        $running = undef;
        my $conf = PVE::QemuConfig->load_config($vmid);
-       foreach_drive($conf, sub {
+       PVE::QemuConfig->foreach_volume($conf, sub {
            my ($ds, $drive) = @_;
            $running = 1 if $drive->{file} eq $volid;
        });
     }
 
     if ($running && do_snapshots_with_qemu($storecfg, $volid)){
-       vm_mon_cmd($vmid, 'blockdev-snapshot-delete-internal-sync', device => $deviceid, name => $snap);
+       mon_cmd($vmid, 'blockdev-snapshot-delete-internal-sync', device => $deviceid, name => $snap);
     } else {
        PVE::Storage::volume_snapshot_delete($storecfg, $volid, $snap, $running);
     }
@@ -4796,7 +4310,7 @@ sub set_migration_caps {
        "compress" => 0
     };
 
-    my $supported_capabilities = vm_mon_cmd_nocheck($vmid, "query-migrate-capabilities");
+    my $supported_capabilities = mon_cmd($vmid, "query-migrate-capabilities");
 
     for my $supported_capability (@$supported_capabilities) {
        push @$cap_ref, {
@@ -4805,7 +4319,60 @@ sub set_migration_caps {
        };
     }
 
-    vm_mon_cmd_nocheck($vmid, "migrate-set-capabilities", capabilities => $cap_ref);
+    mon_cmd($vmid, "migrate-set-capabilities", capabilities => $cap_ref);
+}
+
+sub foreach_volid {
+    my ($conf, $func, @param) = @_;
+
+    my $volhash = {};
+
+    my $test_volid = sub {
+       my ($key, $drive, $snapname) = @_;
+
+       my $volid = $drive->{file};
+       return if !$volid;
+
+       $volhash->{$volid}->{cdrom} //= 1;
+       $volhash->{$volid}->{cdrom} = 0 if !drive_is_cdrom($drive);
+
+       my $replicate = $drive->{replicate} // 1;
+       $volhash->{$volid}->{replicate} //= 0;
+       $volhash->{$volid}->{replicate} = 1 if $replicate;
+
+       $volhash->{$volid}->{shared} //= 0;
+       $volhash->{$volid}->{shared} = 1 if $drive->{shared};
+
+       $volhash->{$volid}->{referenced_in_config} //= 0;
+       $volhash->{$volid}->{referenced_in_config} = 1 if !defined($snapname);
+
+       $volhash->{$volid}->{referenced_in_snapshot}->{$snapname} = 1
+           if defined($snapname);
+
+       my $size = $drive->{size};
+       $volhash->{$volid}->{size} //= $size if $size;
+
+       $volhash->{$volid}->{is_vmstate} //= 0;
+       $volhash->{$volid}->{is_vmstate} = 1 if $key eq 'vmstate';
+
+       $volhash->{$volid}->{is_unused} //= 0;
+       $volhash->{$volid}->{is_unused} = 1 if $key =~ /^unused\d+$/;
+    };
+
+    my $include_opts = {
+       extra_keys => ['vmstate'],
+       include_unused => 1,
+    };
+
+    PVE::QemuConfig->foreach_volume_full($conf, $include_opts, $test_volid);
+    foreach my $snapname (keys %{$conf->{snapshots}}) {
+       my $snap = $conf->{snapshots}->{$snapname};
+       PVE::QemuConfig->foreach_volume_full($snap, $include_opts, $test_volid, $snapname);
+    }
+
+    foreach my $volid (keys %$volhash) {
+       &$func($volid, $volhash->{$volid}, @param);
+    }
 }
 
 my $fast_plug_option = {
@@ -4818,6 +4385,7 @@ my $fast_plug_option = {
     'protection' => 1,
     'vmstatestorage' => 1,
     'hookscript' => 1,
+    'tags' => 1,
 };
 
 # hotplug changes in [PENDING]
@@ -4828,7 +4396,8 @@ sub vmconfig_hotplug_pending {
     my ($vmid, $conf, $storecfg, $selection, $errors) = @_;
 
     my $defaults = load_defaults();
-    my ($arch, $machine_type) = get_basic_machine_info($conf, undef);
+    my $arch = get_vm_arch($conf);
+    my $machine_type = get_vm_machine($conf, undef, $arch);
 
     # commit values which do not have any impact on running VM first
     # Note: those option cannot raise errors, we we do not care about
@@ -4850,14 +4419,14 @@ sub vmconfig_hotplug_pending {
 
     if ($changes) {
        PVE::QemuConfig->write_config($vmid, $conf);
-       $conf = PVE::QemuConfig->load_config($vmid); # update/reload
     }
 
     my $hotplug_features = parse_hotplug_features(defined($conf->{hotplug}) ? $conf->{hotplug} : '1');
 
     my $pending_delete_hash = PVE::QemuConfig->parse_pending_delete($conf->{pending}->{delete});
-    while (my ($opt, $force) = each %$pending_delete_hash) {
+    foreach my $opt (sort keys %$pending_delete_hash) {
        next if $selection && !$selection->{$opt};
+       my $force = $pending_delete_hash->{$opt}->{force};
        eval {
            if ($opt eq 'hotplug') {
                die "skip\n" if ($conf->{hotplug} =~ /memory/);
@@ -4885,7 +4454,7 @@ sub vmconfig_hotplug_pending {
                die "skip\n" if defined($conf->{balloon}) && $conf->{balloon} == 0;
                # here we reset the ballooning value to memory
                my $balloon = $conf->{memory} || $defaults->{memory};
-               vm_mon_cmd($vmid, "balloon", value => $balloon*1024*1024);
+               mon_cmd($vmid, "balloon", value => $balloon*1024*1024);
            } elsif ($fast_plug_option->{$opt}) {
                # do nothing
            } elsif ($opt =~ m/^net(\d+)$/) {
@@ -4909,18 +4478,17 @@ sub vmconfig_hotplug_pending {
        if (my $err = $@) {
            &$add_error($opt, $err) if $err ne "skip\n";
        } else {
-           # save new config if hotplug was successful
            delete $conf->{$opt};
            PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
-           PVE::QemuConfig->write_config($vmid, $conf);
-           $conf = PVE::QemuConfig->load_config($vmid); # update/reload
        }
     }
 
-    my $apply_pending_cloudinit;
+    my ($apply_pending_cloudinit, $apply_pending_cloudinit_done);
     $apply_pending_cloudinit = sub {
+       return if $apply_pending_cloudinit_done; # once is enough
+       $apply_pending_cloudinit_done = 1; # once is enough
+
        my ($key, $value) = @_;
-       $apply_pending_cloudinit = sub {}; # once is enough
 
        my @cloudinit_opts = keys %$confdesc_cloudinit;
        foreach my $opt (keys %{$conf->{pending}}) {
@@ -4954,7 +4522,7 @@ sub vmconfig_hotplug_pending {
                # since we cannot reliably hot unplug usb devices
                # we are disabling it
                die "skip\n" if !$hotplug_features->{usb} || $value =~ m/spice/i;
-               my $d = eval { PVE::JSONSchema::parse_property_string($usbdesc->{format}, $value) };
+               my $d = eval { parse_property_string($usbdesc->{format}, $value) };
                die "skip\n" if !$d;
                qemu_usb_hotplug($storecfg, $conf, $vmid, $opt, $d, $arch, $machine_type);
            } elsif ($opt eq 'vcpus') {
@@ -4969,20 +4537,21 @@ sub vmconfig_hotplug_pending {
                # allow manual ballooning if shares is set to zero
                if ((defined($conf->{shares}) && ($conf->{shares} == 0))) {
                    my $balloon = $conf->{pending}->{balloon} || $conf->{memory} || $defaults->{memory};
-                   vm_mon_cmd($vmid, "balloon", value => $balloon*1024*1024);
+                   mon_cmd($vmid, "balloon", value => $balloon*1024*1024);
                }
            } elsif ($opt =~ m/^net(\d+)$/) {
                # some changes can be done without hotplug
                vmconfig_update_net($storecfg, $conf, $hotplug_features->{network},
                                    $vmid, $opt, $value, $arch, $machine_type);
            } elsif (is_valid_drivename($opt)) {
+               die "skip\n" if $opt eq 'efidisk0';
                # some changes can be done without hotplug
                my $drive = parse_drive($opt, $value);
                if (drive_is_cloudinit($drive)) {
                    &$apply_pending_cloudinit($opt, $value);
                }
                vmconfig_update_disk($storecfg, $conf, $hotplug_features->{disk},
-                                    $vmid, $opt, $value, 1, $arch, $machine_type);
+                                    $vmid, $opt, $value, $arch, $machine_type);
            } elsif ($opt =~ m/^memory$/) { #dimms
                die "skip\n" if !$hotplug_features->{memory};
                $value = PVE::QemuServer::Memory::qemu_memory_hotplug($vmid, $conf, $defaults, $opt, $value);
@@ -4998,13 +4567,12 @@ sub vmconfig_hotplug_pending {
        if (my $err = $@) {
            &$add_error($opt, $err) if $err ne "skip\n";
        } else {
-           # save new config if hotplug was successful
            $conf->{$opt} = $value;
            delete $conf->{pending}->{$opt};
-           PVE::QemuConfig->write_config($vmid, $conf);
-           $conf = PVE::QemuConfig->load_config($vmid); # update/reload
        }
     }
+
+    PVE::QemuConfig->write_config($vmid, $conf);
 }
 
 sub try_deallocate_drive {
@@ -5018,7 +4586,7 @@ sub try_deallocate_drive {
 
            # check if the disk is really unused
            die "unable to delete '$volid' - volume is still in use (snapshot?)\n"
-               if is_volume_in_use($storecfg, $conf, $key, $volid);
+               if PVE::QemuServer::Drive::is_volume_in_use($storecfg, $conf, $key, $volid);
            PVE::Storage::vdisk_free($storecfg, $volid);
            return 1;
        } else {
@@ -5049,68 +4617,54 @@ sub vmconfig_delete_or_detach_drive {
 
 
 sub vmconfig_apply_pending {
-    my ($vmid, $conf, $storecfg) = @_;
+    my ($vmid, $conf, $storecfg, $errors) = @_;
+
+    my $add_apply_error = sub {
+       my ($opt, $msg) = @_;
+       my $err_msg = "unable to apply pending change $opt : $msg";
+       $errors->{$opt} = $err_msg;
+       warn $err_msg;
+    };
 
     # cold plug
 
     my $pending_delete_hash = PVE::QemuConfig->parse_pending_delete($conf->{pending}->{delete});
-    while (my ($opt, $force) = each %$pending_delete_hash) {
-       die "internal error" if $opt =~ m/^unused/;
-       $conf = PVE::QemuConfig->load_config($vmid); # update/reload
-       if (!defined($conf->{$opt})) {
-           PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
-           PVE::QemuConfig->write_config($vmid, $conf);
-       } elsif (is_valid_drivename($opt)) {
-           vmconfig_delete_or_detach_drive($vmid, $storecfg, $conf, $opt, $force);
-           PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
-           delete $conf->{$opt};
-           PVE::QemuConfig->write_config($vmid, $conf);
+    foreach my $opt (sort keys %$pending_delete_hash) {
+       my $force = $pending_delete_hash->{$opt}->{force};
+       eval {
+           if ($opt =~ m/^unused/) {
+               die "internal error";
+           } elsif (defined($conf->{$opt}) && is_valid_drivename($opt)) {
+               vmconfig_delete_or_detach_drive($vmid, $storecfg, $conf, $opt, $force);
+           }
+       };
+       if (my $err = $@) {
+           $add_apply_error->($opt, $err);
        } else {
            PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
            delete $conf->{$opt};
-           PVE::QemuConfig->write_config($vmid, $conf);
        }
     }
 
-    $conf = PVE::QemuConfig->load_config($vmid); # update/reload
+    PVE::QemuConfig->cleanup_pending($conf);
 
     foreach my $opt (keys %{$conf->{pending}}) { # add/change
-       $conf = PVE::QemuConfig->load_config($vmid); # update/reload
-
-       if (defined($conf->{$opt}) && ($conf->{$opt} eq $conf->{pending}->{$opt})) {
-           # skip if nothing changed
-       } elsif (is_valid_drivename($opt)) {
-           vmconfig_register_unused_drive($storecfg, $vmid, $conf, parse_drive($opt, $conf->{$opt}))
-               if defined($conf->{$opt});
-           $conf->{$opt} = $conf->{pending}->{$opt};
+       next if $opt eq 'delete'; # just to be sure
+       eval {
+           if (defined($conf->{$opt}) && is_valid_drivename($opt)) {
+               vmconfig_register_unused_drive($storecfg, $vmid, $conf, parse_drive($opt, $conf->{$opt}))
+           }
+       };
+       if (my $err = $@) {
+           $add_apply_error->($opt, $err);
        } else {
-           $conf->{$opt} = $conf->{pending}->{$opt};
+           $conf->{$opt} = delete $conf->{pending}->{$opt};
        }
-
-       delete $conf->{pending}->{$opt};
-       PVE::QemuConfig->write_config($vmid, $conf);
     }
-}
-
-my $safe_num_ne = sub {
-    my ($a, $b) = @_;
-
-    return 0 if !defined($a) && !defined($b);
-    return 1 if !defined($a);
-    return 1 if !defined($b);
-
-    return $a != $b;
-};
-
-my $safe_string_ne = sub {
-    my ($a, $b) = @_;
-
-    return 0 if !defined($a) && !defined($b);
-    return 1 if !defined($a);
-    return 1 if !defined($b);
 
-    return $a ne $b;
-};
+    # write all changes at once to avoid unnecessary i/o
+    PVE::QemuConfig->write_config($vmid, $conf);
+}
 
 sub vmconfig_update_net {
     my ($storecfg, $conf, $hotplug, $vmid, $opt, $value, $arch, $machine_type) = @_;
@@ -5120,9 +4674,9 @@ sub vmconfig_update_net {
     if ($conf->{$opt}) {
        my $oldnet = parse_net($conf->{$opt});
 
-       if (&$safe_string_ne($oldnet->{model}, $newnet->{model}) ||
-           &$safe_string_ne($oldnet->{macaddr}, $newnet->{macaddr}) ||
-           &$safe_num_ne($oldnet->{queues}, $newnet->{queues}) ||
+       if (safe_string_ne($oldnet->{model}, $newnet->{model}) ||
+           safe_string_ne($oldnet->{macaddr}, $newnet->{macaddr}) ||
+           safe_num_ne($oldnet->{queues}, $newnet->{queues}) ||
            !($newnet->{bridge} && $oldnet->{bridge})) { # bridge/nat mode change
 
             # for non online change, we try to hot-unplug
@@ -5133,19 +4687,24 @@ sub vmconfig_update_net {
            die "internal error" if $opt !~ m/net(\d+)/;
            my $iface = "tap${vmid}i$1";
 
-           if (&$safe_string_ne($oldnet->{bridge}, $newnet->{bridge}) ||
-               &$safe_num_ne($oldnet->{tag}, $newnet->{tag}) ||
-               &$safe_string_ne($oldnet->{trunks}, $newnet->{trunks}) ||
-               &$safe_num_ne($oldnet->{firewall}, $newnet->{firewall})) {
+           if (safe_string_ne($oldnet->{bridge}, $newnet->{bridge}) ||
+               safe_num_ne($oldnet->{tag}, $newnet->{tag}) ||
+               safe_string_ne($oldnet->{trunks}, $newnet->{trunks}) ||
+               safe_num_ne($oldnet->{firewall}, $newnet->{firewall})) {
                PVE::Network::tap_unplug($iface);
-               PVE::Network::tap_plug($iface, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}, $newnet->{trunks}, $newnet->{rate});
-           } elsif (&$safe_num_ne($oldnet->{rate}, $newnet->{rate})) {
+
+               if ($have_sdn) {
+                   PVE::Network::SDN::Zones::tap_plug($iface, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}, $newnet->{trunks}, $newnet->{rate});
+               } else {
+                   PVE::Network::tap_plug($iface, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}, $newnet->{trunks}, $newnet->{rate});
+               }
+           } elsif (safe_num_ne($oldnet->{rate}, $newnet->{rate})) {
                # Rate can be applied on its own but any change above needs to
                # include the rate in tap_plug since OVS resets everything.
                PVE::Network::tap_rate_limit($iface, $newnet->{rate});
            }
 
-           if (&$safe_string_ne($oldnet->{link_down}, $newnet->{link_down})) {
+           if (safe_string_ne($oldnet->{link_down}, $newnet->{link_down})) {
                qemu_set_link_status($vmid, $opt, !$newnet->{link_down});
            }
 
@@ -5161,101 +4720,104 @@ sub vmconfig_update_net {
 }
 
 sub vmconfig_update_disk {
-    my ($storecfg, $conf, $hotplug, $vmid, $opt, $value, $force, $arch, $machine_type) = @_;
-
-    # fixme: do we need force?
+    my ($storecfg, $conf, $hotplug, $vmid, $opt, $value, $arch, $machine_type) = @_;
 
     my $drive = parse_drive($opt, $value);
 
-    if ($conf->{$opt}) {
+    if ($conf->{$opt} && (my $old_drive = parse_drive($opt, $conf->{$opt}))) {
+       my $media = $drive->{media} || 'disk';
+       my $oldmedia = $old_drive->{media} || 'disk';
+       die "unable to change media type\n" if $media ne $oldmedia;
 
-       if (my $old_drive = parse_drive($opt, $conf->{$opt}))  {
+       if (!drive_is_cdrom($old_drive)) {
 
-           my $media = $drive->{media} || 'disk';
-           my $oldmedia = $old_drive->{media} || 'disk';
-           die "unable to change media type\n" if $media ne $oldmedia;
+           if ($drive->{file} ne $old_drive->{file}) {
 
-           if (!drive_is_cdrom($old_drive)) {
+               die "skip\n" if !$hotplug;
 
-               if ($drive->{file} ne $old_drive->{file}) {
+               # unplug and register as unused
+               vm_deviceunplug($vmid, $conf, $opt);
+               vmconfig_register_unused_drive($storecfg, $vmid, $conf, $old_drive)
 
-                   die "skip\n" if !$hotplug;
+           } else {
+               # update existing disk
+
+               # skip non hotpluggable value
+               if (safe_string_ne($drive->{discard}, $old_drive->{discard}) ||
+                   safe_string_ne($drive->{iothread}, $old_drive->{iothread}) ||
+                   safe_string_ne($drive->{queues}, $old_drive->{queues}) ||
+                   safe_string_ne($drive->{cache}, $old_drive->{cache}) ||
+                   safe_string_ne($drive->{ssd}, $old_drive->{ssd})) {
+                   die "skip\n";
+               }
 
-                   # unplug and register as unused
-                   vm_deviceunplug($vmid, $conf, $opt);
-                   vmconfig_register_unused_drive($storecfg, $vmid, $conf, $old_drive)
+               # apply throttle
+               if (safe_num_ne($drive->{mbps}, $old_drive->{mbps}) ||
+                   safe_num_ne($drive->{mbps_rd}, $old_drive->{mbps_rd}) ||
+                   safe_num_ne($drive->{mbps_wr}, $old_drive->{mbps_wr}) ||
+                   safe_num_ne($drive->{iops}, $old_drive->{iops}) ||
+                   safe_num_ne($drive->{iops_rd}, $old_drive->{iops_rd}) ||
+                   safe_num_ne($drive->{iops_wr}, $old_drive->{iops_wr}) ||
+                   safe_num_ne($drive->{mbps_max}, $old_drive->{mbps_max}) ||
+                   safe_num_ne($drive->{mbps_rd_max}, $old_drive->{mbps_rd_max}) ||
+                   safe_num_ne($drive->{mbps_wr_max}, $old_drive->{mbps_wr_max}) ||
+                   safe_num_ne($drive->{iops_max}, $old_drive->{iops_max}) ||
+                   safe_num_ne($drive->{iops_rd_max}, $old_drive->{iops_rd_max}) ||
+                   safe_num_ne($drive->{iops_wr_max}, $old_drive->{iops_wr_max}) ||
+                   safe_num_ne($drive->{bps_max_length}, $old_drive->{bps_max_length}) ||
+                   safe_num_ne($drive->{bps_rd_max_length}, $old_drive->{bps_rd_max_length}) ||
+                   safe_num_ne($drive->{bps_wr_max_length}, $old_drive->{bps_wr_max_length}) ||
+                   safe_num_ne($drive->{iops_max_length}, $old_drive->{iops_max_length}) ||
+                   safe_num_ne($drive->{iops_rd_max_length}, $old_drive->{iops_rd_max_length}) ||
+                   safe_num_ne($drive->{iops_wr_max_length}, $old_drive->{iops_wr_max_length})) {
+
+                   qemu_block_set_io_throttle(
+                       $vmid,"drive-$opt",
+                       ($drive->{mbps} || 0)*1024*1024,
+                       ($drive->{mbps_rd} || 0)*1024*1024,
+                       ($drive->{mbps_wr} || 0)*1024*1024,
+                        $drive->{iops} || 0,
+                        $drive->{iops_rd} || 0,
+                        $drive->{iops_wr} || 0,
+                       ($drive->{mbps_max} || 0)*1024*1024,
+                       ($drive->{mbps_rd_max} || 0)*1024*1024,
+                       ($drive->{mbps_wr_max} || 0)*1024*1024,
+                        $drive->{iops_max} || 0,
+                        $drive->{iops_rd_max} || 0,
+                        $drive->{iops_wr_max} || 0,
+                        $drive->{bps_max_length} || 1,
+                        $drive->{bps_rd_max_length} || 1,
+                        $drive->{bps_wr_max_length} || 1,
+                        $drive->{iops_max_length} || 1,
+                        $drive->{iops_rd_max_length} || 1,
+                        $drive->{iops_wr_max_length} || 1,
+                   );
 
-               } else {
-                   # update existing disk
-
-                   # skip non hotpluggable value
-                   if (&$safe_string_ne($drive->{discard}, $old_drive->{discard}) ||
-                       &$safe_string_ne($drive->{iothread}, $old_drive->{iothread}) ||
-                       &$safe_string_ne($drive->{queues}, $old_drive->{queues}) ||
-                       &$safe_string_ne($drive->{cache}, $old_drive->{cache})) {
-                       die "skip\n";
-                   }
+               }
 
-                   # apply throttle
-                   if (&$safe_num_ne($drive->{mbps}, $old_drive->{mbps}) ||
-                       &$safe_num_ne($drive->{mbps_rd}, $old_drive->{mbps_rd}) ||
-                       &$safe_num_ne($drive->{mbps_wr}, $old_drive->{mbps_wr}) ||
-                       &$safe_num_ne($drive->{iops}, $old_drive->{iops}) ||
-                       &$safe_num_ne($drive->{iops_rd}, $old_drive->{iops_rd}) ||
-                       &$safe_num_ne($drive->{iops_wr}, $old_drive->{iops_wr}) ||
-                       &$safe_num_ne($drive->{mbps_max}, $old_drive->{mbps_max}) ||
-                       &$safe_num_ne($drive->{mbps_rd_max}, $old_drive->{mbps_rd_max}) ||
-                       &$safe_num_ne($drive->{mbps_wr_max}, $old_drive->{mbps_wr_max}) ||
-                       &$safe_num_ne($drive->{iops_max}, $old_drive->{iops_max}) ||
-                       &$safe_num_ne($drive->{iops_rd_max}, $old_drive->{iops_rd_max}) ||
-                       &$safe_num_ne($drive->{iops_wr_max}, $old_drive->{iops_wr_max}) ||
-                       &$safe_num_ne($drive->{bps_max_length}, $old_drive->{bps_max_length}) ||
-                       &$safe_num_ne($drive->{bps_rd_max_length}, $old_drive->{bps_rd_max_length}) ||
-                       &$safe_num_ne($drive->{bps_wr_max_length}, $old_drive->{bps_wr_max_length}) ||
-                       &$safe_num_ne($drive->{iops_max_length}, $old_drive->{iops_max_length}) ||
-                       &$safe_num_ne($drive->{iops_rd_max_length}, $old_drive->{iops_rd_max_length}) ||
-                       &$safe_num_ne($drive->{iops_wr_max_length}, $old_drive->{iops_wr_max_length})) {
-
-                       qemu_block_set_io_throttle($vmid,"drive-$opt",
-                                                  ($drive->{mbps} || 0)*1024*1024,
-                                                  ($drive->{mbps_rd} || 0)*1024*1024,
-                                                  ($drive->{mbps_wr} || 0)*1024*1024,
-                                                  $drive->{iops} || 0,
-                                                  $drive->{iops_rd} || 0,
-                                                  $drive->{iops_wr} || 0,
-                                                  ($drive->{mbps_max} || 0)*1024*1024,
-                                                  ($drive->{mbps_rd_max} || 0)*1024*1024,
-                                                  ($drive->{mbps_wr_max} || 0)*1024*1024,
-                                                  $drive->{iops_max} || 0,
-                                                  $drive->{iops_rd_max} || 0,
-                                                  $drive->{iops_wr_max} || 0,
-                                                  $drive->{bps_max_length} || 1,
-                                                  $drive->{bps_rd_max_length} || 1,
-                                                  $drive->{bps_wr_max_length} || 1,
-                                                  $drive->{iops_max_length} || 1,
-                                                  $drive->{iops_rd_max_length} || 1,
-                                                  $drive->{iops_wr_max_length} || 1);
+               return 1;
+           }
 
-                   }
+       } else { # cdrom
 
-                   return 1;
-               }
+           if ($drive->{file} eq 'none') {
+               mon_cmd($vmid, "eject", force => JSON::true, id => "$opt");
+               if (drive_is_cloudinit($old_drive)) {
+                   vmconfig_register_unused_drive($storecfg, $vmid, $conf, $old_drive);
+               }
+           } else {
+               my $path = get_iso_path($storecfg, $vmid, $drive->{file});
 
-           } else { # cdrom
+               # force eject if locked
+               mon_cmd($vmid, "eject", force => JSON::true, id => "$opt");
 
-               if ($drive->{file} eq 'none') {
-                   vm_mon_cmd($vmid, "eject",force => JSON::true,device => "drive-$opt");
-                   if (drive_is_cloudinit($old_drive)) {
-                       vmconfig_register_unused_drive($storecfg, $vmid, $conf, $old_drive);
-                   }
-               } else {
-                   my $path = get_iso_path($storecfg, $vmid, $drive->{file});
-                   vm_mon_cmd($vmid, "eject", force => JSON::true,device => "drive-$opt"); # force eject if locked
-                   vm_mon_cmd($vmid, "change", device => "drive-$opt",target => "$path") if $path;
+               if ($path) {
+                   mon_cmd($vmid, "blockdev-change-medium",
+                       id => "$opt", filename => "$path");
                }
-
-               return 1;
            }
+
+           return 1;
        }
     }
 
@@ -5265,370 +4827,465 @@ sub vmconfig_update_disk {
     vm_deviceplug($storecfg, $conf, $vmid, $opt, $drive, $arch, $machine_type);
 }
 
-sub vm_start {
-    my ($storecfg, $vmid, $statefile, $skiplock, $migratedfrom, $paused,
-       $forcemachine, $spice_ticket, $migration_network, $migration_type, $targetstorage) = @_;
+# called in locked context by incoming migration
+sub vm_migrate_get_nbd_disks {
+    my ($storecfg, $conf, $replicated_volumes) = @_;
 
-    PVE::QemuConfig->lock_config($vmid, sub {
-       my $conf = PVE::QemuConfig->load_config($vmid, $migratedfrom);
+    my $local_volumes = {};
+    PVE::QemuConfig->foreach_volume($conf, sub {
+       my ($ds, $drive) = @_;
 
-       die "you can't start a vm if it's a template\n" if PVE::QemuConfig->is_template($conf);
+       return if drive_is_cdrom($drive);
 
-       my $is_suspended = PVE::QemuConfig->has_lock($conf, 'suspended');
+       my $volid = $drive->{file};
 
-       PVE::QemuConfig->check_lock($conf)
-           if !($skiplock || $is_suspended);
+       return if !$volid;
 
-       die "VM $vmid already running\n" if check_running($vmid, undef, $migratedfrom);
+       my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
 
-       # clean up leftover reboot request files
-       eval { clear_reboot_request($vmid); };
-       warn $@ if $@;
+       my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+       return if $scfg->{shared};
 
-       if (!$statefile && scalar(keys %{$conf->{pending}})) {
-           vmconfig_apply_pending($vmid, $conf, $storecfg);
-           $conf = PVE::QemuConfig->load_config($vmid); # update/reload
-       }
+       # replicated disks re-use existing state via bitmap
+       my $use_existing = $replicated_volumes->{$volid} ? 1 : 0;
+       $local_volumes->{$ds} = [$volid, $storeid, $volname, $drive, $use_existing];
+    });
+    return $local_volumes;
+}
 
-       PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid);
+# called in locked context by incoming migration
+sub vm_migrate_alloc_nbd_disks {
+    my ($storecfg, $vmid, $source_volumes, $storagemap) = @_;
 
-       my $defaults = load_defaults();
+    my $format = undef;
 
-       # set environment variable useful inside network script
-       $ENV{PVE_MIGRATED_FROM} = $migratedfrom if $migratedfrom;
+    my $nbd = {};
+    foreach my $opt (sort keys %$source_volumes) {
+       my ($volid, $storeid, $volname, $drive, $use_existing) = @{$source_volumes->{$opt}};
 
-       my $local_volumes = {};
+       if ($use_existing) {
+           $nbd->{$opt}->{drivestr} = print_drive($drive);
+           $nbd->{$opt}->{volid} = $volid;
+           $nbd->{$opt}->{replicated} = 1;
+           next;
+       }
 
-       if ($targetstorage) {
-           foreach_drive($conf, sub {
-               my ($ds, $drive) = @_;
+       # If a remote storage is specified and the format of the original
+       # volume is not available there, fall back to the default format.
+       # Otherwise use the same format as the original.
+       if (!$storagemap->{identity}) {
+           $storeid = map_storage($storagemap, $storeid);
+           my ($defFormat, $validFormats) = PVE::Storage::storage_default_format($storecfg, $storeid);
+           my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+           my $fileFormat = qemu_img_format($scfg, $volname);
+           $format = (grep {$fileFormat eq $_} @{$validFormats}) ? $fileFormat : $defFormat;
+       } else {
+           my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+           $format = qemu_img_format($scfg, $volname);
+       }
 
-               return if drive_is_cdrom($drive);
+       my $size = $drive->{size} / 1024;
+       my $newvolid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $format, undef, $size);
+       my $newdrive = $drive;
+       $newdrive->{format} = $format;
+       $newdrive->{file} = $newvolid;
+       my $drivestr = print_drive($newdrive);
+       $nbd->{$opt}->{drivestr} = $drivestr;
+       $nbd->{$opt}->{volid} = $newvolid;
+    }
 
-               my $volid = $drive->{file};
+    return $nbd;
+}
 
-               return if !$volid;
+# see vm_start_nolock for parameters, additionally:
+# migrate_opts:
+#   storagemap = parsed storage map for allocating NBD disks
+sub vm_start {
+    my ($storecfg, $vmid, $params, $migrate_opts) = @_;
 
-               my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
+    return PVE::QemuConfig->lock_config($vmid, sub {
+       my $conf = PVE::QemuConfig->load_config($vmid, $migrate_opts->{migratedfrom});
 
-               my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
-               return if $scfg->{shared};
-               $local_volumes->{$ds} = [$volid, $storeid, $volname];
-           });
+       die "you can't start a vm if it's a template\n"
+           if !$params->{skiptemplate} && PVE::QemuConfig->is_template($conf);
 
-           my $format = undef;
+       my $has_suspended_lock = PVE::QemuConfig->has_lock($conf, 'suspended');
 
-           foreach my $opt (sort keys %$local_volumes) {
+       PVE::QemuConfig->check_lock($conf)
+           if !($params->{skiplock} || $has_suspended_lock);
 
-               my ($volid, $storeid, $volname) = @{$local_volumes->{$opt}};
-               my $drive = parse_drive($opt, $conf->{$opt});
+       $params->{resume} = $has_suspended_lock || defined($conf->{vmstate});
 
-               #if remote storage is specified, use default format
-               if ($targetstorage && $targetstorage ne "1") {
-                   $storeid = $targetstorage;
-                   my ($defFormat, $validFormats) = PVE::Storage::storage_default_format($storecfg, $storeid);
-                   $format = $defFormat;
-               } else {
-                   #else we use same format than original
-                   my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
-                   $format = qemu_img_format($scfg, $volid);
-               }
+       die "VM $vmid already running\n" if check_running($vmid, undef, $migrate_opts->{migratedfrom});
 
-               my $newvolid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $format, undef, ($drive->{size}/1024));
-               my $newdrive = $drive;
-               $newdrive->{format} = $format;
-               $newdrive->{file} = $newvolid;
-               my $drivestr = PVE::QemuServer::print_drive($vmid, $newdrive);
-               $local_volumes->{$opt} = $drivestr;
-               #pass drive to conf for command line
-               $conf->{$opt} = $drivestr;
+       if (my $storagemap = $migrate_opts->{storagemap}) {
+           my $replicated = $migrate_opts->{replicated_volumes};
+           my $disks = vm_migrate_get_nbd_disks($storecfg, $conf, $replicated);
+           $migrate_opts->{nbd} = vm_migrate_alloc_nbd_disks($storecfg, $vmid, $disks, $storagemap);
+
+           foreach my $opt (keys %{$migrate_opts->{nbd}}) {
+               $conf->{$opt} = $migrate_opts->{nbd}->{$opt}->{drivestr};
            }
        }
 
-       PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'pre-start', 1);
-
-       if ($is_suspended) {
-           # enforce machine type on suspended vm to ensure HW compatibility
-           $forcemachine = $conf->{runningmachine};
-           print "Resuming suspended VM\n";
-       }
+       return vm_start_nolock($storecfg, $vmid, $conf, $params, $migrate_opts);
+    });
+}
 
-       my ($cmd, $vollist, $spice_port) = config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine);
 
-       my $migrate_uri;
-       if ($statefile) {
-           if ($statefile eq 'tcp') {
-               my $localip = "localhost";
-               my $datacenterconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
-               my $nodename = PVE::INotify::nodename();
+# params:
+#   statefile => 'tcp', 'unix' for migration or path/volid for RAM state
+#   skiplock => 0/1, skip checking for config lock
+#   skiptemplate => 0/1, skip checking whether VM is template
+#   forcemachine => to force Qemu machine (rollback/migration)
+#   forcecpu => a QEMU '-cpu' argument string to override get_cpu_options
+#   timeout => in seconds
+#   paused => start VM in paused state (backup)
+#   resume => resume from hibernation
+# migrate_opts:
+#   nbd => volumes for NBD exports (vm_migrate_alloc_nbd_disks)
+#   migratedfrom => source node
+#   spice_ticket => used for spice migration, passed via tunnel/stdin
+#   network => CIDR of migration network
+#   type => secure/insecure - tunnel over encrypted connection or plain-text
+#   nbd_proto_version => int, 0 for TCP, 1 for UNIX
+#   replicated_volumes = which volids should be re-used with bitmaps for nbd migration
+sub vm_start_nolock {
+    my ($storecfg, $vmid, $conf, $params, $migrate_opts) = @_;
+
+    my $statefile = $params->{statefile};
+    my $resume = $params->{resume};
+
+    my $migratedfrom = $migrate_opts->{migratedfrom};
+    my $migration_type = $migrate_opts->{type};
 
-               if (!defined($migration_type)) {
-                   if (defined($datacenterconf->{migration}->{type})) {
-                       $migration_type = $datacenterconf->{migration}->{type};
-                   } else {
-                       $migration_type = 'secure';
-                   }
-               }
+    my $res = {};
 
-               if ($migration_type eq 'insecure') {
-                   my $migrate_network_addr = PVE::Cluster::get_local_migration_ip($migration_network);
-                   if ($migrate_network_addr) {
-                       $localip = $migrate_network_addr;
-                   } else {
-                       $localip = PVE::Cluster::remote_node_ip($nodename, 1);
-                   }
+    # clean up leftover reboot request files
+    eval { clear_reboot_request($vmid); };
+    warn $@ if $@;
 
-                   $localip = "[$localip]" if Net::IP::ip_is_ipv6($localip);
-               }
+    if (!$statefile && scalar(keys %{$conf->{pending}})) {
+       vmconfig_apply_pending($vmid, $conf, $storecfg);
+       $conf = PVE::QemuConfig->load_config($vmid); # update/reload
+    }
 
-               my $pfamily = PVE::Tools::get_host_address_family($nodename);
-               my $migrate_port = PVE::Tools::next_migrate_port($pfamily);
-               $migrate_uri = "tcp:${localip}:${migrate_port}";
-               push @$cmd, '-incoming', $migrate_uri;
-               push @$cmd, '-S';
+    PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid);
 
-           } elsif ($statefile eq 'unix') {
-               # should be default for secure migrations as a ssh TCP forward
-               # tunnel is not deterministic reliable ready and fails regurarly
-               # to set up in time, so use UNIX socket forwards
-               my $socket_addr = "/run/qemu-server/$vmid.migrate";
-               unlink $socket_addr;
+    my $defaults = load_defaults();
 
-               $migrate_uri = "unix:$socket_addr";
+    # set environment variable useful inside network script
+    $ENV{PVE_MIGRATED_FROM} = $migratedfrom if $migratedfrom;
 
-               push @$cmd, '-incoming', $migrate_uri;
-               push @$cmd, '-S';
+    PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'pre-start', 1);
 
-           } elsif (-e $statefile) {
-               push @$cmd, '-loadstate', $statefile;
-           } else {
-               my $statepath = PVE::Storage::path($storecfg, $statefile);
-               push @$vollist, $statepath;
-               push @$cmd, '-loadstate', $statepath;
-           }
-       } elsif ($paused) {
-           push @$cmd, '-S';
-       }
+    my $forcemachine = $params->{forcemachine};
+    my $forcecpu = $params->{forcecpu};
+    if ($resume) {
+       # enforce machine and CPU type on suspended vm to ensure HW compatibility
+       $forcemachine = $conf->{runningmachine};
+       $forcecpu = $conf->{runningcpu};
+       print "Resuming suspended VM\n";
+    }
 
-       # host pci devices
-        for (my $i = 0; $i < $MAX_HOSTPCI_DEVICES; $i++)  {
-          my $d = parse_hostpci($conf->{"hostpci$i"});
-          next if !$d;
-         my $pcidevices = $d->{pciid};
-         foreach my $pcidevice (@$pcidevices) {
-               my $pciid = $pcidevice->{id};
+    my ($cmd, $vollist, $spice_port) =
+       config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine, $forcecpu);
 
-               my $info = PVE::SysFSTools::pci_device_info("0000:$pciid");
-               die "IOMMU not present\n" if !PVE::SysFSTools::check_iommu_support();
-               die "no pci device info for device '$pciid'\n" if !$info;
+    my $migration_ip;
+    my $get_migration_ip = sub {
+       my ($nodename) = @_;
 
-               if ($d->{mdev}) {
-                   my $uuid = PVE::SysFSTools::generate_mdev_uuid($vmid, $i);
-                   PVE::SysFSTools::pci_create_mdev_device($pciid, $uuid, $d->{mdev});
-               } else {
-                   die "can't unbind/bind pci group to vfio '$pciid'\n"
-                       if !PVE::SysFSTools::pci_dev_group_bind_to_vfio($pciid);
-                   die "can't reset pci device '$pciid'\n"
-                       if $info->{has_fl_reset} and !PVE::SysFSTools::pci_dev_reset($info);
-               }
-         }
-        }
+       return $migration_ip if defined($migration_ip);
 
-       PVE::Storage::activate_volumes($storecfg, $vollist);
+       my $cidr = $migrate_opts->{network};
 
-       eval {
-           run_command(['/bin/systemctl', 'stop', "$vmid.scope"],
-               outfunc => sub {}, errfunc => sub {});
-       };
-       # Issues with the above 'stop' not being fully completed are extremely rare, a very low
-       # timeout should be more than enough here...
-       PVE::Systemd::wait_for_unit_removed("$vmid.scope", 5);
+       if (!defined($cidr)) {
+           my $dc_conf = PVE::Cluster::cfs_read_file('datacenter.cfg');
+           $cidr = $dc_conf->{migration}->{network};
+       }
 
-       my $cpuunits = defined($conf->{cpuunits}) ? $conf->{cpuunits}
-                                                 : $defaults->{cpuunits};
+       if (defined($cidr)) {
+           my $ips = PVE::Network::get_local_ip_from_cidr($cidr);
 
-       my $start_timeout = ($conf->{hugepages} || $is_suspended) ? 300 : 30;
-       my %run_params = (timeout => $statefile ? undef : $start_timeout, umask => 0077);
+           die "could not get IP: no address configured on local " .
+               "node for network '$cidr'\n" if scalar(@$ips) == 0;
 
-       my %properties = (
-           Slice => 'qemu.slice',
-           KillMode => 'none',
-           CPUShares => $cpuunits
-       );
+           die "could not get IP: multiple addresses configured on local " .
+               "node for network '$cidr'\n" if scalar(@$ips) > 1;
 
-       if (my $cpulimit = $conf->{cpulimit}) {
-           $properties{CPUQuota} = int($cpulimit * 100);
+           $migration_ip = @$ips[0];
        }
-       $properties{timeout} = 10 if $statefile; # setting up the scope shoul be quick
 
-       my $run_qemu = sub {
-           PVE::Tools::run_fork sub {
-               PVE::Systemd::enter_systemd_scope($vmid, "Proxmox VE VM $vmid", %properties);
-               run_command($cmd, %run_params);
-           };
-       };
+       $migration_ip = PVE::Cluster::remote_node_ip($nodename, 1)
+           if !defined($migration_ip);
 
-       if ($conf->{hugepages}) {
-
-           my $code = sub {
-               my $hugepages_topology = PVE::QemuServer::Memory::hugepages_topology($conf);
-               my $hugepages_host_topology = PVE::QemuServer::Memory::hugepages_host_topology();
+       return $migration_ip;
+    };
 
-               PVE::QemuServer::Memory::hugepages_mount();
-               PVE::QemuServer::Memory::hugepages_allocate($hugepages_topology, $hugepages_host_topology);
+    my $migrate_uri;
+    if ($statefile) {
+       if ($statefile eq 'tcp') {
+           my $localip = "localhost";
+           my $datacenterconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
+           my $nodename = nodename();
 
-               eval { $run_qemu->() };
-               if (my $err = $@) {
-                   PVE::QemuServer::Memory::hugepages_reset($hugepages_host_topology);
-                   die $err;
+           if (!defined($migration_type)) {
+               if (defined($datacenterconf->{migration}->{type})) {
+                   $migration_type = $datacenterconf->{migration}->{type};
+               } else {
+                   $migration_type = 'secure';
                }
+           }
 
-               PVE::QemuServer::Memory::hugepages_pre_deallocate($hugepages_topology);
-           };
-           eval { PVE::QemuServer::Memory::hugepages_update_locked($code); };
+           if ($migration_type eq 'insecure') {
+               $localip = $get_migration_ip->($nodename);
+               $localip = "[$localip]" if Net::IP::ip_is_ipv6($localip);
+           }
+
+           my $pfamily = PVE::Tools::get_host_address_family($nodename);
+           my $migrate_port = PVE::Tools::next_migrate_port($pfamily);
+           $migrate_uri = "tcp:${localip}:${migrate_port}";
+           push @$cmd, '-incoming', $migrate_uri;
+           push @$cmd, '-S';
+
+       } elsif ($statefile eq 'unix') {
+           # should be default for secure migrations as a ssh TCP forward
+           # tunnel is not deterministic reliable ready and fails regurarly
+           # to set up in time, so use UNIX socket forwards
+           my $socket_addr = "/run/qemu-server/$vmid.migrate";
+           unlink $socket_addr;
+
+           $migrate_uri = "unix:$socket_addr";
+
+           push @$cmd, '-incoming', $migrate_uri;
+           push @$cmd, '-S';
 
+       } elsif (-e $statefile) {
+           push @$cmd, '-loadstate', $statefile;
        } else {
-           eval { $run_qemu->() };
+           my $statepath = PVE::Storage::path($storecfg, $statefile);
+           push @$vollist, $statefile;
+           push @$cmd, '-loadstate', $statepath;
        }
+    } elsif ($params->{paused}) {
+       push @$cmd, '-S';
+    }
 
-       if (my $err = $@) {
-           # deactivate volumes if start fails
-           eval { PVE::Storage::deactivate_volumes($storecfg, $vollist); };
-           die "start failed: $err";
-       }
+    # host pci devices
+    for (my $i = 0; $i < $PVE::QemuServer::PCI::MAX_HOSTPCI_DEVICES; $i++)  {
+      my $d = parse_hostpci($conf->{"hostpci$i"});
+      next if !$d;
+      my $pcidevices = $d->{pciid};
+      foreach my $pcidevice (@$pcidevices) {
+           my $pciid = $pcidevice->{id};
+
+           my $info = PVE::SysFSTools::pci_device_info("$pciid");
+           die "IOMMU not present\n" if !PVE::SysFSTools::check_iommu_support();
+           die "no pci device info for device '$pciid'\n" if !$info;
+
+           if ($d->{mdev}) {
+               my $uuid = PVE::SysFSTools::generate_mdev_uuid($vmid, $i);
+               PVE::SysFSTools::pci_create_mdev_device($pciid, $uuid, $d->{mdev});
+           } else {
+               die "can't unbind/bind pci group to vfio '$pciid'\n"
+                   if !PVE::SysFSTools::pci_dev_group_bind_to_vfio($pciid);
+               die "can't reset pci device '$pciid'\n"
+                   if $info->{has_fl_reset} and !PVE::SysFSTools::pci_dev_reset($info);
+           }
+      }
+    }
 
-       print "migration listens on $migrate_uri\n" if $migrate_uri;
+    PVE::Storage::activate_volumes($storecfg, $vollist);
 
-       if ($statefile && $statefile ne 'tcp' && $statefile ne 'unix')  {
-           eval { vm_mon_cmd_nocheck($vmid, "cont"); };
-           warn $@ if $@;
-       }
+    eval {
+       run_command(['/bin/systemctl', 'stop', "$vmid.scope"],
+           outfunc => sub {}, errfunc => sub {});
+    };
+    # Issues with the above 'stop' not being fully completed are extremely rare, a very low
+    # timeout should be more than enough here...
+    PVE::Systemd::wait_for_unit_removed("$vmid.scope", 5);
+
+    my $cpuunits = defined($conf->{cpuunits}) ? $conf->{cpuunits}
+                                             : $defaults->{cpuunits};
+
+    my $start_timeout = $params->{timeout} // config_aware_timeout($conf, $resume);
+    my %run_params = (
+       timeout => $statefile ? undef : $start_timeout,
+       umask => 0077,
+       noerr => 1,
+    );
 
-       #start nbd server for storage migration
-       if ($targetstorage) {
-           my $nodename = PVE::INotify::nodename();
-           my $migrate_network_addr = PVE::Cluster::get_local_migration_ip($migration_network);
-           my $localip = $migrate_network_addr ? $migrate_network_addr : PVE::Cluster::remote_node_ip($nodename, 1);
-           my $pfamily = PVE::Tools::get_host_address_family($nodename);
-           my $storage_migrate_port = PVE::Tools::next_migrate_port($pfamily);
+    # when migrating, prefix QEMU output so other side can pick up any
+    # errors that might occur and show the user
+    if ($migratedfrom) {
+       $run_params{quiet} = 1;
+       $run_params{logfunc} = sub { print "QEMU: $_[0]\n" };
+    }
 
-           vm_mon_cmd_nocheck($vmid, "nbd-server-start", addr => { type => 'inet', data => { host => "${localip}", port => "${storage_migrate_port}" } } );
+    my %properties = (
+       Slice => 'qemu.slice',
+       KillMode => 'none',
+       CPUShares => $cpuunits
+    );
 
-           $localip = "[$localip]" if Net::IP::ip_is_ipv6($localip);
+    if (my $cpulimit = $conf->{cpulimit}) {
+       $properties{CPUQuota} = int($cpulimit * 100);
+    }
+    $properties{timeout} = 10 if $statefile; # setting up the scope shoul be quick
 
-           foreach my $opt (sort keys %$local_volumes) {
-               my $volid = $local_volumes->{$opt};
-               vm_mon_cmd_nocheck($vmid, "nbd-server-add", device => "drive-$opt", writable => JSON::true );
-               my $migrate_storage_uri = "nbd:${localip}:${storage_migrate_port}:exportname=drive-$opt";
-               print "storage migration listens on $migrate_storage_uri volume:$volid\n";
-           }
-       }
+    my $run_qemu = sub {
+       PVE::Tools::run_fork sub {
+           PVE::Systemd::enter_systemd_scope($vmid, "Proxmox VE VM $vmid", %properties);
 
-       if ($migratedfrom) {
-           eval {
-               set_migration_caps($vmid);
-           };
-           warn $@ if $@;
+           my $exitcode = run_command($cmd, %run_params);
+           die "QEMU exited with code $exitcode\n" if $exitcode;
+       };
+    };
 
-           if ($spice_port) {
-               print "spice listens on port $spice_port\n";
-               if ($spice_ticket) {
-                   vm_mon_cmd_nocheck($vmid, "set_password", protocol => 'spice', password => $spice_ticket);
-                   vm_mon_cmd_nocheck($vmid, "expire_password", protocol => 'spice', time => "+30");
-               }
-           }
+    if ($conf->{hugepages}) {
 
-       } else {
-           vm_mon_cmd_nocheck($vmid, "balloon", value => $conf->{balloon}*1024*1024)
-               if !$statefile && $conf->{balloon};
+       my $code = sub {
+           my $hugepages_topology = PVE::QemuServer::Memory::hugepages_topology($conf);
+           my $hugepages_host_topology = PVE::QemuServer::Memory::hugepages_host_topology();
+
+           PVE::QemuServer::Memory::hugepages_mount();
+           PVE::QemuServer::Memory::hugepages_allocate($hugepages_topology, $hugepages_host_topology);
 
-           foreach my $opt (keys %$conf) {
-               next if $opt !~  m/^net\d+$/;
-               my $nicconf = parse_net($conf->{$opt});
-               qemu_set_link_status($vmid, $opt, 0) if $nicconf->{link_down};
+           eval { $run_qemu->() };
+           if (my $err = $@) {
+               PVE::QemuServer::Memory::hugepages_reset($hugepages_host_topology)
+                   if !$conf->{keephugepages};
+               die $err;
            }
-       }
 
-       vm_mon_cmd_nocheck($vmid, 'qom-set',
-                   path => "machine/peripheral/balloon0",
-                   property => "guest-stats-polling-interval",
-                   value => 2) if (!defined($conf->{balloon}) || $conf->{balloon});
+           PVE::QemuServer::Memory::hugepages_pre_deallocate($hugepages_topology)
+               if !$conf->{keephugepages};
+       };
+       eval { PVE::QemuServer::Memory::hugepages_update_locked($code); };
 
-       if ($is_suspended && (my $vmstate = $conf->{vmstate})) {
-           print "Resumed VM, removing state\n";
-           delete $conf->@{qw(lock vmstate runningmachine)};
-           PVE::Storage::deactivate_volumes($storecfg, [$vmstate]);
-           PVE::Storage::vdisk_free($storecfg, $vmstate);
-           PVE::QemuConfig->write_config($vmid, $conf);
-       }
+    } else {
+       eval { $run_qemu->() };
+    }
 
-       PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-start');
-    });
-}
+    if (my $err = $@) {
+       # deactivate volumes if start fails
+       eval { PVE::Storage::deactivate_volumes($storecfg, $vollist); };
+       die "start failed: $err";
+    }
 
-sub vm_mon_cmd {
-    my ($vmid, $execute, %params) = @_;
+    print "migration listens on $migrate_uri\n" if $migrate_uri;
+    $res->{migrate_uri} = $migrate_uri;
 
-    my $cmd = { execute => $execute, arguments => \%params };
-    vm_qmp_command($vmid, $cmd);
-}
+    if ($statefile && $statefile ne 'tcp' && $statefile ne 'unix')  {
+       eval { mon_cmd($vmid, "cont"); };
+       warn $@ if $@;
+    }
 
-sub vm_mon_cmd_nocheck {
-    my ($vmid, $execute, %params) = @_;
+    #start nbd server for storage migration
+    if (my $nbd = $migrate_opts->{nbd}) {
+       my $nbd_protocol_version = $migrate_opts->{nbd_proto_version} // 0;
 
-    my $cmd = { execute => $execute, arguments => \%params };
-    vm_qmp_command($vmid, $cmd, 1);
-}
+       my $migrate_storage_uri;
+       # nbd_protocol_version > 0 for unix socket support
+       if ($nbd_protocol_version > 0 && $migration_type eq 'secure') {
+           my $socket_path = "/run/qemu-server/$vmid\_nbd.migrate";
+           mon_cmd($vmid, "nbd-server-start", addr => { type => 'unix', data => { path => $socket_path } } );
+           $migrate_storage_uri = "nbd:unix:$socket_path";
+       } else {
+           my $nodename = nodename();
+           my $localip = $get_migration_ip->($nodename);
+           my $pfamily = PVE::Tools::get_host_address_family($nodename);
+           my $storage_migrate_port = PVE::Tools::next_migrate_port($pfamily);
 
-sub vm_qmp_command {
-    my ($vmid, $cmd, $nocheck) = @_;
+           mon_cmd($vmid, "nbd-server-start", addr => {
+               type => 'inet',
+               data => {
+                   host => "${localip}",
+                   port => "${storage_migrate_port}",
+               },
+           });
+           $localip = "[$localip]" if Net::IP::ip_is_ipv6($localip);
+           $migrate_storage_uri = "nbd:${localip}:${storage_migrate_port}";
+       }
+
+       $res->{migrate_storage_uri} = $migrate_storage_uri;
 
-    my $res;
+       foreach my $opt (sort keys %$nbd) {
+           my $drivestr = $nbd->{$opt}->{drivestr};
+           my $volid = $nbd->{$opt}->{volid};
+           mon_cmd($vmid, "nbd-server-add", device => "drive-$opt", writable => JSON::true );
+           my $nbd_uri = "$migrate_storage_uri:exportname=drive-$opt";
+           print "storage migration listens on $nbd_uri volume:$drivestr\n";
+           print "re-using replicated volume: $opt - $volid\n"
+               if $nbd->{$opt}->{replicated};
 
-    my $timeout;
-    if ($cmd->{arguments}) {
-       $timeout = delete $cmd->{arguments}->{timeout};
+           $res->{drives}->{$opt} = $nbd->{$opt};
+           $res->{drives}->{$opt}->{nbd_uri} = $nbd_uri;
+       }
     }
 
-    eval {
-       die "VM $vmid not running\n" if !check_running($vmid, $nocheck);
-       my $sname = qmp_socket($vmid);
-       if (-e $sname) { # test if VM is reasonambe new and supports qmp/qga
-           my $qmpclient = PVE::QMPClient->new();
+    if ($migratedfrom) {
+       eval {
+           set_migration_caps($vmid);
+       };
+       warn $@ if $@;
 
-           $res = $qmpclient->cmd($vmid, $cmd, $timeout);
-       } else {
-           die "unable to open monitor socket\n";
+       if ($spice_port) {
+           print "spice listens on port $spice_port\n";
+           $res->{spice_port} = $spice_port;
+           if ($migrate_opts->{spice_ticket}) {
+               mon_cmd($vmid, "set_password", protocol => 'spice', password =>
+                   $migrate_opts->{spice_ticket});
+               mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30");
+           }
+       }
+
+    } else {
+       mon_cmd($vmid, "balloon", value => $conf->{balloon}*1024*1024)
+           if !$statefile && $conf->{balloon};
+
+       foreach my $opt (keys %$conf) {
+           next if $opt !~  m/^net\d+$/;
+           my $nicconf = parse_net($conf->{$opt});
+           qemu_set_link_status($vmid, $opt, 0) if $nicconf->{link_down};
        }
-    };
-    if (my $err = $@) {
-       syslog("err", "VM $vmid qmp command failed - $err");
-       die $err;
     }
 
-    return $res;
-}
+    mon_cmd($vmid, 'qom-set',
+               path => "machine/peripheral/balloon0",
+               property => "guest-stats-polling-interval",
+               value => 2) if (!defined($conf->{balloon}) || $conf->{balloon});
 
-sub vm_human_monitor_command {
-    my ($vmid, $cmdline) = @_;
+    if ($resume) {
+       print "Resumed VM, removing state\n";
+       if (my $vmstate = $conf->{vmstate}) {
+           PVE::Storage::deactivate_volumes($storecfg, [$vmstate]);
+           PVE::Storage::vdisk_free($storecfg, $vmstate);
+       }
+       delete $conf->@{qw(lock vmstate runningmachine runningcpu)};
+       PVE::QemuConfig->write_config($vmid, $conf);
+    }
 
-    my $cmd = {
-       execute => 'human-monitor-command',
-       arguments => { 'command-line' => $cmdline},
-    };
+    PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-start');
 
-    return vm_qmp_command($vmid, $cmd);
+    return $res;
 }
 
 sub vm_commandline {
     my ($storecfg, $vmid, $snapname) = @_;
 
     my $conf = PVE::QemuConfig->load_config($vmid);
+    my $forcemachine;
+    my $forcecpu;
 
     if ($snapname) {
        my $snapshot = $conf->{snapshots}->{$snapname};
        die "snapshot '$snapname' does not exist\n" if !defined($snapshot);
 
+       # check for machine or CPU overrides in snapshot
+       $forcemachine = $snapshot->{runningmachine};
+       $forcecpu = $snapshot->{runningcpu};
+
        $snapshot->{digest} = $conf->{digest}; # keep file digest for API
 
        $conf = $snapshot;
@@ -5636,7 +5293,8 @@ sub vm_commandline {
 
     my $defaults = load_defaults();
 
-    my $cmd = config_to_command($storecfg, $vmid, $conf, $defaults);
+    my $cmd = config_to_command($storecfg, $vmid, $conf, $defaults,
+       $forcemachine, $forcecpu);
 
     return PVE::Tools::cmd2string($cmd);
 }
@@ -5650,7 +5308,7 @@ sub vm_reset {
 
        PVE::QemuConfig->check_lock($conf) if !$skiplock;
 
-       vm_mon_cmd($vmid, "system_reset");
+       mon_cmd($vmid, "system_reset");
     });
 }
 
@@ -5687,7 +5345,7 @@ sub vm_stop_cleanup {
        }
 
        if ($conf->{ivshmem}) {
-           my $ivshmem = PVE::JSONSchema::parse_property_string($ivshmem_fmt, $conf->{ivshmem});
+           my $ivshmem = parse_property_string($ivshmem_fmt, $conf->{ivshmem});
            # just delete it for now, VMs which have this already open do not
            # are affected, but new VMs will get a separated one. If this
            # becomes an issue we either add some sort of ref-counting or just
@@ -5733,15 +5391,12 @@ sub _do_vm_stop {
     eval {
        if ($shutdown) {
            if (defined($conf) && parse_guest_agent($conf)->{enabled}) {
-               vm_qmp_command($vmid, {
-                       execute => "guest-shutdown",
-                       arguments => { timeout => $timeout }
-                   }, $nocheck);
+               mon_cmd($vmid, "guest-shutdown", timeout => $timeout);
            } else {
-               vm_qmp_command($vmid, { execute => "system_powerdown" }, $nocheck);
+               mon_cmd($vmid, "system_powerdown");
            }
        } else {
-           vm_qmp_command($vmid, { execute => "quit" }, $nocheck);
+           mon_cmd($vmid, "quit");
        }
     };
     my $err = $@;
@@ -5767,7 +5422,11 @@ sub _do_vm_stop {
            return;
        }
     } else {
-       if ($force) {
+       if (!check_running($vmid, $nocheck)) {
+           warn "Unexpected: VM shutdown command failed, but VM not running anymore..\n";
+           return;
+       }
+       if ($force) {
            warn "VM quit/powerdown failed - terminating now with SIGTERM\n";
            kill 15, $pid;
        } else {
@@ -5818,18 +5477,26 @@ sub vm_reboot {
     my ($vmid, $timeout) = @_;
 
     PVE::QemuConfig->lock_config($vmid, sub {
+       eval {
 
-       # only reboot if running, as qmeventd starts it again on a stop event
-       return if !check_running($vmid);
+           # only reboot if running, as qmeventd starts it again on a stop event
+           return if !check_running($vmid);
 
-       create_reboot_request($vmid);
+           create_reboot_request($vmid);
 
-       my $storecfg = PVE::Storage::config();
-       _do_vm_stop($storecfg, $vmid, undef, undef, $timeout, 1);
+           my $storecfg = PVE::Storage::config();
+           _do_vm_stop($storecfg, $vmid, undef, undef, $timeout, 1);
 
+       };
+       if (my $err = $@) {
+           # avoid that the next normal shutdown will be confused for a reboot
+           clear_reboot_request($vmid);
+           die $err;
+       }
    });
 }
 
+# note: if using the statestorage parameter, the caller has to check privileges
 sub vm_suspend {
     my ($vmid, $skiplock, $includestate, $statestorage) = @_;
 
@@ -5853,11 +5520,23 @@ sub vm_suspend {
            $conf->{lock} = 'suspending';
            my $date = strftime("%Y-%m-%d", localtime(time()));
            $storecfg = PVE::Storage::config();
-           $vmstate = PVE::QemuConfig->__snapshot_save_vmstate($vmid, $conf, "suspend-$date", $storecfg, $statestorage, 1);
+           if (!$statestorage) {
+               $statestorage = find_vmstate_storage($conf, $storecfg);
+               # check permissions for the storage
+               my $rpcenv = PVE::RPCEnvironment::get();
+               if ($rpcenv->{type} ne 'cli') {
+                   my $authuser = $rpcenv->get_user();
+                   $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
+               }
+           }
+
+
+           $vmstate = PVE::QemuConfig->__snapshot_save_vmstate(
+               $vmid, $conf, "suspend-$date", $storecfg, $statestorage, 1);
            $path = PVE::Storage::path($storecfg, $vmstate);
            PVE::QemuConfig->write_config($vmid, $conf);
        } else {
-           vm_mon_cmd($vmid, "stop");
+           mon_cmd($vmid, "stop");
        }
     });
 
@@ -5866,9 +5545,9 @@ sub vm_suspend {
        PVE::Storage::activate_volumes($storecfg, [$vmstate]);
 
        eval {
-           vm_mon_cmd($vmid, "savevm-start", statefile => $path);
+           mon_cmd($vmid, "savevm-start", statefile => $path);
            for(;;) {
-               my $state = vm_mon_cmd_nocheck($vmid, "query-savevm");
+               my $state = mon_cmd($vmid, "query-savevm");
                if (!$state->{status}) {
                    die "savevm not active\n";
                } elsif ($state->{status} eq 'active') {
@@ -5891,10 +5570,10 @@ sub vm_suspend {
            if ($err) {
                # cleanup, but leave suspending lock, to indicate something went wrong
                eval {
-                   vm_mon_cmd($vmid, "savevm-end");
+                   mon_cmd($vmid, "savevm-end");
                    PVE::Storage::deactivate_volumes($storecfg, [$vmstate]);
                    PVE::Storage::vdisk_free($storecfg, $vmstate);
-                   delete $conf->@{qw(vmstate runningmachine)};
+                   delete $conf->@{qw(vmstate runningmachine runningcpu)};
                    PVE::QemuConfig->write_config($vmid, $conf);
                };
                warn $@ if $@;
@@ -5904,7 +5583,7 @@ sub vm_suspend {
            die "lock changed unexpectedly\n"
                if !PVE::QemuConfig->has_lock($conf, 'suspending');
 
-           vm_qmp_command($vmid, { execute => "quit" });
+           mon_cmd($vmid, "quit");
            $conf->{lock} = 'suspended';
            PVE::QemuConfig->write_config($vmid, $conf);
        });
@@ -5915,8 +5594,7 @@ sub vm_resume {
     my ($vmid, $skiplock, $nocheck) = @_;
 
     PVE::QemuConfig->lock_config($vmid, sub {
-       my $vm_mon_cmd = $nocheck ? \&vm_mon_cmd_nocheck : \&vm_mon_cmd;
-       my $res = $vm_mon_cmd->($vmid, 'query-status');
+       my $res = mon_cmd($vmid, 'query-status');
        my $resume_cmd = 'cont';
 
        if ($res->{status} && $res->{status} eq 'suspended') {
@@ -5931,7 +5609,7 @@ sub vm_resume {
                if !($skiplock || PVE::QemuConfig->has_lock($conf, 'backup'));
        }
 
-       $vm_mon_cmd->($vmid, $resume_cmd);
+       mon_cmd($vmid, $resume_cmd);
     });
 }
 
@@ -5943,26 +5621,11 @@ sub vm_sendkey {
        my $conf = PVE::QemuConfig->load_config($vmid);
 
        # there is no qmp command, so we use the human monitor command
-       my $res = vm_human_monitor_command($vmid, "sendkey $key");
+       my $res = PVE::QemuServer::Monitor::hmp_cmd($vmid, "sendkey $key");
        die $res if $res ne '';
     });
 }
 
-sub vm_destroy {
-    my ($storecfg, $vmid, $skiplock) = @_;
-
-    PVE::QemuConfig->lock_config($vmid, sub {
-
-       my $conf = PVE::QemuConfig->load_config($vmid);
-
-       if (!check_running($vmid)) {
-           destroy_vm($storecfg, $vmid, undef, $skiplock);
-       } else {
-           die "VM $vmid is running - destroy failed\n";
-       }
-    });
-}
-
 # vzdump restore implementaion
 
 sub tar_archive_read_firstfile {
@@ -6009,31 +5672,15 @@ sub tar_restore_cleanup {
     }
 }
 
-sub restore_archive {
+sub restore_file_archive {
     my ($archive, $vmid, $user, $opts) = @_;
 
-    my $format = $opts->{format};
-    my $comp;
-
-    if ($archive =~ m/\.tgz$/ || $archive =~ m/\.tar\.gz$/) {
-       $format = 'tar' if !$format;
-       $comp = 'gzip';
-    } elsif ($archive =~ m/\.tar$/) {
-       $format = 'tar' if !$format;
-    } elsif ($archive =~ m/.tar.lzo$/) {
-       $format = 'tar' if !$format;
-       $comp = 'lzop';
-    } elsif ($archive =~ m/\.vma$/) {
-       $format = 'vma' if !$format;
-    } elsif ($archive =~ m/\.vma\.gz$/) {
-       $format = 'vma' if !$format;
-       $comp = 'gzip';
-    } elsif ($archive =~ m/\.vma\.lzo$/) {
-       $format = 'vma' if !$format;
-       $comp = 'lzop';
-    } else {
-       $format = 'vma' if !$format; # default
-    }
+    return restore_vma_archive($archive, $vmid, $user, $opts)
+       if $archive eq '-';
+
+    my $info = PVE::Storage::archive_info($archive);
+    my $format = $opts->{format} // $info->{format};
+    my $comp = $info->{compression};
 
     # try to detect archive format
     if ($format eq 'tar') {
@@ -6043,7 +5690,146 @@ sub restore_archive {
     }
 }
 
-sub restore_update_config_line {
+# hepler to remove disks that will not be used after restore
+my $restore_cleanup_oldconf = sub {
+    my ($storecfg, $vmid, $oldconf, $virtdev_hash) = @_;
+
+    PVE::QemuConfig->foreach_volume($oldconf, sub {
+       my ($ds, $drive) = @_;
+
+       return if drive_is_cdrom($drive, 1);
+
+       my $volid = $drive->{file};
+       return if !$volid || $volid =~ m|^/|;
+
+       my ($path, $owner) = PVE::Storage::path($storecfg, $volid);
+       return if !$path || !$owner || ($owner != $vmid);
+
+       # Note: only delete disk we want to restore
+       # other volumes will become unused
+       if ($virtdev_hash->{$ds}) {
+           eval { PVE::Storage::vdisk_free($storecfg, $volid); };
+           if (my $err = $@) {
+               warn $err;
+           }
+       }
+    });
+
+    # delete vmstate files, after the restore we have no snapshots anymore
+    foreach my $snapname (keys %{$oldconf->{snapshots}}) {
+       my $snap = $oldconf->{snapshots}->{$snapname};
+       if ($snap->{vmstate}) {
+           eval { PVE::Storage::vdisk_free($storecfg, $snap->{vmstate}); };
+           if (my $err = $@) {
+               warn $err;
+           }
+       }
+    }
+};
+
+# Helper to parse vzdump backup device hints
+#
+# $rpcenv: Environment, used to ckeck storage permissions
+# $user: User ID, to check storage permissions
+# $storecfg: Storage configuration
+# $fh: the file handle for reading the configuration
+# $devinfo: should contain device sizes for all backu-up'ed devices
+# $options: backup options (pool, default storage)
+#
+# Return: $virtdev_hash, updates $devinfo (add devname, virtdev, format, storeid)
+my $parse_backup_hints = sub {
+    my ($rpcenv, $user, $storecfg, $fh, $devinfo, $options) = @_;
+
+    my $virtdev_hash = {};
+
+    while (defined(my $line = <$fh>)) {
+       if ($line =~ m/^\#qmdump\#map:(\S+):(\S+):(\S*):(\S*):$/) {
+           my ($virtdev, $devname, $storeid, $format) = ($1, $2, $3, $4);
+           die "archive does not contain data for drive '$virtdev'\n"
+               if !$devinfo->{$devname};
+
+           if (defined($options->{storage})) {
+               $storeid = $options->{storage} || 'local';
+           } elsif (!$storeid) {
+               $storeid = 'local';
+           }
+           $format = 'raw' if !$format;
+           $devinfo->{$devname}->{devname} = $devname;
+           $devinfo->{$devname}->{virtdev} = $virtdev;
+           $devinfo->{$devname}->{format} = $format;
+           $devinfo->{$devname}->{storeid} = $storeid;
+
+           # check permission on storage
+           my $pool = $options->{pool}; # todo: do we need that?
+           if ($user ne 'root@pam') {
+               $rpcenv->check($user, "/storage/$storeid", ['Datastore.AllocateSpace']);
+           }
+
+           $virtdev_hash->{$virtdev} = $devinfo->{$devname};
+       } elsif ($line =~ m/^((?:ide|sata|scsi)\d+):\s*(.*)\s*$/) {
+           my $virtdev = $1;
+           my $drive = parse_drive($virtdev, $2);
+           if (drive_is_cloudinit($drive)) {
+               my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file});
+               $storeid = $options->{storage} if defined ($options->{storage});
+               my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+               my $format = qemu_img_format($scfg, $volname); # has 'raw' fallback
+
+               $virtdev_hash->{$virtdev} = {
+                   format => $format,
+                   storeid => $storeid,
+                   size => PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE,
+                   is_cloudinit => 1,
+               };
+           }
+       }
+    }
+
+    return $virtdev_hash;
+};
+
+# Helper to allocate and activate all volumes required for a restore
+#
+# $storecfg: Storage configuration
+# $virtdev_hash: as returned by parse_backup_hints()
+#
+# Returns: { $virtdev => $volid }
+my $restore_allocate_devices = sub {
+    my ($storecfg, $virtdev_hash, $vmid) = @_;
+
+    my $map = {};
+    foreach my $virtdev (sort keys %$virtdev_hash) {
+       my $d = $virtdev_hash->{$virtdev};
+       my $alloc_size = int(($d->{size} + 1024 - 1)/1024);
+       my $storeid = $d->{storeid};
+       my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+
+       # test if requested format is supported
+       my ($defFormat, $validFormats) = PVE::Storage::storage_default_format($storecfg, $storeid);
+       my $supported = grep { $_ eq $d->{format} } @$validFormats;
+       $d->{format} = $defFormat if !$supported;
+
+       my $name;
+       if ($d->{is_cloudinit}) {
+           $name = "vm-$vmid-cloudinit";
+           $name .= ".$d->{format}" if $d->{format} ne 'raw';
+       }
+
+       my $volid = PVE::Storage::vdisk_alloc(
+           $storecfg, $storeid, $vmid, $d->{format}, $name, $alloc_size);
+
+       print STDERR "new volume ID is '$volid'\n";
+       $d->{volid} = $volid;
+
+       PVE::Storage::activate_volumes($storecfg, [$volid]);
+
+       $map->{$virtdev} = $volid;
+    }
+
+    return $map;
+};
+
+my $restore_update_config_line = sub {
     my ($outfd, $cookie, $vmid, $map, $line, $unique) = @_;
 
     return if $line =~ m/^\#qmdump\#/;
@@ -6084,7 +5870,7 @@ sub restore_update_config_line {
        } elsif ($map->{$virtdev}) {
            delete $di->{format}; # format can change on restore
            $di->{file} = $map->{$virtdev};
-           $value = print_drive($vmid, $di);
+           $value = print_drive($di);
            print $outfd "$virtdev: $value\n";
        } else {
            print $outfd $line;
@@ -6106,7 +5892,37 @@ sub restore_update_config_line {
     } else {
        print $outfd $line;
     }
-}
+};
+
+my $restore_deactivate_volumes = sub {
+    my ($storecfg, $devinfo) = @_;
+
+    my $vollist = [];
+    foreach my $devname (keys %$devinfo) {
+       my $volid = $devinfo->{$devname}->{volid};
+       push @$vollist, $volid if $volid;
+    }
+
+    PVE::Storage::deactivate_volumes($storecfg, $vollist);
+};
+
+my $restore_destroy_volumes = sub {
+    my ($storecfg, $devinfo) = @_;
+
+    foreach my $devname (keys %$devinfo) {
+       my $volid = $devinfo->{$devname}->{volid};
+       next if !$volid;
+       eval {
+           if ($volid =~ m|^/|) {
+               unlink $volid || die 'unlink failed\n';
+           } else {
+               PVE::Storage::vdisk_free($storecfg, $volid);
+           }
+           print STDERR "temporary volume '$volid' sucessfuly removed\n";
+       };
+       print STDERR "unable to cleanup '$volid' - $@" if $@;
+    }
+};
 
 sub scan_volids {
     my ($cfg, $vmid) = @_;
@@ -6125,52 +5941,11 @@ sub scan_volids {
     return $volid_hash;
 }
 
-sub is_volume_in_use {
-    my ($storecfg, $conf, $skip_drive, $volid) = @_;
-
-    my $path = PVE::Storage::path($storecfg, $volid);
-
-    my $scan_config = sub {
-       my ($cref, $snapname) = @_;
-
-       foreach my $key (keys %$cref) {
-           my $value = $cref->{$key};
-           if (is_valid_drivename($key)) {
-               next if $skip_drive && $key eq $skip_drive;
-               my $drive = parse_drive($key, $value);
-               next if !$drive || !$drive->{file} || drive_is_cdrom($drive);
-               return 1 if $volid eq $drive->{file};
-               if ($drive->{file} =~ m!^/!) {
-                   return 1 if $drive->{file} eq $path;
-               } else {
-                   my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file}, 1);
-                   next if !$storeid;
-                   my $scfg = PVE::Storage::storage_config($storecfg, $storeid, 1);
-                   next if !$scfg;
-                   return 1 if $path eq PVE::Storage::path($storecfg, $drive->{file}, $snapname);
-               }
-           }
-       }
-
-       return 0;
-    };
-
-    return 1 if &$scan_config($conf);
-
-    undef $skip_drive;
-
-    foreach my $snapname (keys %{$conf->{snapshots}}) {
-       return 1 if &$scan_config($conf->{snapshots}->{$snapname}, $snapname);
-    }
-
-    return 0;
-}
-
-sub update_disksize {
+sub update_disk_config {
     my ($vmid, $conf, $volid_hash) = @_;
 
     my $changes;
-    my $prefix = "VM $vmid:";
+    my $prefix = "VM $vmid";
 
     # used and unused disks
     my $referenced = {};
@@ -6182,109 +5957,288 @@ sub update_disksize {
     my $referencedpath = {};
 
     # update size info
-    foreach my $opt (keys %$conf) {
-       if (is_valid_drivename($opt)) {
-           my $drive = parse_drive($opt, $conf->{$opt});
-           my $volid = $drive->{file};
-           next if !$volid;
+    PVE::QemuConfig->foreach_volume($conf, sub {
+       my ($opt, $drive) = @_;
 
-           $referenced->{$volid} = 1;
-           if ($volid_hash->{$volid} &&
-               (my $path = $volid_hash->{$volid}->{path})) {
-               $referencedpath->{$path} = 1;
-           }
+       my $volid = $drive->{file};
+       return if !$volid;
+       my $volume = $volid_hash->{$volid};
 
-           next if drive_is_cdrom($drive);
-           next if !$volid_hash->{$volid};
+       # mark volid as "in-use" for next step
+       $referenced->{$volid} = 1;
+       if ($volume && (my $path = $volume->{path})) {
+           $referencedpath->{$path} = 1;
+       }
 
-           $drive->{size} = $volid_hash->{$volid}->{size};
-           my $new = print_drive($vmid, $drive);
-           if ($new ne $conf->{$opt}) {
-               $changes = 1;
-               $conf->{$opt} = $new;
-               print "$prefix update disk '$opt' information.\n";
-           }
+       return if drive_is_cdrom($drive);
+       return if !$volume;
+
+       my ($updated, $msg) = PVE::QemuServer::Drive::update_disksize($drive, $volume->{size});
+       if (defined($updated)) {
+           $changes = 1;
+           $conf->{$opt} = print_drive($updated);
+           print "$prefix ($opt): $msg\n";
        }
-    }
+    });
 
     # remove 'unusedX' entry if volume is used
-    foreach my $opt (keys %$conf) {
-       next if $opt !~ m/^unused\d+$/;
-       my $volid = $conf->{$opt};
+    PVE::QemuConfig->foreach_unused_volume($conf, sub {
+       my ($opt, $drive) = @_;
+
+       my $volid = $drive->{file};
+       return if !$volid;
+
        my $path = $volid_hash->{$volid}->{path} if $volid_hash->{$volid};
        if ($referenced->{$volid} || ($path && $referencedpath->{$path})) {
-           print "$prefix remove entry '$opt', its volume '$volid' is in use.\n";
+           print "$prefix remove entry '$opt', its volume '$volid' is in use\n";
            $changes = 1;
            delete $conf->{$opt};
        }
 
-       $referenced->{$volid} = 1;
-       $referencedpath->{$path} = 1 if $path;
-    }
+       $referenced->{$volid} = 1;
+       $referencedpath->{$path} = 1 if $path;
+    });
+
+    foreach my $volid (sort keys %$volid_hash) {
+       next if $volid =~ m/vm-$vmid-state-/;
+       next if $referenced->{$volid};
+       my $path = $volid_hash->{$volid}->{path};
+       next if !$path; # just to be sure
+       next if $referencedpath->{$path};
+       $changes = 1;
+       my $key = PVE::QemuConfig->add_unused_volume($conf, $volid);
+       print "$prefix add unreferenced volume '$volid' as '$key' to config\n";
+       $referencedpath->{$path} = 1; # avoid to add more than once (aliases)
+    }
+
+    return $changes;
+}
+
+sub rescan {
+    my ($vmid, $nolock, $dryrun) = @_;
+
+    my $cfg = PVE::Storage::config();
+
+    # FIXME: Remove once our RBD plugin can handle CT and VM on a single storage
+    # see: https://pve.proxmox.com/pipermail/pve-devel/2018-July/032900.html
+    foreach my $stor (keys %{$cfg->{ids}}) {
+       delete($cfg->{ids}->{$stor}) if ! $cfg->{ids}->{$stor}->{content}->{images};
+    }
+
+    print "rescan volumes...\n";
+    my $volid_hash = scan_volids($cfg, $vmid);
+
+    my $updatefn =  sub {
+       my ($vmid) = @_;
+
+       my $conf = PVE::QemuConfig->load_config($vmid);
+
+       PVE::QemuConfig->check_lock($conf);
+
+       my $vm_volids = {};
+       foreach my $volid (keys %$volid_hash) {
+           my $info = $volid_hash->{$volid};
+           $vm_volids->{$volid} = $info if $info->{vmid} && $info->{vmid} == $vmid;
+       }
+
+       my $changes = update_disk_config($vmid, $conf, $vm_volids);
+
+       PVE::QemuConfig->write_config($vmid, $conf) if $changes && !$dryrun;
+    };
+
+    if (defined($vmid)) {
+       if ($nolock) {
+           &$updatefn($vmid);
+       } else {
+           PVE::QemuConfig->lock_config($vmid, $updatefn, $vmid);
+       }
+    } else {
+       my $vmlist = config_list();
+       foreach my $vmid (keys %$vmlist) {
+           if ($nolock) {
+               &$updatefn($vmid);
+           } else {
+               PVE::QemuConfig->lock_config($vmid, $updatefn, $vmid);
+           }
+       }
+    }
+}
+
+sub restore_proxmox_backup_archive {
+    my ($archive, $vmid, $user, $options) = @_;
+
+    my $storecfg = PVE::Storage::config();
+
+    my ($storeid, $volname) = PVE::Storage::parse_volume_id($archive);
+    my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+
+    my $server = $scfg->{server};
+    my $datastore = $scfg->{datastore};
+    my $username = $scfg->{username} // 'root@pam';
+    my $fingerprint = $scfg->{fingerprint};
+    my $keyfile = PVE::Storage::PBSPlugin::pbs_encryption_key_file_name($storecfg, $storeid);
+
+    my $repo = "$username\@$server:$datastore";
+
+    # This is only used for `pbs-restore`!
+    my $password = PVE::Storage::PBSPlugin::pbs_get_password($scfg, $storeid);
+    local $ENV{PBS_PASSWORD} = $password;
+    local $ENV{PBS_FINGERPRINT} = $fingerprint if defined($fingerprint);
+
+    my ($vtype, $pbs_backup_name, undef, undef, undef, undef, $format) =
+       PVE::Storage::parse_volname($storecfg, $archive);
+
+    die "got unexpected vtype '$vtype'\n" if $vtype ne 'backup';
+
+    die "got unexpected backup format '$format'\n" if $format ne 'pbs-vm';
+
+    my $tmpdir = "/var/tmp/vzdumptmp$$";
+    rmtree $tmpdir;
+    mkpath $tmpdir;
+
+    my $conffile = PVE::QemuConfig->config_file($vmid);
+    my $tmpfn = "$conffile.$$.tmp";
+     # disable interrupts (always do cleanups)
+    local $SIG{INT} =
+       local $SIG{TERM} =
+       local $SIG{QUIT} =
+       local $SIG{HUP} = sub { print STDERR "got interrupt - ignored\n"; };
+
+    # Note: $oldconf is undef if VM does not exists
+    my $cfs_path = PVE::QemuConfig->cfs_config_path($vmid);
+    my $oldconf = PVE::Cluster::cfs_read_file($cfs_path);
+
+    my $rpcenv = PVE::RPCEnvironment::get();
+    my $devinfo = {};
+
+    eval {
+       # enable interrupts
+       local $SIG{INT} =
+           local $SIG{TERM} =
+           local $SIG{QUIT} =
+           local $SIG{HUP} =
+           local $SIG{PIPE} = sub { die "interrupted by signal\n"; };
+
+       my $cfgfn = "$tmpdir/qemu-server.conf";
+       my $firewall_config_fn = "$tmpdir/fw.conf";
+       my $index_fn = "$tmpdir/index.json";
+
+       my $cmd = "restore";
+
+       my $param = [$pbs_backup_name, "index.json", $index_fn];
+       PVE::Storage::PBSPlugin::run_raw_client_cmd($scfg, $storeid, $cmd, $param);
+       my $index = PVE::Tools::file_get_contents($index_fn);
+       $index = decode_json($index);
+
+       # print Dumper($index);
+       foreach my $info (@{$index->{files}}) {
+           if ($info->{filename} =~ m/^(drive-\S+).img.fidx$/) {
+               my $devname = $1;
+               if ($info->{size} =~ m/^(\d+)$/) { # untaint size
+                   $devinfo->{$devname}->{size} = $1;
+               } else {
+                   die "unable to parse file size in 'index.json' - got '$info->{size}'\n";
+               }
+           }
+       }
+
+       my $is_qemu_server_backup = scalar(
+           grep { $_->{filename} eq 'qemu-server.conf.blob' } @{$index->{files}}
+       );
+       if (!$is_qemu_server_backup) {
+           die "backup does not look like a qemu-server backup (missing 'qemu-server.conf' file)\n";
+       }
+       my $has_firewall_config = scalar(grep { $_->{filename} eq 'fw.conf.blob' } @{$index->{files}});
+
+       $param = [$pbs_backup_name, "qemu-server.conf", $cfgfn];
+       PVE::Storage::PBSPlugin::run_raw_client_cmd($scfg, $storeid, $cmd, $param);
+
+       if ($has_firewall_config) {
+           $param = [$pbs_backup_name, "fw.conf", $firewall_config_fn];
+           PVE::Storage::PBSPlugin::run_raw_client_cmd($scfg, $storeid, $cmd, $param);
+
+           my $pve_firewall_dir = '/etc/pve/firewall';
+           mkdir $pve_firewall_dir; # make sure the dir exists
+           PVE::Tools::file_copy($firewall_config_fn, "${pve_firewall_dir}/$vmid.fw");
+       }
+
+       my $fh = IO::File->new($cfgfn, "r") ||
+           die "unable to read qemu-server.conf - $!\n";
 
-    foreach my $volid (sort keys %$volid_hash) {
-       next if $volid =~ m/vm-$vmid-state-/;
-       next if $referenced->{$volid};
-       my $path = $volid_hash->{$volid}->{path};
-       next if !$path; # just to be sure
-       next if $referencedpath->{$path};
-       $changes = 1;
-       my $key = PVE::QemuConfig->add_unused_volume($conf, $volid);
-       print "$prefix add unreferenced volume '$volid' as '$key' to config.\n";
-       $referencedpath->{$path} = 1; # avoid to add more than once (aliases)
-    }
+       my $virtdev_hash = $parse_backup_hints->($rpcenv, $user, $storecfg, $fh, $devinfo, $options);
 
-    return $changes;
-}
+       # fixme: rate limit?
 
-sub rescan {
-    my ($vmid, $nolock, $dryrun) = @_;
+       # create empty/temp config
+       PVE::Tools::file_set_contents($conffile, "memory: 128\nlock: create");
 
-    my $cfg = PVE::Storage::config();
+       $restore_cleanup_oldconf->($storecfg, $vmid, $oldconf, $virtdev_hash) if $oldconf;
 
-    # FIXME: Remove once our RBD plugin can handle CT and VM on a single storage
-    # see: https://pve.proxmox.com/pipermail/pve-devel/2018-July/032900.html
-    foreach my $stor (keys %{$cfg->{ids}}) {
-       delete($cfg->{ids}->{$stor}) if ! $cfg->{ids}->{$stor}->{content}->{images};
-    }
+       # allocate volumes
+       my $map = $restore_allocate_devices->($storecfg, $virtdev_hash, $vmid);
 
-    print "rescan volumes...\n";
-    my $volid_hash = scan_volids($cfg, $vmid);
+       foreach my $virtdev (sort keys %$virtdev_hash) {
+           my $d = $virtdev_hash->{$virtdev};
+           next if $d->{is_cloudinit}; # no need to restore cloudinit
 
-    my $updatefn =  sub {
-       my ($vmid) = @_;
+           my $volid = $d->{volid};
 
-       my $conf = PVE::QemuConfig->load_config($vmid);
+           my $path = PVE::Storage::path($storecfg, $volid);
 
-       PVE::QemuConfig->check_lock($conf);
+           # This is the ONLY user of the PBS_ env vars set on top of this function!
+           my $pbs_restore_cmd = [
+               '/usr/bin/pbs-restore',
+               '--repository', $repo,
+               $pbs_backup_name,
+               "$d->{devname}.img.fidx",
+               $path,
+               '--verbose',
+               ];
 
-       my $vm_volids = {};
-       foreach my $volid (keys %$volid_hash) {
-           my $info = $volid_hash->{$volid};
-           $vm_volids->{$volid} = $info if $info->{vmid} && $info->{vmid} == $vmid;
+           push @$pbs_restore_cmd, '--format', $d->{format} if $d->{format};
+           push @$pbs_restore_cmd, '--keyfile', $keyfile if -e $keyfile;
+
+           if (PVE::Storage::volume_has_feature($storecfg, 'sparseinit', $volid)) {
+               push @$pbs_restore_cmd, '--skip-zero';
+           }
+
+           my $dbg_cmdstring = PVE::Tools::cmd2string($pbs_restore_cmd);
+           print "restore proxmox backup image: $dbg_cmdstring\n";
+           run_command($pbs_restore_cmd);
        }
 
-       my $changes = update_disksize($vmid, $conf, $vm_volids);
+       $fh->seek(0, 0) || die "seek failed - $!\n";
 
-       PVE::QemuConfig->write_config($vmid, $conf) if $changes && !$dryrun;
-    };
+       my $outfd = new IO::File ($tmpfn, "w") ||
+           die "unable to write config for VM $vmid\n";
 
-    if (defined($vmid)) {
-       if ($nolock) {
-           &$updatefn($vmid);
-       } else {
-           PVE::QemuConfig->lock_config($vmid, $updatefn, $vmid);
-       }
-    } else {
-       my $vmlist = config_list();
-       foreach my $vmid (keys %$vmlist) {
-           if ($nolock) {
-               &$updatefn($vmid);
-           } else {
-               PVE::QemuConfig->lock_config($vmid, $updatefn, $vmid);
-           }
+       my $cookie = { netcount => 0 };
+       while (defined(my $line = <$fh>)) {
+           $restore_update_config_line->($outfd, $cookie, $vmid, $map, $line, $options->{unique});
        }
+
+       $fh->close();
+       $outfd->close();
+    };
+    my $err = $@;
+
+    $restore_deactivate_volumes->($storecfg, $devinfo);
+
+    rmtree $tmpdir;
+
+    if ($err) {
+       unlink $tmpfn;
+       $restore_destroy_volumes->($storecfg, $devinfo);
+       die $err;
     }
+
+    rename($tmpfn, $conffile) ||
+       die "unable to commit configuration file '$conffile'\n";
+
+    PVE::Cluster::cfs_update(); # make sure we read new file
+
+    eval { rescan($vmid, 1); };
+    warn $@ if $@;
 }
 
 sub restore_vma_archive {
@@ -6324,14 +6278,9 @@ sub restore_vma_archive {
     }
 
     if ($comp) {
-       my $cmd;
-       if ($comp eq 'gzip') {
-           $cmd = ['zcat', $readfrom];
-       } elsif ($comp eq 'lzop') {
-           $cmd = ['lzop', '-d', '-c', $readfrom];
-       } else {
-           die "unknown compression method '$comp'\n";
-       }
+       my $info = PVE::Storage::decompressor_info('vma', $comp);
+       my $cmd = $info->{decompressor};
+       push @$cmd, $readfrom;
        $add_pipe->($cmd);
     }
 
@@ -6364,20 +6313,18 @@ sub restore_vma_archive {
     my $conffile = PVE::QemuConfig->config_file($vmid);
     my $tmpfn = "$conffile.$$.tmp";
 
-    # Note: $oldconf is undef if VM does not exists
+    # Note: $oldconf is undef if VM does not exist
     my $cfs_path = PVE::QemuConfig->cfs_config_path($vmid);
     my $oldconf = PVE::Cluster::cfs_read_file($cfs_path);
 
     my %storage_limits;
 
     my $print_devmap = sub {
-       my $virtdev_hash = {};
-
        my $cfgfn = "$tmpdir/qemu-server.conf";
 
        # we can read the config - that is already extracted
        my $fh = IO::File->new($cfgfn, "r") ||
-           "unable to read qemu-server.conf - $!\n";
+           die "unable to read qemu-server.conf - $!\n";
 
        my $fwcfgfn = "$tmpdir/qemu-server.fw";
        if (-f $fwcfgfn) {
@@ -6386,51 +6333,7 @@ sub restore_vma_archive {
            PVE::Tools::file_copy($fwcfgfn, "${pve_firewall_dir}/$vmid.fw");
        }
 
-       while (defined(my $line = <$fh>)) {
-           if ($line =~ m/^\#qmdump\#map:(\S+):(\S+):(\S*):(\S*):$/) {
-               my ($virtdev, $devname, $storeid, $format) = ($1, $2, $3, $4);
-               die "archive does not contain data for drive '$virtdev'\n"
-                   if !$devinfo->{$devname};
-               if (defined($opts->{storage})) {
-                   $storeid = $opts->{storage} || 'local';
-               } elsif (!$storeid) {
-                   $storeid = 'local';
-               }
-               $format = 'raw' if !$format;
-               $devinfo->{$devname}->{devname} = $devname;
-               $devinfo->{$devname}->{virtdev} = $virtdev;
-               $devinfo->{$devname}->{format} = $format;
-               $devinfo->{$devname}->{storeid} = $storeid;
-
-               # check permission on storage
-               my $pool = $opts->{pool}; # todo: do we need that?
-               if ($user ne 'root@pam') {
-                   $rpcenv->check($user, "/storage/$storeid", ['Datastore.AllocateSpace']);
-               }
-
-               $storage_limits{$storeid} = $bwlimit;
-
-               $virtdev_hash->{$virtdev} = $devinfo->{$devname};
-           } elsif ($line =~ m/^((?:ide|sata|scsi)\d+):\s*(.*)\s*$/) {
-               my $virtdev = $1;
-               my $drive = parse_drive($virtdev, $2);
-               if (drive_is_cloudinit($drive)) {
-                   my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file});
-                   my $scfg = PVE::Storage::storage_config($cfg, $storeid);
-                   my $format = qemu_img_format($scfg, $volname); # has 'raw' fallback
-
-                   my $d = {
-                       format => $format,
-                       storeid => $opts->{storage} // $storeid,
-                       size => PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE,
-                       file => $drive->{file}, # to make drive_is_cloudinit check possible
-                       name => "vm-$vmid-cloudinit",
-                       is_cloudinit => 1,
-                   };
-                   $virtdev_hash->{$virtdev} = $d;
-               }
-           }
-       }
+       my $virtdev_hash = $parse_backup_hints->($rpcenv, $user, $cfg, $fh, $devinfo, $opts);
 
        foreach my $key (keys %storage_limits) {
            my $limit = PVE::Storage::get_bandwidth_limit('restore', [$key], $bwlimit);
@@ -6447,81 +6350,35 @@ sub restore_vma_archive {
        # create empty/temp config
        if ($oldconf) {
            PVE::Tools::file_set_contents($conffile, "memory: 128\n");
-           foreach_drive($oldconf, sub {
-               my ($ds, $drive) = @_;
-
-               return if drive_is_cdrom($drive, 1);
-
-               my $volid = $drive->{file};
-               return if !$volid || $volid =~ m|^/|;
-
-               my ($path, $owner) = PVE::Storage::path($cfg, $volid);
-               return if !$path || !$owner || ($owner != $vmid);
-
-               # Note: only delete disk we want to restore
-               # other volumes will become unused
-               if ($virtdev_hash->{$ds}) {
-                   eval { PVE::Storage::vdisk_free($cfg, $volid); };
-                   if (my $err = $@) {
-                       warn $err;
-                   }
-               }
-           });
-
-           # delete vmstate files, after the restore we have no snapshots anymore
-           foreach my $snapname (keys %{$oldconf->{snapshots}}) {
-               my $snap = $oldconf->{snapshots}->{$snapname};
-               if ($snap->{vmstate}) {
-                   eval { PVE::Storage::vdisk_free($cfg, $snap->{vmstate}); };
-                   if (my $err = $@) {
-                       warn $err;
-                   }
-               }
-           }
+           $restore_cleanup_oldconf->($cfg, $vmid, $oldconf, $virtdev_hash);
        }
 
-       my $map = {};
+       # allocate volumes
+       my $map = $restore_allocate_devices->($cfg, $virtdev_hash, $vmid);
+
+       # print restore information to $fifofh
        foreach my $virtdev (sort keys %$virtdev_hash) {
            my $d = $virtdev_hash->{$virtdev};
-           my $alloc_size = int(($d->{size} + 1024 - 1)/1024);
+           next if $d->{is_cloudinit}; # no need to restore cloudinit
+
            my $storeid = $d->{storeid};
-           my $scfg = PVE::Storage::storage_config($cfg, $storeid);
+           my $volid = $d->{volid};
 
            my $map_opts = '';
            if (my $limit = $storage_limits{$storeid}) {
                $map_opts .= "throttling.bps=$limit:throttling.group=$storeid:";
            }
 
-           # test if requested format is supported
-           my ($defFormat, $validFormats) = PVE::Storage::storage_default_format($cfg, $storeid);
-           my $supported = grep { $_ eq $d->{format} } @$validFormats;
-           $d->{format} = $defFormat if !$supported;
-
-           my $name;
-           if ($d->{is_cloudinit}) {
-               $name = $d->{name};
-               $name .= ".$d->{format}" if $d->{format} ne 'raw';
-           }
-
-           my $volid = PVE::Storage::vdisk_alloc($cfg, $storeid, $vmid, $d->{format}, $name, $alloc_size);
-           print STDERR "new volume ID is '$volid'\n";
-           $d->{volid} = $volid;
-
-           PVE::Storage::activate_volumes($cfg, [$volid]);
-
            my $write_zeros = 1;
            if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', $volid)) {
                $write_zeros = 0;
            }
 
-           if (!$d->{is_cloudinit}) {
-               my $path = PVE::Storage::path($cfg, $volid);
+           my $path = PVE::Storage::path($cfg, $volid);
 
-               print $fifofh "${map_opts}format=$d->{format}:${write_zeros}:$d->{devname}=$path\n";
+           print $fifofh "${map_opts}format=$d->{format}:${write_zeros}:$d->{devname}=$path\n";
 
-               print "map '$d->{devname}' to '$path' (write zeros = ${write_zeros})\n";
-           }
-           $map->{$virtdev} = $volid;
+           print "map '$d->{devname}' to '$path' (write zeros = ${write_zeros})\n";
        }
 
        $fh->seek(0, 0) || die "seek failed - $!\n";
@@ -6531,7 +6388,7 @@ sub restore_vma_archive {
 
        my $cookie = { netcount => 0 };
        while (defined(my $line = <$fh>)) {
-           restore_update_config_line($outfd, $cookie, $vmid, $map, $line, $opts->{unique});
+           $restore_update_config_line->($outfd, $cookie, $vmid, $map, $line, $opts->{unique});
        }
 
        $fh->close();
@@ -6578,38 +6435,17 @@ sub restore_vma_archive {
 
     alarm($oldtimeout) if $oldtimeout;
 
-    my $vollist = [];
-    foreach my $devname (keys %$devinfo) {
-       my $volid = $devinfo->{$devname}->{volid};
-       push @$vollist, $volid if $volid;
-    }
-
-    PVE::Storage::deactivate_volumes($cfg, $vollist);
+    $restore_deactivate_volumes->($cfg, $devinfo);
 
     unlink $mapfifo;
+    rmtree $tmpdir;
 
     if ($err) {
-       rmtree $tmpdir;
        unlink $tmpfn;
-
-       foreach my $devname (keys %$devinfo) {
-           my $volid = $devinfo->{$devname}->{volid};
-           next if !$volid;
-           eval {
-               if ($volid =~ m|^/|) {
-                   unlink $volid || die 'unlink failed\n';
-               } else {
-                   PVE::Storage::vdisk_free($cfg, $volid);
-               }
-               print STDERR "temporary volume '$volid' sucessfuly removed\n";
-           };
-           print STDERR "unable to cleanup '$volid' - $@" if $@;
-       }
+       $restore_destroy_volumes->($cfg, $devinfo);
        die $err;
     }
 
-    rmtree $tmpdir;
-
     rename($tmpfn, $conffile) ||
        die "unable to commit configuration file '$conffile'\n";
 
@@ -6630,9 +6466,11 @@ sub restore_tar_archive {
 
     my $storecfg = PVE::Storage::config();
 
-    # destroy existing data - keep empty config
+    # avoid zombie disks when restoring over an existing VM -> cleanup first
+    # pass keep_empty_config=1 to keep the config (thus VMID) reserved for us
+    # skiplock=1 because qmrestore has set the 'create' lock itself already
     my $vmcfgfn = PVE::QemuConfig->config_file($vmid);
-    destroy_vm($storecfg, $vmid, 1) if -f $vmcfgfn;
+    destroy_vm($storecfg, $vmid, 1, { lock => 'restore' }) if -f $vmcfgfn;
 
     my $tocmd = "/usr/lib/qemu-server/qmextract";
 
@@ -6704,20 +6542,15 @@ sub restore_tar_archive {
 
        my $cookie = { netcount => 0 };
        while (defined (my $line = <$srcfd>)) {
-           restore_update_config_line($outfd, $cookie, $vmid, $map, $line, $opts->{unique});
+           $restore_update_config_line->($outfd, $cookie, $vmid, $map, $line, $opts->{unique});
        }
 
        $srcfd->close();
        $outfd->close();
     };
-    my $err = $@;
-
-    if ($err) {
-
+    if (my $err = $@) {
        unlink $tmpfn;
-
        tar_restore_cleanup($storecfg, "$tmpdir/qmrestore.stat") if !$opts->{info};
-
        die $err;
     }
 
@@ -6737,7 +6570,7 @@ sub foreach_storage_used_by_vm {
 
     my $sidhash = {};
 
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
        return if drive_is_cdrom($drive);
 
@@ -6752,6 +6585,9 @@ sub foreach_storage_used_by_vm {
     }
 }
 
+my $qemu_snap_storage = {
+    rbd => 1,
+};
 sub do_snapshots_with_qemu {
     my ($storecfg, $volid) = @_;
 
@@ -6772,7 +6608,7 @@ sub do_snapshots_with_qemu {
 sub qga_check_running {
     my ($vmid, $nowarn) = @_;
 
-    eval { vm_mon_cmd($vmid, "guest-ping", timeout => 3); };
+    eval { mon_cmd($vmid, "guest-ping", timeout => 3); };
     if ($@) {
        warn "Qemu Guest Agent is not running - $@" if !$nowarn;
        return 0;
@@ -6785,7 +6621,7 @@ sub template_create {
 
     my $storecfg = PVE::Storage::config();
 
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
 
        return if drive_is_cdrom($drive);
@@ -6796,7 +6632,7 @@ sub template_create {
 
        my $voliddst = PVE::Storage::vdisk_create_base($storecfg, $volid);
        $drive->{file} = $voliddst;
-       $conf->{$ds} = print_drive($vmid, $drive);
+       $conf->{$ds} = print_drive($drive);
        PVE::QemuConfig->write_config($vmid, $conf);
     });
 }
@@ -6830,7 +6666,7 @@ sub qemu_img_convert {
     my $cachemode;
     my $src_path;
     my $src_is_iscsi = 0;
-    my $src_format = 'raw';
+    my $src_format;
 
     if ($src_storeid) {
        PVE::Storage::activate_volumes($storecfg, [$src_volid], $snapname);
@@ -6841,7 +6677,7 @@ sub qemu_img_convert {
        $cachemode = 'none' if $src_scfg->{type} eq 'zfspool';
     } elsif (-f $src_volid) {
        $src_path = $src_volid;
-       if ($src_path =~ m/\.($QEMU_FORMAT_RE)$/) {
+       if ($src_path =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
            $src_format = $1;
        }
     }
@@ -6855,14 +6691,15 @@ sub qemu_img_convert {
 
     my $cmd = [];
     push @$cmd, '/usr/bin/qemu-img', 'convert', '-p', '-n';
-    push @$cmd, '-l', "snapshot.name=$snapname" if($snapname && $src_format eq "qcow2");
+    push @$cmd, '-l', "snapshot.name=$snapname"
+       if $snapname && $src_format && $src_format eq "qcow2";
     push @$cmd, '-t', 'none' if $dst_scfg->{type} eq 'zfspool';
     push @$cmd, '-T', $cachemode if defined($cachemode);
 
     if ($src_is_iscsi) {
        push @$cmd, '--image-opts';
        $src_path = convert_iscsi_path($src_path);
-    } else {
+    } elsif ($src_format) {
        push @$cmd, '-f', $src_format;
     }
 
@@ -6901,7 +6738,7 @@ sub qemu_img_convert {
 sub qemu_img_format {
     my ($scfg, $volname) = @_;
 
-    if ($scfg->{path} && $volname =~ m/\.($QEMU_FORMAT_RE)$/) {
+    if ($scfg->{path} && $volname =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
        return $1;
     } else {
        return "raw";
@@ -6909,7 +6746,7 @@ sub qemu_img_format {
 }
 
 sub qemu_drive_mirror {
-    my ($vmid, $drive, $dst_volid, $vmiddst, $is_zero_initialized, $jobs, $skipcomplete, $qga, $bwlimit) = @_;
+    my ($vmid, $drive, $dst_volid, $vmiddst, $is_zero_initialized, $jobs, $completion, $qga, $bwlimit, $src_bitmap) = @_;
 
     $jobs = {} if !$jobs;
 
@@ -6936,6 +6773,12 @@ sub qemu_drive_mirror {
     my $opts = { timeout => 10, device => "drive-$drive", mode => "existing", sync => "full", target => $qemu_target };
     $opts->{format} = $format if $format;
 
+    if (defined($src_bitmap)) {
+       $opts->{sync} = 'incremental';
+       $opts->{bitmap} = $src_bitmap;
+       print "drive mirror re-using dirty bitmap '$src_bitmap'\n";
+    }
+
     if (defined($bwlimit)) {
        $opts->{speed} = $bwlimit * 1024;
        print "drive mirror is starting for drive-$drive with bandwidth limit: ${bwlimit} KB/s\n";
@@ -6944,18 +6787,24 @@ sub qemu_drive_mirror {
     }
 
     # if a job already runs for this device we get an error, catch it for cleanup
-    eval { vm_mon_cmd($vmid, "drive-mirror", %$opts); };
+    eval { mon_cmd($vmid, "drive-mirror", %$opts); };
     if (my $err = $@) {
        eval { PVE::QemuServer::qemu_blockjobs_cancel($vmid, $jobs) };
        warn "$@\n" if $@;
        die "mirroring error: $err\n";
     }
 
-    qemu_drive_mirror_monitor ($vmid, $vmiddst, $jobs, $skipcomplete, $qga);
+    qemu_drive_mirror_monitor ($vmid, $vmiddst, $jobs, $completion, $qga);
 }
 
+# $completion can be either
+# 'complete': wait until all jobs are ready, block-job-complete them (default)
+# 'cancel': wait until all jobs are ready, block-job-cancel them
+# 'skip': wait until all jobs are ready, return with block jobs in ready state
 sub qemu_drive_mirror_monitor {
-    my ($vmid, $vmiddst, $jobs, $skipcomplete, $qga) = @_;
+    my ($vmid, $vmiddst, $jobs, $completion, $qga) = @_;
+
+    $completion //= 'complete';
 
     eval {
        my $err_complete = 0;
@@ -6963,7 +6812,7 @@ sub qemu_drive_mirror_monitor {
        while (1) {
            die "storage migration timed out\n" if $err_complete > 300;
 
-           my $stats = vm_mon_cmd($vmid, "query-block-jobs");
+           my $stats = mon_cmd($vmid, "query-block-jobs");
 
            my $running_mirror_jobs = {};
            foreach my $stat (@$stats) {
@@ -7000,13 +6849,13 @@ sub qemu_drive_mirror_monitor {
 
            if ($readycounter == scalar(keys %$jobs)) {
                print "all mirroring jobs are ready \n";
-               last if $skipcomplete; #do the complete later
+               last if $completion eq 'skip'; #do the complete later
 
                if ($vmiddst && $vmiddst != $vmid) {
                    my $agent_running = $qga && qga_check_running($vmid);
                    if ($agent_running) {
                        print "freeze filesystem\n";
-                       eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fsfreeze-freeze"); };
+                       eval { mon_cmd($vmid, "guest-fsfreeze-freeze"); };
                    } else {
                        print "suspend vm\n";
                        eval { PVE::QemuServer::vm_suspend($vmid, 1); };
@@ -7017,7 +6866,7 @@ sub qemu_drive_mirror_monitor {
 
                    if ($agent_running) {
                        print "unfreeze filesystem\n";
-                       eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fsfreeze-thaw"); };
+                       eval { mon_cmd($vmid, "guest-fsfreeze-thaw"); };
                    } else {
                        print "resume vm\n";
                        eval {  PVE::QemuServer::vm_resume($vmid, 1, 1); };
@@ -7030,7 +6879,15 @@ sub qemu_drive_mirror_monitor {
                        # try to switch the disk if source and destination are on the same guest
                        print "$job: Completing block job...\n";
 
-                       eval { vm_mon_cmd($vmid, "block-job-complete", device => $job) };
+                       my $op;
+                       if ($completion eq 'complete') {
+                           $op = 'block-job-complete';
+                       } elsif ($completion eq 'cancel') {
+                           $op = 'block-job-cancel';
+                       } else {
+                           die "invalid completion value: $completion\n";
+                       }
+                       eval { mon_cmd($vmid, $op, device => $job) };
                        if ($@ =~ m/cannot be completed/) {
                            print "$job: Block job cannot be completed, try again.\n";
                            $err_complete++;
@@ -7058,12 +6915,12 @@ sub qemu_blockjobs_cancel {
 
     foreach my $job (keys %$jobs) {
        print "$job: Cancelling block job\n";
-       eval { vm_mon_cmd($vmid, "block-job-cancel", device => $job); };
+       eval { mon_cmd($vmid, "block-job-cancel", device => $job); };
        $jobs->{$job}->{cancel} = 1;
     }
 
     while (1) {
-       my $stats = vm_mon_cmd($vmid, "query-block-jobs");
+       my $stats = mon_cmd($vmid, "query-block-jobs");
 
        my $running_jobs = {};
        foreach my $stat (@$stats) {
@@ -7086,7 +6943,7 @@ sub qemu_blockjobs_cancel {
 
 sub clone_disk {
     my ($storecfg, $vmid, $running, $drivename, $drive, $snapname,
-       $newvmid, $storage, $format, $full, $newvollist, $jobs, $skipcomplete, $qga, $bwlimit) = @_;
+       $newvmid, $storage, $format, $full, $newvollist, $jobs, $completion, $qga, $bwlimit, $conf) = @_;
 
     my $newvolid;
 
@@ -7104,27 +6961,52 @@ sub clone_disk {
 
        print "create full clone of drive $drivename ($drive->{file})\n";
        my $name = undef;
-       $newvolid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $newvmid, $dst_format, $name, ($size/1024));
+       if (drive_is_cloudinit($drive)) {
+           $name = "vm-$newvmid-cloudinit";
+           $name .= ".$dst_format" if $dst_format ne 'raw';
+           $snapname = undef;
+           $size = PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE;
+       } elsif ($drivename eq 'efidisk0') {
+           $size = get_efivars_size($conf);
+       }
+       $size /= 1024;
+       $newvolid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $newvmid, $dst_format, $name, $size);
        push @$newvollist, $newvolid;
 
        PVE::Storage::activate_volumes($storecfg, [$newvolid]);
 
+       if (drive_is_cloudinit($drive)) {
+           goto no_data_clone;
+       }
+
        my $sparseinit = PVE::Storage::volume_has_feature($storecfg, 'sparseinit', $newvolid);
        if (!$running || $snapname) {
            # TODO: handle bwlimits
-           qemu_img_convert($drive->{file}, $newvolid, $size, $snapname, $sparseinit);
+           if ($drivename eq 'efidisk0') {
+               # the relevant data on the efidisk may be smaller than the source
+               # e.g. on RBD/ZFS, so we use dd to copy only the amount
+               # that is given by the OVMF_VARS.fd
+               my $src_path = PVE::Storage::path($storecfg, $drive->{file});
+               my $dst_path = PVE::Storage::path($storecfg, $newvolid);
+               run_command(['qemu-img', 'dd', '-n', '-O', $dst_format, "bs=1", "count=$size",
+                   "if=$src_path", "of=$dst_path"]);
+           } else {
+               qemu_img_convert($drive->{file}, $newvolid, $size, $snapname, $sparseinit);
+           }
        } else {
 
            my $kvmver = get_running_qemu_version ($vmid);
-           if (!qemu_machine_feature_enabled (undef, $kvmver, 2, 7)) {
+           if (!min_version($kvmver, 2, 7)) {
                die "drive-mirror with iothread requires qemu version 2.7 or higher\n"
                    if $drive->{iothread};
            }
 
-           qemu_drive_mirror($vmid, $drivename, $newvolid, $newvmid, $sparseinit, $jobs, $skipcomplete, $qga, $bwlimit);
+           qemu_drive_mirror($vmid, $drivename, $newvolid, $newvmid, $sparseinit, $jobs,
+               $completion, $qga, $bwlimit);
        }
     }
 
+no_data_clone:
     my ($size) = PVE::Storage::volume_size_info($storecfg, $newvolid, 3);
 
     my $disk = $drive;
@@ -7135,64 +7017,12 @@ sub clone_disk {
     return $disk;
 }
 
-# this only works if VM is running
-sub get_current_qemu_machine {
-    my ($vmid) = @_;
-
-    my $cmd = { execute => 'query-machines', arguments => {} };
-    my $res = vm_qmp_command($vmid, $cmd);
-
-    my ($current, $default);
-    foreach my $e (@$res) {
-       $default = $e->{name} if $e->{'is-default'};
-       $current = $e->{name} if $e->{'is-current'};
-    }
-
-    # fallback to the default machine if current is not supported by qemu
-    return $current || $default || 'pc';
-}
-
 sub get_running_qemu_version {
     my ($vmid) = @_;
-    my $cmd = { execute => 'query-version', arguments => {} };
-    my $res = vm_qmp_command($vmid, $cmd);
+    my $res = mon_cmd($vmid, "query-version");
     return "$res->{qemu}->{major}.$res->{qemu}->{minor}";
 }
 
-sub qemu_machine_feature_enabled {
-    my ($machine, $kvmver, $version_major, $version_minor) = @_;
-
-    my $current_major;
-    my $current_minor;
-
-    if ($machine && $machine =~ m/^((?:pc(-i440fx|-q35)?|virt)-(\d+)\.(\d+))/) {
-
-       $current_major = $3;
-       $current_minor = $4;
-
-    } elsif ($kvmver =~ m/^(\d+)\.(\d+)/) {
-
-       $current_major = $1;
-       $current_minor = $2;
-    }
-
-    return 1 if $current_major > $version_major ||
-                ($current_major == $version_major &&
-                 $current_minor >= $version_minor);
-}
-
-sub qemu_machine_pxe {
-    my ($vmid, $conf) = @_;
-
-    my $machine =  PVE::QemuServer::get_current_qemu_machine($vmid);
-
-    if ($conf->{machine} && $conf->{machine} =~ m/\.pxe$/) {
-       $machine .= '.pxe';
-    }
-
-    return $machine;
-}
-
 sub qemu_use_old_bios_files {
     my ($machine_type) = @_;
 
@@ -7204,17 +7034,37 @@ sub qemu_use_old_bios_files {
         $machine_type = $1;
         $use_old_bios_files = 1;
     } else {
-       my $kvmver = kvm_user_version();
+       my $version = extract_version($machine_type, kvm_user_version());
         # Note: kvm version < 2.4 use non-efi pxe files, and have problems when we
         # load new efi bios files on migration. So this hack is required to allow
         # live migration from qemu-2.2 to qemu-2.4, which is sometimes used when
         # updrading from proxmox-ve-3.X to proxmox-ve 4.0
-       $use_old_bios_files = !qemu_machine_feature_enabled ($machine_type, $kvmver, 2, 4);
+       $use_old_bios_files = !min_version($version, 2, 4);
     }
 
     return ($use_old_bios_files, $machine_type);
 }
 
+sub get_efivars_size {
+    my ($conf) = @_;
+    my $arch = get_vm_arch($conf);
+    my (undef, $ovmf_vars) = get_ovmf_files($arch);
+    die "uefi vars image '$ovmf_vars' not found\n" if ! -f $ovmf_vars;
+    return -s $ovmf_vars;
+}
+
+sub update_efidisk_size {
+    my ($conf) = @_;
+
+    return if !defined($conf->{efidisk0});
+
+    my $disk = PVE::QemuServer::parse_drive('efidisk0', $conf->{efidisk0});
+    $disk->{size} = get_efivars_size($conf);
+    $conf->{efidisk0} = print_drive($disk);
+
+    return;
+}
+
 sub create_efidisk($$$$$) {
     my ($storecfg, $storeid, $vmid, $fmt, $arch) = @_;
 
@@ -7227,14 +7077,15 @@ sub create_efidisk($$$$$) {
     PVE::Storage::activate_volumes($storecfg, [$volid]);
 
     qemu_img_convert($ovmf_vars, $volid, $vars_size_b, undef, 0);
+    my ($size) = PVE::Storage::volume_size_info($storecfg, $volid, 3);
 
-    return ($volid, $vars_size);
+    return ($volid, $size/1024);
 }
 
 sub vm_iothreads_list {
     my ($vmid) = @_;
 
-    my $res = vm_mon_cmd($vmid, 'query-iothreads');
+    my $res = mon_cmd($vmid, 'query-iothreads');
 
     my $iothreads = {};
     foreach my $iothread (@$res) {
@@ -7258,50 +7109,13 @@ sub scsihw_infos {
     }
 
     my $controller = int($drive->{index} / $maxdev);
-    my $controller_prefix = ($conf->{scsihw} && $conf->{scsihw} eq 'virtio-scsi-single') ? "virtioscsi" : "scsihw";
+    my $controller_prefix = ($conf->{scsihw} && $conf->{scsihw} eq 'virtio-scsi-single')
+       ? "virtioscsi"
+       : "scsihw";
 
     return ($maxdev, $controller, $controller_prefix);
 }
 
-sub add_hyperv_enlightenments {
-    my ($cpuFlags, $winversion, $machine_type, $kvmver, $bios, $gpu_passthrough, $hv_vendor_id) = @_;
-
-    return if $winversion < 6;
-    return if $bios && $bios eq 'ovmf' && $winversion < 8;
-
-    if ($gpu_passthrough || defined($hv_vendor_id)) {
-       $hv_vendor_id //= 'proxmox';
-       push @$cpuFlags , "hv_vendor_id=$hv_vendor_id";
-    }
-
-    if (qemu_machine_feature_enabled ($machine_type, $kvmver, 2, 3)) {
-       push @$cpuFlags , 'hv_spinlocks=0x1fff';
-       push @$cpuFlags , 'hv_vapic';
-       push @$cpuFlags , 'hv_time';
-    } else {
-       push @$cpuFlags , 'hv_spinlocks=0xffff';
-    }
-
-    if (qemu_machine_feature_enabled ($machine_type, $kvmver, 2, 6)) {
-       push @$cpuFlags , 'hv_reset';
-       push @$cpuFlags , 'hv_vpindex';
-       push @$cpuFlags , 'hv_runtime';
-    }
-
-    if ($winversion >= 7) {
-       push @$cpuFlags , 'hv_relaxed';
-
-       if (qemu_machine_feature_enabled ($machine_type, $kvmver, 2, 12)) {
-           push @$cpuFlags , 'hv_synic';
-           push @$cpuFlags , 'hv_stimer';
-       }
-
-       if (qemu_machine_feature_enabled ($machine_type, $kvmver, 3, 1)) {
-           push @$cpuFlags , 'hv_ipi';
-       }
-    }
-}
-
 sub windows_version {
     my ($ostype) = @_;
 
@@ -7340,17 +7154,28 @@ sub resolve_dst_disk_format {
        return $format;
 }
 
-sub resolve_first_disk {
-    my $conf = shift;
-    my @disks = PVE::QemuServer::valid_drive_names();
-    my $firstdisk;
-    foreach my $ds (reverse @disks) {
-       next if !$conf->{$ds};
-       my $disk = PVE::QemuServer::parse_drive($ds, $conf->{$ds});
-       next if PVE::QemuServer::drive_is_cdrom($disk);
-       $firstdisk = $ds;
-    }
-    return $firstdisk;
+# NOTE: if this logic changes, please update docs & possibly gui logic
+sub find_vmstate_storage {
+    my ($conf, $storecfg) = @_;
+
+    # first, return storage from conf if set
+    return $conf->{vmstatestorage} if $conf->{vmstatestorage};
+
+    my ($target, $shared, $local);
+
+    foreach_storage_used_by_vm($conf, sub {
+       my ($sid) = @_;
+       my $scfg = PVE::Storage::storage_config($storecfg, $sid);
+       my $dst = $scfg->{shared} ? \$shared : \$local;
+       $$dst = $sid if !$$dst || $scfg->{path}; # prefer file based storage
+    });
+
+    # second, use shared storage where VM has at least one disk
+    # third, use local storage where VM has at least one disk
+    # fall back to local storage
+    $target = $shared // $local // 'local';
+
+    return $target;
 }
 
 sub generate_uuid {
@@ -7367,7 +7192,7 @@ sub generate_smbios1_uuid {
 sub nbd_stop {
     my ($vmid) = @_;
 
-    vm_mon_cmd($vmid, 'nbd-server-stop');
+    mon_cmd($vmid, 'nbd-server-stop');
 }
 
 sub create_reboot_request {
@@ -7389,6 +7214,74 @@ sub clear_reboot_request {
     return $res;
 }
 
+sub bootorder_from_legacy {
+    my ($conf, $bootcfg) = @_;
+
+    my $boot = $bootcfg->{legacy} || $boot_fmt->{legacy}->{default};
+    my $bootindex_hash = {};
+    my $i = 1;
+    foreach my $o (split(//, $boot)) {
+       $bootindex_hash->{$o} = $i*100;
+       $i++;
+    }
+
+    my $bootorder = {};
+
+    PVE::QemuConfig->foreach_volume($conf, sub {
+       my ($ds, $drive) = @_;
+
+       if (drive_is_cdrom ($drive, 1)) {
+           if ($bootindex_hash->{d}) {
+               $bootorder->{$ds} = $bootindex_hash->{d};
+               $bootindex_hash->{d} += 1;
+           }
+       } elsif ($bootindex_hash->{c}) {
+           $bootorder->{$ds} = $bootindex_hash->{c}
+               if $conf->{bootdisk} && $conf->{bootdisk} eq $ds;
+           $bootindex_hash->{c} += 1;
+       }
+    });
+
+    if ($bootindex_hash->{n}) {
+       for (my $i = 0; $i < $MAX_NETS; $i++) {
+           my $netname = "net$i";
+           next if !$conf->{$netname};
+           $bootorder->{$netname} = $bootindex_hash->{n};
+           $bootindex_hash->{n} += 1;
+       }
+    }
+
+    return $bootorder;
+}
+
+# Generate default device list for 'boot: order=' property. Matches legacy
+# default boot order, but with explicit device names. This is important, since
+# the fallback for when neither 'order' nor the old format is specified relies
+# on 'bootorder_from_legacy' above, and it would be confusing if this diverges.
+sub get_default_bootdevices {
+    my ($conf) = @_;
+
+    my @ret = ();
+
+    # harddisk
+    my $first = PVE::QemuServer::Drive::resolve_first_disk($conf, 0);
+    push @ret, $first if $first;
+
+    # cdrom
+    $first = PVE::QemuServer::Drive::resolve_first_disk($conf, 1);
+    push @ret, $first if $first;
+
+    # network
+    for (my $i = 0; $i < $MAX_NETS; $i++) {
+       my $netname = "net$i";
+       next if !$conf->{$netname};
+       push @ret, $netname;
+       last;
+    }
+
+    return \@ret;
+}
+
 # bash completion helper
 
 sub complete_backup_archives {
@@ -7407,7 +7300,7 @@ sub complete_backup_archives {
     my $res = [];
     foreach my $id (keys %$data) {
        foreach my $item (@{$data->{$id}}) {
-           next if $item->{format} !~ m/^vma\.(gz|lzo)$/;
+           next if $item->{format} !~ m/^vma\.(${\PVE::Storage::Plugin::COMPRESSOR_RE})$/;
            push @$res, $item->{volid} if defined($item->{volid});
        }
     }
@@ -7462,4 +7355,22 @@ sub complete_storage {
     return $res;
 }
 
+sub complete_migration_storage {
+    my ($cmd, $param, $current_value, $all_args) = @_;
+
+    my $targetnode = @$all_args[1];
+
+    my $cfg = PVE::Storage::config();
+    my $ids = $cfg->{ids};
+
+    my $res = [];
+    foreach my $sid (keys %$ids) {
+       next if !PVE::Storage::storage_check_enabled($cfg, $sid, $targetnode, 1);
+       next if !$ids->{$sid}->{content}->{images};
+       push @$res, $sid;
+    }
+
+    return $res;
+}
+
 1;