]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
parse_config: optional strict mode
[qemu-server.git] / PVE / QemuServer.pm
index 14a17af0adb5013425aa0333dc8711a2bca5bdcf..2b7ac40494645dab1b857088fd5f579141bd24dc 100644 (file)
@@ -64,6 +64,14 @@ eval {
 my $EDK2_FW_BASE = '/usr/share/pve-edk2-firmware/';
 my $OVMF = {
     x86_64 => {
+       '4m-no-smm' => [
+           "$EDK2_FW_BASE/OVMF_CODE_4M.fd",
+           "$EDK2_FW_BASE/OVMF_VARS_4M.fd",
+       ],
+       '4m-no-smm-ms' => [
+           "$EDK2_FW_BASE/OVMF_CODE_4M.fd",
+           "$EDK2_FW_BASE/OVMF_VARS_4M.ms.fd",
+       ],
        '4m' => [
            "$EDK2_FW_BASE/OVMF_CODE_4M.secboot.fd",
            "$EDK2_FW_BASE/OVMF_VARS_4M.fd",
@@ -111,25 +119,10 @@ PVE::JSONSchema::register_standard_option('pve-qemu-machine', {
        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',
+    format => 'storage-pair-list',
     optional => 1,
 });
 
@@ -278,6 +271,21 @@ my $rng_fmt = {
     },
 };
 
+my $meta_info_fmt = {
+    'ctime' => {
+       type => 'integer',
+       description => "The guest creation timestamp as UNIX epoch time",
+       minimum => 0,
+       optional => 1,
+    },
+    'creation-qemu' => {
+       type => 'string',
+       description => "The QEMU (machine) version from the time this VM was created.",
+       pattern => '\d+(\.\d+)+',
+       optional => 1,
+    },
+};
+
 my $confdesc = {
     onboot => {
        optional => 1,
@@ -699,6 +707,12 @@ EODESCR
        description => "Configure a VirtIO-based Random Number Generator.",
        optional => 1,
     },
+    meta => {
+       type => 'string',
+       format => $meta_info_fmt,
+       description => "Some (read-only) meta-information about this guest.",
+       optional => 1,
+    },
 };
 
 my $cicustom_fmt = {
@@ -726,6 +740,14 @@ my $cicustom_fmt = {
        format => 'pve-volume-id',
        format_description => 'volume',
     },
+    vendor => {
+    type => 'string',
+    optional => 1,
+    description => 'Specify a custom file containing all vendor data passed to the VM via'
+     .' cloud-init.',
+    format => 'pve-volume-id',
+    format_description => 'volume',
+    },
 };
 PVE::JSONSchema::register_format('pve-qm-cicustom', $cicustom_fmt);
 
@@ -892,13 +914,10 @@ my $net_fmt = {
         default_key => 1,
     },
     (map { $_ => { keyAlias => 'model', alias => 'macaddr' }} @$nic_model_list),
-    bridge => {
-       type => 'string',
+    bridge => get_standard_option('pve-bridge-id', {
        description => $net_fmt_bridge_descr,
-       format_description => 'bridge',
-       pattern => '[-_.\w\d]+',
        optional => 1,
-    },
+    }),
     queues => {
        type => 'integer',
        minimum => 0, maximum => 16,
@@ -1227,7 +1246,7 @@ sub kvm_user_version {
 my sub extract_version {
     my ($machine_type, $version) = @_;
     $version = kvm_user_version() if !defined($version);
-    PVE::QemuServer::Machine::extract_version($machine_type, $version)
+    return PVE::QemuServer::Machine::extract_version($machine_type, $version)
 }
 
 sub kernel_has_vhost_net {
@@ -1376,9 +1395,9 @@ sub scsi_inquiry {
     # see /usr/include/scsi/sg.h
     my $sg_io_hdr_t = "i i C C s I P P P I I i P C C C C S S i I I";
 
-    my $packet = pack($sg_io_hdr_t, ord('S'), -3, length($cmd),
-                     length($sensebuf), 0, length($buf), $buf,
-                     $cmd, $sensebuf, 6000);
+    my $packet = pack(
+       $sg_io_hdr_t, ord('S'), -3, length($cmd), length($sensebuf), 0, length($buf), $buf, $cmd, $sensebuf, 6000
+    );
 
     $ret = ioctl($fh, $SG_IO, $packet);
     if (!$ret) {
@@ -1393,11 +1412,10 @@ sub scsi_inquiry {
     }
 
     my $res = {};
-    (my $byte0, my $byte1, $res->{vendor},
-     $res->{product}, $res->{revision}) = unpack("C C x6 A8 A16 A4", $buf);
+    $res->@{qw(type removable vendor product revision)} = unpack("C C x6 A8 A16 A4", $buf);
 
-    $res->{removable} = $byte1 & 128 ? 1 : 0;
-    $res->{type} = $byte0 & 31;
+    $res->{removable} = $res->{removable} & 128 ? 1 : 0;
+    $res->{type} &= 0x1F;
 
     return $res;
 }
@@ -1429,7 +1447,7 @@ sub print_tabletdevice_full {
 }
 
 sub print_keyboarddevice_full {
-    my ($conf, $arch, $machine) = @_;
+    my ($conf, $arch) = @_;
 
     return if $arch ne 'aarch64';
 
@@ -1589,6 +1607,10 @@ sub print_drive_commandline_full {
        $opts .= ",snapshot=$v";
     }
 
+    if (defined($drive->{ro})) { # ro maps to QEMUs `readonly`, which accepts `on` or `off` only
+       $opts .= ",readonly=" . ($drive->{ro} ? 'on' : 'off');
+    }
+
     foreach my $type (['', '-total'], [_rd => '-read'], [_wr => '-write']) {
        my ($dir, $qmpname) = @$type;
        if (my $v = $drive->{"mbps$dir"}) {
@@ -1844,7 +1866,6 @@ sub print_vga_device {
     my $q35 = PVE::QemuServer::Machine::machine_type_is_q35($conf);
     my $vgaid = "vga" . ($id // '');
     my $pciaddr;
-
     if ($q35 && $vgaid eq 'vga') {
        # the first display uses pcie.0 bus on q35 machines
        $pciaddr = print_pcie_addr($vgaid, $bridges, $arch, $machine);
@@ -2097,6 +2118,53 @@ sub parse_rng {
     return $res;
 }
 
+sub parse_meta_info {
+    my ($value) = @_;
+
+    return if !$value;
+
+    my $res = eval { parse_property_string($meta_info_fmt, $value) };
+    warn $@ if $@;
+    return $res;
+}
+
+sub new_meta_info_string {
+    my () = @_; # for now do not allow to override any value
+
+    return PVE::JSONSchema::print_property_string(
+       {
+           'creation-qemu' => kvm_user_version(),
+           ctime => "". int(time()),
+       },
+       $meta_info_fmt
+    );
+}
+
+sub qemu_created_version_fixups {
+    my ($conf, $forcemachine, $kvmver) = @_;
+
+    my $meta = parse_meta_info($conf->{meta}) // {};
+    my $forced_vers = PVE::QemuServer::Machine::extract_version($forcemachine);
+
+    # check if we need to apply some handling for VMs that always use the latest machine version but
+    # had a machine version transition happen that affected HW such that, e.g., an OS config change
+    # would be required (we do not want to pin machine version for non-windows OS type)
+    if (
+       (!defined($conf->{machine}) || $conf->{machine} =~ m/^(?:pc|q35|virt)$/) # non-versioned machine
+       && (!defined($meta->{'creation-qemu'}) || !min_version($meta->{'creation-qemu'}, 6, 1)) # created before 6.1
+       && (!$forced_vers || min_version($forced_vers, 6, 1)) # handle snapshot-rollback/migrations
+       && min_version($kvmver, 6, 1) # only need to apply the change since 6.1
+    ) {
+       my $q35 = PVE::QemuServer::Machine::machine_type_is_q35($conf);
+       if ($q35 && $conf->{ostype} && $conf->{ostype} eq 'l26') {
+           # this changed to default-on in Q 6.1 for q35 machines, it will mess with PCI slot view
+           # and thus with the predictable interface naming of systemd
+           return ['-global', 'ICH9-LPC.acpi-pci-hotplug-with-bridge-support=off'];
+       }
+    }
+    return;
+}
+
 PVE::JSONSchema::register_format('pve-qm-usb-device', \&verify_usb_device);
 sub verify_usb_device {
     my ($value, $noerr) = @_;
@@ -2112,9 +2180,17 @@ sub verify_usb_device {
 sub json_config_properties {
     my $prop = shift;
 
+    my $skip_json_config_opts = {
+       parent => 1,
+       snaptime => 1,
+       vmstate => 1,
+       runningmachine => 1,
+       runningcpu => 1,
+       meta => 1,
+    };
+
     foreach my $opt (keys %$confdesc) {
-       next if $opt eq 'parent' || $opt eq 'snaptime' || $opt eq 'vmstate' ||
-           $opt eq 'runningmachine' || $opt eq 'runningcpu';
+       next if $skip_json_config_opts->{$opt};
        $prop->{$opt} = $confdesc->{$opt};
     }
 
@@ -2236,7 +2312,7 @@ sub destroy_vm {
 }
 
 sub parse_vm_config {
-    my ($filename, $raw) = @_;
+    my ($filename, $raw, $strict) = @_;
 
     return if !defined($raw);
 
@@ -2246,6 +2322,16 @@ sub parse_vm_config {
        pending => {},
     };
 
+    my $handle_error = sub {
+       my ($msg) = @_;
+
+       if ($strict) {
+           die $msg;
+       } else {
+           warn $msg;
+       }
+    };
+
     $filename =~ m|/qemu-server/(\d+)\.conf$|
        || die "got strange filename '$filename'";
 
@@ -2300,14 +2386,14 @@ sub parse_vm_config {
            if ($section eq 'pending') {
                $conf->{delete} = $value; # we parse this later
            } else {
-               warn "vm $vmid - propertry 'delete' is only allowed in [PENDING]\n";
+               $handle_error->("vm $vmid - property 'delete' is only allowed in [PENDING]\n");
            }
        } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) {
            my $key = $1;
            my $value = $2;
            eval { $value = check_type($key, $value); };
            if ($@) {
-               warn "vm $vmid - unable to parse value of '$key' - $@";
+               $handle_error->("vm $vmid - unable to parse value of '$key' - $@");
            } else {
                $key = 'ide2' if $key eq 'cdrom';
                my $fmt = $confdesc->{$key}->{format};
@@ -2317,7 +2403,7 @@ sub parse_vm_config {
                        $v->{file} = $volid;
                        $value = print_drive($v);
                    } else {
-                       warn "vm $vmid - unable to parse value of '$key'\n";
+                       $handle_error->("vm $vmid - unable to parse value of '$key'\n");
                        next;
                    }
                }
@@ -2325,7 +2411,7 @@ sub parse_vm_config {
                $conf->{$key} = $value;
            }
        } else {
-           warn "vm $vmid - unable to parse config: $line\n";
+           $handle_error->("vm $vmid - unable to parse config: $line\n");
        }
     }
 
@@ -3159,15 +3245,16 @@ sub get_vm_machine {
     return $machine;
 }
 
-sub get_ovmf_files($$) {
-    my ($arch, $efidisk) = @_;
+sub get_ovmf_files($$$) {
+    my ($arch, $efidisk, $smm) = @_;
 
     my $types = $OVMF->{$arch}
        or die "no OVMF images known for architecture '$arch'\n";
 
     my $type = 'default';
     if (defined($efidisk->{efitype}) && $efidisk->{efitype} eq '4m') {
-       $type = $efidisk->{'pre-enrolled-keys'} ? "4m-ms" : "4m";
+       $type = $smm ? "4m" : "4m-no-smm";
+       $type .= '-ms' if $efidisk->{'pre-enrolled-keys'};
     }
 
     return $types->{$type}->@*;
@@ -3308,16 +3395,23 @@ my sub get_cpuunits {
     my ($conf) = @_;
     return $conf->{cpuunits} // (PVE::CGroup::cgroup_mode() == 2 ? 100 : 1024);
 }
+
+# Since commit 277d33454f77ec1d1e0bc04e37621e4dd2424b67 in pve-qemu, smm is not off by default
+# anymore. But smm=off seems to be required when using SeaBIOS and serial display.
+my sub should_disable_smm {
+    my ($conf, $vga) = @_;
+
+    return (!defined($conf->{bios}) || $conf->{bios} eq 'seabios') &&
+       $vga->{type} && $vga->{type} =~ m/^(serial\d+|none)$/;
+}
+
 sub config_to_command {
     my ($storecfg, $vmid, $conf, $defaults, $forcemachine, $forcecpu,
         $pbs_backing) = @_;
 
     my $cmd = [];
-    my $globalFlags = [];
-    my $machineFlags = [];
-    my $rtcFlags = [];
+    my ($globalFlags, $machineFlags, $rtcFlags) = ([], [], []);
     my $devices = [];
-    my $pciaddr = '';
     my $bridges = {};
     my $ostype = $conf->{ostype};
     my $winversion = windows_version($ostype);
@@ -3433,7 +3527,7 @@ sub config_to_command {
            $d = parse_drive('efidisk0', $efidisk);
        }
 
-       my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch, $d);
+       my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch, $d, $q35);
        die "uefi base image '$ovmf_code' not found\n" if ! -f $ovmf_code;
 
        my ($path, $format);
@@ -3487,6 +3581,10 @@ sub config_to_command {
        }
     }
 
+    if (defined(my $fixups = qemu_created_version_fixups($conf, $forcemachine, $kvmver))) {
+       push @$cmd, $fixups->@*;
+    }
+
     if ($conf->{vmgenid}) {
        push @$devices, '-device', 'vmgenid,guid='.$conf->{vmgenid};
     }
@@ -3511,10 +3609,8 @@ sub config_to_command {
     }
 
     # enable absolute mouse coordinates (needed by vnc)
-    my $tablet;
-    if (defined($conf->{tablet})) {
-       $tablet = $conf->{tablet};
-    } else {
+    my $tablet = $conf->{tablet};
+    if (!defined($tablet)) {
        $tablet = $defaults->{tablet};
        $tablet = 0 if $qxlnum; # disable for spice because it is not needed
        $tablet = 0 if $vga->{type} =~ m/^serial\d+$/; # disable if we use serial terminal (no vga card)
@@ -3542,23 +3638,22 @@ sub config_to_command {
 
     # serial devices
     for (my $i = 0; $i < $MAX_SERIAL_PORTS; $i++)  {
-       if (my $path = $conf->{"serial$i"}) {
-           if ($path eq 'socket') {
-               my $socket = "/var/run/qemu-server/${vmid}.serial$i";
-               push @$devices, '-chardev', "socket,id=serial$i,path=$socket,server=on,wait=off";
-               # On aarch64, serial0 is the UART device. Qemu only allows
-               # connecting UART devices via the '-serial' command line, as
-               # the device has a fixed slot on the hardware...
-               if ($arch eq 'aarch64' && $i == 0) {
-                   push @$devices, '-serial', "chardev:serial$i";
-               } else {
-                   push @$devices, '-device', "isa-serial,chardev=serial$i";
-               }
+       my $path = $conf->{"serial$i"} or next;
+       if ($path eq 'socket') {
+           my $socket = "/var/run/qemu-server/${vmid}.serial$i";
+           push @$devices, '-chardev', "socket,id=serial$i,path=$socket,server=on,wait=off";
+           # On aarch64, serial0 is the UART device. Qemu only allows
+           # connecting UART devices via the '-serial' command line, as
+           # the device has a fixed slot on the hardware...
+           if ($arch eq 'aarch64' && $i == 0) {
+               push @$devices, '-serial', "chardev:serial$i";
            } else {
-               die "no such serial device\n" if ! -c $path;
-               push @$devices, '-chardev', "tty,id=serial$i,path=$path";
                push @$devices, '-device', "isa-serial,chardev=serial$i";
            }
+       } else {
+           die "no such serial device\n" if ! -c $path;
+           push @$devices, '-chardev', "tty,id=serial$i,path=$path";
+           push @$devices, '-device', "isa-serial,chardev=serial$i";
        }
     }
 
@@ -3742,13 +3837,13 @@ sub config_to_command {
 
     # enable balloon by default, unless explicitly disabled
     if (!defined($conf->{balloon}) || $conf->{balloon}) {
-       $pciaddr = print_pci_addr("balloon0", $bridges, $arch, $machine_type);
+       my $pciaddr = print_pci_addr("balloon0", $bridges, $arch, $machine_type);
        push @$devices, '-device', "virtio-balloon-pci,id=balloon0$pciaddr";
     }
 
     if ($conf->{watchdog}) {
        my $wdopts = parse_watchdog($conf->{watchdog});
-       $pciaddr = print_pci_addr("watchdog", $bridges, $arch, $machine_type);
+       my $pciaddr = print_pci_addr("watchdog", $bridges, $arch, $machine_type);
        my $watchdog = $wdopts->{model} || 'i6300esb';
        push @$devices, '-device', "$watchdog$pciaddr";
        push @$devices, '-watchdog-action', $wdopts->{action} if $wdopts->{action};
@@ -3792,7 +3887,7 @@ sub config_to_command {
            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 $pciaddr = print_pci_addr("$controller_prefix$controller", $bridges, $arch, $machine_type);
            my $scsihw_type = $scsihw =~ m/^virtio-scsi-single/ ? "virtio-scsi-pci" : $scsihw;
 
            my $iothread = '';
@@ -3815,7 +3910,7 @@ sub config_to_command {
 
         if ($drive->{interface} eq 'sata') {
            my $controller = int($drive->{index} / $PVE::QemuServer::Drive::MAX_SATA_DISKS);
-           $pciaddr = print_pci_addr("ahci$controller", $bridges, $arch, $machine_type);
+           my $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;
@@ -3880,15 +3975,12 @@ sub config_to_command {
     # pci.4 is nested in pci.1
     $bridges->{1} = 1 if $bridges->{4};
 
-    if (!$q35) {
-       # add pci bridges
-        if (min_version($machine_version, 2, 3)) {
+    if (!$q35) { # add pci bridges
+       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) {
@@ -3898,11 +3990,10 @@ sub config_to_command {
        if ($k == 2 && $legacy_igd) {
            $k_name = "$k-igd";
        }
-       $pciaddr = print_pci_addr("pci.$k_name", undef, $arch, $machine_type);
-
+       my $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
+
+       if ($q35) { # add after -readconfig pve-q35.cfg
            splice @$devices, 2, 0, '-device', $devstr;
        } else {
            unshift @$devices, '-device', $devstr if $k > 0;
@@ -3913,6 +4004,8 @@ sub config_to_command {
        push @$machineFlags, 'accel=tcg';
     }
 
+    push @$machineFlags, 'smm=off' if should_disable_smm($conf, $vga);
+
     my $machine_type_min = $machine_type;
     if ($add_pve_version) {
        $machine_type_min =~ s/\+pve\d+$//;
@@ -4033,43 +4126,33 @@ sub vm_deviceplug {
     qemu_add_pci_bridge($storecfg, $conf, $vmid, $deviceid, $arch, $machine_type);
 
     if ($deviceid eq 'tablet') {
-
        qemu_deviceadd($vmid, print_tabletdevice_full($conf, $arch));
-
     } elsif ($deviceid eq 'keyboard') {
-
        qemu_deviceadd($vmid, print_keyboarddevice_full($conf, $arch));
-
     } elsif ($deviceid =~ m/^usb(\d+)$/) {
-
        die "usb hotplug currently not reliable\n";
        # since we can't reliably hot unplug all added usb devices and usb
        # passthrough breaks live migration we disable usb hotplugging for now
        #qemu_deviceadd($vmid, PVE::QemuServer::USB::print_usbdevice_full($conf, $deviceid, $device));
-
     } elsif ($deviceid =~ m/^(virtio)(\d+)$/) {
-
        qemu_iothread_add($vmid, $deviceid, $device);
 
-        qemu_driveadd($storecfg, $vmid, $device);
-        my $devicefull = print_drivedevice_full($storecfg, $conf, $vmid, $device, undef, $arch, $machine_type);
+       qemu_driveadd($storecfg, $vmid, $device);
+       my $devicefull = print_drivedevice_full($storecfg, $conf, $vmid, $device, undef, $arch, $machine_type);
 
-        qemu_deviceadd($vmid, $devicefull);
+       qemu_deviceadd($vmid, $devicefull);
        eval { qemu_deviceaddverify($vmid, $deviceid); };
        if (my $err = $@) {
            eval { qemu_drivedel($vmid, $deviceid); };
            warn $@ if $@;
            die $err;
         }
-
     } elsif ($deviceid =~ m/^(virtioscsi|scsihw)(\d+)$/) {
-
-
-        my $scsihw = defined($conf->{scsihw}) ? $conf->{scsihw} : "lsi";
-        my $pciaddr = print_pci_addr($deviceid, undef, $arch, $machine_type);
+       my $scsihw = defined($conf->{scsihw}) ? $conf->{scsihw} : "lsi";
+       my $pciaddr = print_pci_addr($deviceid, undef, $arch, $machine_type);
        my $scsihw_type = $scsihw eq 'virtio-scsi-single' ? "virtio-scsi-pci" : $scsihw;
 
-        my $devicefull = "$scsihw_type,id=$deviceid$pciaddr";
+       my $devicefull = "$scsihw_type,id=$deviceid$pciaddr";
 
        if($deviceid =~ m/^virtioscsi(\d+)$/ && $device->{iothread}) {
            qemu_iothread_add($vmid, $deviceid, $device);
@@ -4080,11 +4163,9 @@ sub vm_deviceplug {
            $devicefull .= ",num_queues=$device->{queues}";
        }
 
-        qemu_deviceadd($vmid, $devicefull);
-        qemu_deviceaddverify($vmid, $deviceid);
-
+       qemu_deviceadd($vmid, $devicefull);
+       qemu_deviceaddverify($vmid, $deviceid);
     } elsif ($deviceid =~ m/^(scsi)(\d+)$/) {
-
         qemu_findorcreatescsihw($storecfg,$conf, $vmid, $device, $arch, $machine_type);
         qemu_driveadd($storecfg, $vmid, $device);
 
@@ -4095,9 +4176,7 @@ sub vm_deviceplug {
            warn $@ if $@;
            die $err;
         }
-
     } elsif ($deviceid =~ m/^(net)(\d+)$/) {
-
        return if !qemu_netdevadd($vmid, $conf, $arch, $device, $deviceid);
 
        my $machine_type = PVE::QemuServer::Machine::qemu_machine_pxe($vmid, $conf);
@@ -4116,16 +4195,13 @@ sub vm_deviceplug {
            warn $@ if $@;
            die $err;
        }
-
     } elsif (!$q35 && $deviceid =~ m/^(pci\.)(\d+)$/) {
-
        my $bridgeid = $2;
        my $pciaddr = print_pci_addr($deviceid, undef, $arch, $machine_type);
        my $devicefull = "pci-bridge,id=pci.$bridgeid,chassis_nr=$bridgeid$pciaddr";
 
        qemu_deviceadd($vmid, $devicefull);
        qemu_deviceaddverify($vmid, $deviceid);
-
     } else {
        die "can't hotplug device '$deviceid'\n";
     }
@@ -4757,7 +4833,7 @@ sub vmconfig_hotplug_pending {
            } elsif ($opt eq 'cpuunits') {
                $cgroup->change_cpu_shares(undef, 1024);
            } elsif ($opt eq 'cpulimit') {
-               $cgroup->change_cpu_quota(-1, 100000);
+               $cgroup->change_cpu_quota(undef, undef); # reset, cgroup module can better decide values
            } else {
                die "skip\n";
            }
@@ -4853,6 +4929,8 @@ sub vmconfig_hotplug_pending {
            } elsif ($opt eq 'cpulimit') {
                my $cpulimit = $conf->{pending}->{$opt} == 0 ? -1 : int($conf->{pending}->{$opt} * 100000);
                $cgroup->change_cpu_quota($cpulimit, 100000);
+           } elsif ($opt eq 'agent') {
+               vmconfig_update_agent($conf, $opt, $value);
            } else {
                die "skip\n";  # skip non-hot-pluggable options
            }
@@ -4912,6 +4990,8 @@ sub vmconfig_delete_or_detach_drive {
 sub vmconfig_apply_pending {
     my ($vmid, $conf, $storecfg, $errors) = @_;
 
+    return if !scalar(keys %{$conf->{pending}});
+
     my $add_apply_error = sub {
        my ($opt, $msg) = @_;
        my $err_msg = "unable to apply pending change $opt : $msg";
@@ -5012,6 +5092,29 @@ sub vmconfig_update_net {
     }
 }
 
+sub vmconfig_update_agent {
+    my ($conf, $opt, $value) = @_;
+
+    die "skip\n" if !$conf->{$opt};
+
+    my $hotplug_options = { fstrim_cloned_disks => 1 };
+
+    my $old_agent = parse_guest_agent($conf);
+    my $agent = parse_guest_agent({$opt => $value});
+
+    for my $option (keys %$agent) { # added/changed options
+       next if defined($hotplug_options->{$option});
+       die "skip\n" if safe_string_ne($agent->{$option}, $old_agent->{$option});
+    }
+
+    for my $option (keys %$old_agent) { # removed options
+       next if defined($hotplug_options->{$option});
+       die "skip\n" if safe_string_ne($old_agent->{$option}, $agent->{$option});
+    }
+
+    return; # either no actual change (e.g., format string reordered) or just hotpluggable changes
+}
+
 sub vmconfig_update_disk {
     my ($storecfg, $conf, $hotplug, $vmid, $opt, $value, $arch, $machine_type) = @_;
 
@@ -5129,6 +5232,7 @@ sub vm_migrate_get_nbd_disks {
        my ($ds, $drive) = @_;
 
        return if drive_is_cdrom($drive);
+       return if $ds eq 'tpmstate0';
 
        my $volid = $drive->{file};
 
@@ -5167,7 +5271,7 @@ sub vm_migrate_alloc_nbd_disks {
        # 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);
+           $storeid = PVE::JSONSchema::map_id($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);
@@ -5260,7 +5364,8 @@ sub vm_start {
 #   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
+#   replicated_volumes => which volids should be re-used with bitmaps for nbd migration
+#   tpmstate_vol => new volid of tpmstate0, not yet contained in config
 sub vm_start_nolock {
     my ($storecfg, $vmid, $conf, $params, $migrate_opts) = @_;
 
@@ -5285,6 +5390,13 @@ sub vm_start_nolock {
     # this way we can reuse the old ISO with the correct config
     PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid) if !$migratedfrom;
 
+    # override TPM state vol if migrated, conf is out of date still
+    if (my $tpmvol = $migrate_opts->{tpmstate_vol}) {
+        my $parsed = parse_drive("tpmstate0", $conf->{tpmstate0});
+        $parsed->{file} = $tpmvol;
+        $conf->{tpmstate0} = print_drive($parsed);
+    }
+
     my $defaults = load_defaults();
 
     # set environment variable useful inside network script
@@ -5392,7 +5504,12 @@ sub vm_start_nolock {
        $pci_devices->{$i} = parse_hostpci($dev);
     }
 
-    my $pci_id_list = [ map { $_->{id} } map { $_->{pciid}->@* } values $pci_devices->%* ];
+    # do not reserve pciid for mediated devices, sysfs will error out for duplicate assignment
+    my $real_pci_devices = [ grep { !(defined($_->{mdev}) && scalar($_->{pciid}->@*) == 1) } values $pci_devices->%* ];
+
+    # map to a flat list of pci ids
+    my $pci_id_list = [ map { $_->{id} } map { $_->{pciid}->@* } $real_pci_devices->@* ];
+
     # reserve all PCI IDs before actually doing anything with them
     PVE::QemuServer::PCI::reserve_pci_usage($pci_id_list, $vmid, $start_timeout);
 
@@ -5465,8 +5582,10 @@ sub vm_start_nolock {
 
            my $exitcode = run_command($cmd, %run_params);
            if ($exitcode) {
-               warn "stopping swtpm instance (pid $tpmpid) due to QEMU startup error\n";
-               kill 'TERM', $tpmpid if $tpmpid;
+               if ($tpmpid) {
+                   warn "stopping swtpm instance (pid $tpmpid) due to QEMU startup error\n";
+                   kill 'TERM', $tpmpid;
+               }
                die "QEMU exited with code $exitcode\n";
            }
        };
@@ -5612,9 +5731,8 @@ sub vm_commandline {
     my ($storecfg, $vmid, $snapname) = @_;
 
     my $conf = PVE::QemuConfig->load_config($vmid);
-    my $forcemachine;
-    my $forcecpu;
 
+    my ($forcemachine, $forcecpu);
     if ($snapname) {
        my $snapshot = $conf->{snapshots}->{$snapname};
        die "snapshot '$snapname' does not exist\n" if !defined($snapshot);
@@ -5630,8 +5748,7 @@ sub vm_commandline {
 
     my $defaults = load_defaults();
 
-    my $cmd = config_to_command($storecfg, $vmid, $conf, $defaults,
-       $forcemachine, $forcecpu);
+    my $cmd = config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine, $forcecpu);
 
     return PVE::Tools::cmd2string($cmd);
 }
@@ -7341,9 +7458,11 @@ sub qemu_drive_mirror_monitor {
                    if ($agent_running) {
                        print "freeze filesystem\n";
                        eval { mon_cmd($vmid, "guest-fsfreeze-freeze"); };
+                       warn $@ if $@;
                    } else {
                        print "suspend vm\n";
                        eval { PVE::QemuServer::vm_suspend($vmid, 1); };
+                       warn $@ if $@;
                    }
 
                    # if we clone a disk for a new target vm, we don't switch the disk
@@ -7352,9 +7471,11 @@ sub qemu_drive_mirror_monitor {
                    if ($agent_running) {
                        print "unfreeze filesystem\n";
                        eval { mon_cmd($vmid, "guest-fsfreeze-thaw"); };
+                       warn $@ if $@;
                    } else {
                        print "resume vm\n";
-                       eval {  PVE::QemuServer::vm_resume($vmid, 1, 1); };
+                       eval { PVE::QemuServer::vm_resume($vmid, 1, 1); };
+                       warn $@ if $@;
                    }
 
                    last;
@@ -7513,8 +7634,8 @@ sub clone_disk {
 no_data_clone:
     my ($size) = eval { PVE::Storage::volume_size_info($storecfg, $newvolid, 10) };
 
-    my $disk = $drive;
-    $disk->{format} = undef;
+    my $disk = dclone($drive);
+    delete $disk->{format};
     $disk->{file} = $newvolid;
     $disk->{size} = $size if defined($size);
 
@@ -7553,7 +7674,8 @@ sub get_efivars_size {
     my ($conf) = @_;
     my $arch = get_vm_arch($conf);
     my $efidisk = $conf->{efidisk0} ? parse_drive('efidisk0', $conf->{efidisk0}) : undef;
-    my (undef, $ovmf_vars) = get_ovmf_files($arch, $efidisk);
+    my $smm = PVE::QemuServer::Machine::machine_type_is_q35($conf);
+    my (undef, $ovmf_vars) = get_ovmf_files($arch, $efidisk, $smm);
     die "uefi vars image '$ovmf_vars' not found\n" if ! -f $ovmf_vars;
     return -s $ovmf_vars;
 }
@@ -7578,10 +7700,10 @@ sub update_tpmstate_size {
     $conf->{tpmstate0} = print_drive($disk);
 }
 
-sub create_efidisk($$$$$$) {
-    my ($storecfg, $storeid, $vmid, $fmt, $arch, $efidisk) = @_;
+sub create_efidisk($$$$$$$) {
+    my ($storecfg, $storeid, $vmid, $fmt, $arch, $efidisk, $smm) = @_;
 
-    my (undef, $ovmf_vars) = get_ovmf_files($arch, $efidisk);
+    my (undef, $ovmf_vars) = get_ovmf_files($arch, $efidisk, $smm);
     die "EFI vars default image not found\n" if ! -f $ovmf_vars;
 
     my $vars_size_b = -s $ovmf_vars;