]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
fix #4435: devices list: avoid error for undefined value
[qemu-server.git] / PVE / QemuServer.pm
index ec1cc78af1cb567f1e652eb75bf6e9242a9979fd..4292da74c8bb99fe794e619ed9b09a26aeb0d3fd 100644 (file)
@@ -102,9 +102,11 @@ my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
 # 'backup', 'snapshot' or 'rollback'. Most actions are not allowed when such lock is set.
 # But you can ignore this kind of lock with the --skiplock flag.
 
-cfs_register_file('/qemu-server/',
-                 \&parse_vm_config,
-                 \&write_vm_config);
+cfs_register_file(
+    '/qemu-server/',
+    \&parse_vm_config,
+    \&write_vm_config
+);
 
 PVE::JSONSchema::register_standard_option('pve-qm-stateuri', {
     description => "Some command save/restore state from this location.",
@@ -114,15 +116,14 @@ PVE::JSONSchema::register_standard_option('pve-qm-stateuri', {
 });
 
 PVE::JSONSchema::register_standard_option('pve-qemu-machine', {
-       description => "Specifies the Qemu machine type.",
+       description => "Specifies the QEMU machine type.",
        type => 'string',
        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,
 });
 
-#no warnings 'redefine';
-
+# FIXME: remove in favor of just using the INotify one, it's cached there exactly the same way
 my $nodename_cache;
 sub nodename {
     $nodename_cache //= PVE::INotify::nodename();
@@ -149,7 +150,7 @@ PVE::JSONSchema::register_format('pve-qm-watchdog', $watchdog_fmt);
 
 my $agent_fmt = {
     enabled => {
-       description => "Enable/disable communication with a Qemu Guest Agent (QGA) running in the VM.",
+       description => "Enable/disable communication with a QEMU Guest Agent (QGA) running in the VM.",
        type => 'boolean',
        default => 0,
        default_key => 1,
@@ -299,7 +300,9 @@ my $confdesc = {
        type => 'string', format => 'pve-hotplug-features',
        description => "Selectively enable hotplug features. This is a comma separated list of"
            ." hotplug features: 'network', 'disk', 'cpu', 'memory', 'usb' and 'cloudinit'. Use '0' to disable"
-           ." hotplug completely. Using '1' as value is an alias for the default `network,disk,usb`.",
+           ." hotplug completely. Using '1' as value is an alias for the default `network,disk,usb`."
+           ." USB hotplugging is possible for guests with machine version >= 7.1 and ostype l26 or"
+           ." windows > 7.",
         default => 'network,disk,usb',
     },
     reboot => {
@@ -478,7 +481,7 @@ EODESC
     },
     agent => {
        optional => 1,
-       description => "Enable/disable communication with the Qemu Guest Agent and its properties.",
+       description => "Enable/disable communication with the QEMU Guest Agent and its properties.",
        type => 'string',
        format => $agent_fmt,
     },
@@ -710,7 +713,7 @@ EODESCR
     },
     affinity => {
        type => 'string', format => 'pve-cpuset',
-       description => "List of host cores used to execute guest processes.",
+       description => "List of host cores used to execute guest processes, for example: 0,5,8-11",
        optional => 1,
     },
 };
@@ -823,7 +826,7 @@ while (my ($k, $v) = each %$confdesc) {
     PVE::JSONSchema::register_standard_option("pve-qm-$k", $v);
 }
 
-my $MAX_USB_DEVICES = 5;
+my $MAX_USB_DEVICES = 14;
 my $MAX_NETS = 32;
 my $MAX_SERIAL_PORTS = 4;
 my $MAX_PARALLEL_PORTS = 3;
@@ -920,7 +923,7 @@ my $net_fmt = {
     }),
     queues => {
        type => 'integer',
-       minimum => 0, maximum => 16,
+       minimum => 0, maximum => 64,
        description => 'Number of packet queues to be used on the device.',
        optional => 1,
     },
@@ -1092,7 +1095,9 @@ EODESCR
     usb3 => {
        optional => 1,
        type => 'boolean',
-       description => "Specifies whether if given host option is a USB3 device or port.",
+       description => "Specifies whether if given host option is a USB3 device or port."
+           ." For modern guests (machine version >= 7.1 and ostype l26 and windows > 7), this flag"
+           ." is irrelevant (all devices are plugged into a xhci controller).",
         default => 0,
     },
 };
@@ -1100,7 +1105,8 @@ EODESCR
 my $usbdesc = {
     optional => 1,
     type => 'string', format => $usb_fmt,
-    description => "Configure an USB device (n is 0 to 4).",
+    description => "Configure an USB device (n is 0 to 4, for machine version >= 7.1 and ostype"
+       ." l26 or windows > 7, n can be up to 14).",
 };
 PVE::JSONSchema::register_standard_option("pve-qm-usb", $usbdesc);
 
@@ -1743,7 +1749,7 @@ sub print_pbs_blockdev {
 }
 
 sub print_netdevice_full {
-    my ($vmid, $conf, $net, $netid, $bridges, $use_old_bios_files, $arch, $machine_type) = @_;
+    my ($vmid, $conf, $net, $netid, $bridges, $use_old_bios_files, $arch, $machine_type, $machine_version) = @_;
 
     my $device = $net->{model};
     if ($net->{model} eq 'virtio') {
@@ -1757,7 +1763,15 @@ sub print_netdevice_full {
        # and out of each queue plus one config interrupt and control vector queue
        my $vectors = $net->{queues} * 2 + 2;
        $tmpstr .= ",vectors=$vectors,mq=on";
+       if (min_version($machine_version, 7, 1)) {
+           $tmpstr .= ",packed=on";
+       }
     }
+
+    if (min_version($machine_version, 7, 1) && $net->{model} eq 'virtio'){
+       $tmpstr .= ",rx_queue_size=1024,tx_queue_size=1024";
+    }
+
     $tmpstr .= ",bootindex=$net->{bootindex}" if $net->{bootindex} ;
 
     if (my $mtu = $net->{mtu}) {
@@ -1936,14 +1950,14 @@ sub parse_numa {
 
 # netX: e1000=XX:XX:XX:XX:XX:XX,bridge=vmbr0,rate=<mbps>
 sub parse_net {
-    my ($data) = @_;
+    my ($data, $disable_mac_autogen) = @_;
 
     my $res = eval { parse_property_string($net_fmt, $data) };
     if ($@) {
        warn $@;
        return;
     }
-    if (!defined($res->{macaddr})) {
+    if (!defined($res->{macaddr}) && !$disable_mac_autogen) {
        my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
        $res->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
     }
@@ -2276,6 +2290,15 @@ sub cloudinit_config_properties {
     return dclone($confdesc_cloudinit);
 }
 
+sub cloudinit_pending_properties {
+    my $p = {
+       map { $_ => 1 } keys $confdesc_cloudinit->%*,
+       name => 1,
+    };
+    $p->{"net$_"} = 1 for 0..($MAX_NETS-1);
+    return $p;
+}
+
 sub check_type {
     my ($key, $value) = @_;
 
@@ -2413,6 +2436,13 @@ sub parse_vm_config {
 
     my $conf = $res;
     my $descr;
+    my $finish_description = sub {
+       if (defined($descr)) {
+           $descr =~ s/\s+$//;
+           $conf->{description} = $descr;
+       }
+       $descr = undef;
+    };
     my $section = '';
 
     my @lines = split(/\n/, $raw);
@@ -2421,26 +2451,18 @@ sub parse_vm_config {
 
        if ($line =~ m/^\[PENDING\]\s*$/i) {
            $section = 'pending';
-           if (defined($descr)) {
-               $descr =~ s/\s+$//;
-               $conf->{description} = $descr;
-           }
-           $descr = undef;
+           $finish_description->();
            $conf = $res->{$section} = {};
            next;
        } elsif ($line =~ m/^\[special:cloudinit\]\s*$/i) {
            $section = 'cloudinit';
-           $descr = undef;
+           $finish_description->();
            $conf = $res->{$section} = {};
            next;
 
        } elsif ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
            $section = $1;
-           if (defined($descr)) {
-               $descr =~ s/\s+$//;
-               $conf->{description} = $descr;
-           }
-           $descr = undef;
+           $finish_description->();
            $conf = $res->{snapshots}->{$section} = {};
            next;
        }
@@ -2470,6 +2492,11 @@ sub parse_vm_config {
        } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) {
            my $key = $1;
            my $value = $2;
+           if ($section eq 'cloudinit') {
+               # ignore validation only used for informative purpose
+               $conf->{$key} = $value;
+               next;
+           }
            eval { $value = check_type($key, $value); };
            if ($@) {
                $handle_error->("vm $vmid - unable to parse value of '$key' - $@");
@@ -2494,10 +2521,7 @@ sub parse_vm_config {
        }
     }
 
-    if (defined($descr)) {
-       $descr =~ s/\s+$//;
-       $conf->{description} = $descr;
-    }
+    $finish_description->();
     delete $res->{snapstate}; # just to be sure
 
     return $res;
@@ -2554,8 +2578,6 @@ sub write_vm_config {
 
     &$cleanup_config($conf->{pending}, 1);
 
-    &$cleanup_config($conf->{cloudinit});
-
     foreach my $snapname (keys %{$conf->{snapshots}}) {
        die "internal error: snapshot name '$snapname' is forbidden" if lc($snapname) eq 'pending';
        &$cleanup_config($conf->{snapshots}->{$snapname}, undef, $snapname);
@@ -2599,7 +2621,7 @@ sub write_vm_config {
        $raw .= &$generate_raw_config($conf->{pending}, 1);
     }
 
-    if (scalar(keys %{$conf->{cloudinit}})){
+    if (scalar(keys %{$conf->{cloudinit}}) && PVE::QemuConfig->has_cloudinit($conf)){
        $raw .= "\n[special:cloudinit]\n";
        $raw .= &$generate_raw_config($conf->{cloudinit});
     }
@@ -2766,6 +2788,12 @@ sub check_local_storage_availability {
 sub check_running {
     my ($vmid, $nocheck, $node) = @_;
 
+    # $nocheck is set when called during a migration, in which case the config
+    # file might still or already reside on the *other* node
+    # - because rename has already happened, and current node is source
+    # - because rename hasn't happened yet, and current node is target
+    # - because rename has happened, current node is target, but hasn't yet
+    # processed it yet
     PVE::QemuConfig::assert_config_exists_on_node($vmid, $node) if !$nocheck;
     return PVE::QemuServer::Helpers::vm_running_locally($vmid);
 }
@@ -2791,7 +2819,7 @@ sub vzlist {
 our $vmstatus_return_properties = {
     vmid => get_standard_option('pve-vmid'),
     status => {
-       description => "Qemu process status.",
+       description => "QEMU process status.",
        type => 'string',
        enum => ['stopped', 'running'],
     },
@@ -2813,7 +2841,7 @@ our $vmstatus_return_properties = {
        optional => 1,
     },
     qmpstatus => {
-       description => "Qemu QMP agent status.",
+       description => "QEMU QMP agent status.",
        type => 'string',
        optional => 1,
     },
@@ -3486,11 +3514,51 @@ my sub should_disable_smm {
        $vga->{type} && $vga->{type} =~ m/^(serial\d+|none)$/;
 }
 
+my sub print_ovmf_drive_commandlines {
+    my ($conf, $storecfg, $vmid, $arch, $q35, $version_guard) = @_;
+
+    my $d = $conf->{efidisk0} ? parse_drive('efidisk0', $conf->{efidisk0}) : undef;
+
+    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 $var_drive_str = "if=pflash,unit=1,id=drive-efidisk0";
+    if ($d) {
+       my ($storeid, $volname) = PVE::Storage::parse_volume_id($d->{file}, 1);
+       my ($path, $format) = $d->@{'file', 'format'};
+       if ($storeid) {
+           $path = PVE::Storage::path($storecfg, $d->{file});
+           if (!defined($format)) {
+               my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+               $format = qemu_img_format($scfg, $volname);
+           }
+       } elsif (!defined($format)) {
+           die "efidisk format must be specified\n";
+       }
+       # SPI flash does lots of read-modify-write OPs, without writeback this gets really slow #3329
+       if ($path =~ m/^rbd:/) {
+           $var_drive_str .= ',cache=writeback';
+           $path .= ':rbd_cache_policy=writeback'; # avoid write-around, we *need* to cache writes too
+       }
+       $var_drive_str .= ",format=$format,file=$path";
+
+       $var_drive_str .= ",size=" . (-s $ovmf_vars) if $format eq 'raw' && $version_guard->(4, 1, 2);
+       $var_drive_str .= ',readonly=on' if drive_is_read_only($conf, $d);
+    } else {
+       log_warn("no efidisk configured! Using temporary efivars disk.");
+       my $path = "/tmp/$vmid-ovmf.fd";
+       PVE::Tools::file_copy($ovmf_vars, $path, -s $ovmf_vars);
+       $var_drive_str .= ",format=raw,file=$path";
+       $var_drive_str .= ",size=" . (-s $ovmf_vars) if $version_guard->(4, 1, 2);
+    }
+
+    return ("if=pflash,unit=0,format=raw,readonly=on,file=$ovmf_code", $var_drive_str);
+}
+
 sub config_to_command {
     my ($storecfg, $vmid, $conf, $defaults, $forcemachine, $forcecpu,
         $pbs_backing) = @_;
 
-    my $cmd = [];
     my ($globalFlags, $machineFlags, $rtcFlags) = ([], [], []);
     my $devices = [];
     my $bridges = {};
@@ -3552,11 +3620,9 @@ sub config_to_command {
     my $use_old_bios_files = undef;
     ($use_old_bios_files, $machine_type) = qemu_use_old_bios_files($machine_type);
 
+    my $cmd = [];
     if ($conf->{affinity}) {
-       push @$cmd, "/usr/bin/taskset";
-       push @$cmd, "--cpu-list";
-       push @$cmd, "--all-tasks";
-       push @$cmd, $conf->{affinity};
+       push @$cmd, '/usr/bin/taskset', '--cpu-list', '--all-tasks', $conf->{affinity};
     }
 
     push @$cmd, $kvm_binary;
@@ -3608,54 +3674,10 @@ sub config_to_command {
     }
 
     if ($conf->{bios} && $conf->{bios} eq 'ovmf') {
-       my $d;
-       if (my $efidisk = $conf->{efidisk0}) {
-           $d = parse_drive('efidisk0', $efidisk);
-       }
-
-       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);
-       my $read_only_str = '';
-       if ($d) {
-           my ($storeid, $volname) = PVE::Storage::parse_volume_id($d->{file}, 1);
-           $format = $d->{format};
-           if ($storeid) {
-               $path = PVE::Storage::path($storecfg, $d->{file});
-               if (!defined($format)) {
-                   my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
-                   $format = qemu_img_format($scfg, $volname);
-               }
-           } else {
-               $path = $d->{file};
-               die "efidisk format must be specified\n"
-                   if !defined($format);
-           }
-
-           $read_only_str = ',readonly=on' if drive_is_read_only($conf, $d);
-       } else {
-           log_warn("no efidisk configured! Using temporary efivars disk.");
-           $path = "/tmp/$vmid-ovmf.fd";
-           PVE::Tools::file_copy($ovmf_vars, $path, -s $ovmf_vars);
-           $format = 'raw';
-       }
-
-       my $size_str = "";
-
-       if ($format eq 'raw' && $version_guard->(4, 1, 2)) {
-           $size_str = ",size=" . (-s $ovmf_vars);
-       }
-
-       # SPI flash does lots of read-modify-write OPs, without writeback this gets really slow #3329
-       my $cache = "";
-       if ($path =~ m/^rbd:/) {
-               $cache = ',cache=writeback';
-               $path .= ':rbd_cache_policy=writeback'; # avoid write-around, we *need* to cache writes too
-       }
-
-       push @$cmd, '-drive', "if=pflash,unit=0,format=raw,readonly=on,file=$ovmf_code";
-       push @$cmd, '-drive', "if=pflash,unit=1$cache,format=$format,id=drive-efidisk0$size_str,file=${path}${read_only_str}";
+       my ($code_drive_str, $var_drive_str) =
+           print_ovmf_drive_commandlines($conf, $storecfg, $vmid, $arch, $q35, $version_guard);
+       push $cmd->@*, '-drive', $code_drive_str;
+       push $cmd->@*, '-drive', $var_drive_str;
     }
 
     if ($q35) { # tell QEMU to load q35 config early
@@ -3677,7 +3699,7 @@ sub config_to_command {
 
     # add usb controllers
     my @usbcontrollers = PVE::QemuServer::USB::get_usb_controllers(
-        $conf, $bridges, $arch, $machine_type, $usbdesc->{format}, $MAX_USB_DEVICES);
+       $conf, $bridges, $arch, $machine_type, $usbdesc->{format}, $MAX_USB_DEVICES, $machine_version);
     push @$devices, @usbcontrollers if @usbcontrollers;
     my $vga = parse_vga($conf->{vga});
 
@@ -3719,7 +3741,7 @@ sub config_to_command {
     $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, $bootorder);
+       $conf, $usbdesc->{format}, $MAX_USB_DEVICES, $usb_dev_features, $bootorder, $machine_version);
     push @$devices, @usbdevices if @usbdevices;
 
     # serial devices
@@ -3728,7 +3750,7 @@ sub config_to_command {
        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
+           # 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) {
@@ -4033,6 +4055,7 @@ sub config_to_command {
        next if !$conf->{$netname};
        my $d = parse_net($conf->{$netname});
        next if !$d;
+       # save the MAC addr here (could be auto-gen. in some odd setups) for FDB registering later?
 
        $use_virtio = 1 if $d->{model} eq 'virtio';
 
@@ -4042,7 +4065,7 @@ sub config_to_command {
        push @$devices, '-netdev', $netdevfull;
 
        my $netdevicefull = print_netdevice_full(
-           $vmid, $conf, $d, $netname, $bridges, $use_old_bios_files, $arch, $machine_type);
+           $vmid, $conf, $d, $netname, $bridges, $use_old_bios_files, $arch, $machine_type, $machine_version);
 
        push @$devices, '-device', $netdevicefull;
     }
@@ -4171,7 +4194,7 @@ sub vm_devices_list {
        my $to_check = [];
        for my $d (@$devices_to_check) {
            $devices->{$d->{'qdev_id'}} = 1 if $d->{'qdev_id'};
-           next if !$d->{'pci_bridge'};
+           next if !$d->{'pci_bridge'} || !$d->{'pci_bridge'}->{devices};
 
            $devices->{$d->{'qdev_id'}} += scalar(@{$d->{'pci_bridge'}->{devices}});
            push @$to_check, @{$d->{'pci_bridge'}->{devices}};
@@ -4199,7 +4222,7 @@ sub vm_devices_list {
     # 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+$/) {
+       if ($per->{name} =~ m/^usb(?:redirdev)?\d+$/) {
            $devices->{$per->{name}} = 1;
        }
     }
@@ -4222,11 +4245,12 @@ sub vm_deviceplug {
        qemu_deviceadd($vmid, print_tabletdevice_full($conf, $arch));
     } elsif ($deviceid eq 'keyboard') {
        qemu_deviceadd($vmid, print_keyboarddevice_full($conf, $arch));
+    } elsif ($deviceid =~ m/^usbredirdev(\d+)$/) {
+       my $id = $1;
+       qemu_spice_usbredir_chardev_add($vmid, "usbredirchardev$id");
+       qemu_deviceadd($vmid, PVE::QemuServer::USB::print_spice_usbdevice($id, "xhci", $id + 1));
     } 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));
+       qemu_deviceadd($vmid, PVE::QemuServer::USB::print_usbdevice_full($conf, $deviceid, $device, {}, $1 + 1));
     } elsif ($deviceid =~ m/^(virtio)(\d+)$/) {
        qemu_iothread_add($vmid, $deviceid, $device);
 
@@ -4273,11 +4297,12 @@ sub vm_deviceplug {
        return if !qemu_netdevadd($vmid, $conf, $arch, $device, $deviceid);
 
        my $machine_type = PVE::QemuServer::Machine::qemu_machine_pxe($vmid, $conf);
+       my $machine_version = PVE::QemuServer::Machine::extract_version($machine_type);
        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);
+           $vmid, $conf, $device, $deviceid, undef, $use_old_bios_files, $arch, $machine_type, $machine_version);
        qemu_deviceadd($vmid, $netdevicefull);
        eval {
            qemu_deviceaddverify($vmid, $deviceid);
@@ -4312,14 +4337,14 @@ sub vm_deviceunplug {
     my $bootdisks = PVE::QemuServer::Drive::get_bootdisks($conf);
     die "can't unplug bootdisk '$deviceid'\n" if grep {$_ eq $deviceid} @$bootdisks;
 
-    if ($deviceid eq 'tablet' || $deviceid eq 'keyboard') {
+    if ($deviceid eq 'tablet' || $deviceid eq 'keyboard' || $deviceid eq 'xhci') {
        qemu_devicedel($vmid, $deviceid);
+    } elsif ($deviceid =~ m/^usbredirdev\d+$/) {
+       qemu_devicedel($vmid, $deviceid);
+       qemu_devicedelverify($vmid, $deviceid);
     } elsif ($deviceid =~ m/^usb\d+$/) {
-       die "usb hotplug currently not reliable\n";
-       # when unplugging usb devices this way, there may be remaining usb
-       # controllers/hubs so we disable it for now
-       #qemu_devicedel($vmid, $deviceid);
-       #qemu_devicedelverify($vmid, $deviceid);
+       qemu_devicedel($vmid, $deviceid);
+       qemu_devicedelverify($vmid, $deviceid);
     } elsif ($deviceid =~ m/^(virtio)(\d+)$/) {
        my $device = parse_drive($deviceid, $conf->{$deviceid});
 
@@ -4351,6 +4376,20 @@ sub vm_deviceunplug {
     return 1;
 }
 
+sub qemu_spice_usbredir_chardev_add {
+    my ($vmid, $id) = @_;
+
+    mon_cmd($vmid, "chardev-add" , (
+       id => $id,
+       backend => {
+           type => 'spicevmc',
+           data => {
+               type => "usbredir",
+           },
+       },
+    ));
+}
+
 sub qemu_deviceadd {
     my ($vmid, $devicefull) = @_;
 
@@ -4565,15 +4604,14 @@ sub qemu_usb_hotplug {
     vm_deviceunplug($vmid, $conf, $deviceid);
 
     # check if xhci controller is necessary and available
-    if ($device->{usb3}) {
-
-       my $devicelist = vm_devices_list($vmid);
+    my $devicelist = vm_devices_list($vmid);
 
-       if (!$devicelist->{xhci}) {
-           my $pciaddr = print_pci_addr("xhci", undef, $arch, $machine_type);
-           qemu_deviceadd($vmid, "nec-usb-xhci,id=xhci$pciaddr");
-       }
+    if (!$devicelist->{xhci}) {
+       my $pciaddr = print_pci_addr("xhci", undef, $arch, $machine_type);
+       qemu_deviceadd($vmid, PVE::QemuServer::USB::print_qemu_xhci_controller($pciaddr));
     }
+
+    # print_usbdevice_full expects the parsed device
     my $d = parse_usb_device($device->{host});
     $d->{usb3} = $device->{usb3};
 
@@ -4869,11 +4907,78 @@ sub vmconfig_hotplug_pending {
        $errors->{$opt} = "hotplug problem - $msg";
     };
 
+    my $cloudinit_pending_properties = PVE::QemuServer::cloudinit_pending_properties();
+
+    my $cloudinit_record_changed = sub {
+       my ($conf, $opt, $old, $new) = @_;
+       return if !$cloudinit_pending_properties->{$opt};
+
+       my $ci = ($conf->{cloudinit} //= {});
+
+       my $recorded = $ci->{$opt};
+       my %added = map { $_ => 1 } PVE::Tools::split_list(delete($ci->{added}) // '');
+
+       if (defined($new)) {
+           if (defined($old)) {
+               # an existing value is being modified
+               if (defined($recorded)) {
+                   # the value was already not in sync
+                   if ($new eq $recorded) {
+                       # a value is being reverted to the cloud-init state:
+                       delete $ci->{$opt};
+                       delete $added{$opt};
+                   } else {
+                       # the value was changed multiple times, do nothing
+                   }
+               } elsif ($added{$opt}) {
+                   # the value had been marked as added and is being changed, do nothing
+               } else {
+                   # the value is new, record it:
+                   $ci->{$opt} = $old;
+               }
+           } else {
+               # a new value is being added
+               if (defined($recorded)) {
+                   # it was already not in sync
+                   if ($new eq $recorded) {
+                       # a value is being reverted to the cloud-init state:
+                       delete $ci->{$opt};
+                       delete $added{$opt};
+                   } else {
+                       # the value had temporarily been removed, do nothing
+                   }
+               } elsif ($added{$opt}) {
+                   # the value had been marked as added already, do nothing
+               } else {
+                   # the value is new, add it
+                   $added{$opt} = 1;
+               }
+           }
+       } elsif (!defined($old)) {
+           # a non-existent value is being removed? ignore...
+       } else {
+           # a value is being deleted
+           if (defined($recorded)) {
+               # a value was already recorded, just keep it
+           } elsif ($added{$opt}) {
+               # the value was marked as added, remove it
+               delete $added{$opt};
+           } else {
+               # a previously unrecorded value is being removed, record the old value:
+               $ci->{$opt} = $old;
+           }
+       }
+
+       my $added = join(',', sort keys %added);
+       $ci->{added} = $added if length($added);
+    };
+
     my $changes = 0;
     foreach my $opt (keys %{$conf->{pending}}) { # add/change
        if ($fast_plug_option->{$opt}) {
-           $conf->{$opt} = $conf->{pending}->{$opt};
-           delete $conf->{pending}->{$opt};
+           my $new = delete $conf->{pending}->{$opt};
+           $cloudinit_record_changed->($conf, $opt, $conf->{$opt}, $new);
+           $conf->{$opt} = $new;
            $changes = 1;
        }
     }
@@ -4882,10 +4987,16 @@ sub vmconfig_hotplug_pending {
        PVE::QemuConfig->write_config($vmid, $conf);
     }
 
+    my $ostype = $conf->{ostype};
+    my $version = extract_version($machine_type, get_running_qemu_version($vmid));
     my $hotplug_features = parse_hotplug_features(defined($conf->{hotplug}) ? $conf->{hotplug} : '1');
+    my $usb_hotplug = $hotplug_features->{usb}
+       && min_version($version, 7, 1)
+       && defined($ostype) && ($ostype eq 'l26' || windows_version($ostype) > 7);
 
     my $cgroup = PVE::QemuServer::CGroup->new($vmid);
     my $pending_delete_hash = PVE::QemuConfig->parse_pending_delete($conf->{pending}->{delete});
+
     foreach my $opt (sort keys %$pending_delete_hash) {
        next if $selection && !$selection->{$opt};
        my $force = $pending_delete_hash->{$opt}->{force};
@@ -4902,11 +5013,11 @@ sub vmconfig_hotplug_pending {
                    vm_deviceunplug($vmid, $conf, 'tablet');
                    vm_deviceunplug($vmid, $conf, 'keyboard') if $arch eq 'aarch64';
                }
-           } elsif ($opt =~ m/^usb\d+/) {
-               die "skip\n";
-               # since we cannot reliably hot unplug usb devices we are disabling it
-               #die "skip\n" if !$hotplug_features->{usb} || $conf->{$opt} =~ m/spice/i;
-               #vm_deviceunplug($vmid, $conf, $opt);
+           } elsif ($opt =~ m/^usb(\d+)$/) {
+               my $index = $1;
+               die "skip\n" if !$usb_hotplug;
+               vm_deviceunplug($vmid, $conf, "usbredirdev$index"); # if it's a spice port
+               vm_deviceunplug($vmid, $conf, $opt);
            } elsif ($opt eq 'vcpus') {
                die "skip\n" if !$hotplug_features->{cpu};
                qemu_cpu_hotplug($vmid, $conf, undef);
@@ -4939,11 +5050,13 @@ sub vmconfig_hotplug_pending {
        if (my $err = $@) {
            &$add_error($opt, $err) if $err ne "skip\n";
        } else {
-           delete $conf->{$opt};
+           my $old = delete $conf->{$opt};
+           $cloudinit_record_changed->($conf, $opt, $old, undef);
            PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
        }
     }
 
+    my $cloudinit_opt;
     foreach my $opt (keys %{$conf->{pending}}) {
        next if $selection && !$selection->{$opt};
        my $value = $conf->{pending}->{$opt};
@@ -4960,13 +5073,15 @@ sub vmconfig_hotplug_pending {
                    vm_deviceunplug($vmid, $conf, 'tablet');
                    vm_deviceunplug($vmid, $conf, 'keyboard') if $arch eq 'aarch64';
                }
-           } elsif ($opt =~ m/^usb\d+$/) {
-               die "skip\n";
-               # since we cannot reliably hot unplug usb devices we disable it for now
-               #die "skip\n" if !$hotplug_features->{usb} || $value =~ m/spice/i;
-               #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 =~ m/^usb(\d+)$/) {
+               my $index = $1;
+               die "skip\n" if !$usb_hotplug;
+               my $d = eval { parse_property_string($usbdesc->{format}, $value) };
+               my $id = $opt;
+               if ($d->{host} eq 'spice')  {
+                   $id = "usbredirdev$index";
+               }
+               qemu_usb_hotplug($storecfg, $conf, $vmid, $id, $d, $arch, $machine_type);
            } elsif ($opt eq 'vcpus') {
                die "skip\n" if !$hotplug_features->{cpu};
                qemu_cpu_hotplug($vmid, $conf, $value);
@@ -4990,7 +5105,9 @@ sub vmconfig_hotplug_pending {
                # some changes can be done without hotplug
                my $drive = parse_drive($opt, $value);
                if (drive_is_cloudinit($drive)) {
-                   PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid);
+                   $cloudinit_opt = [$opt, $drive];
+                   # apply all the other changes first, then generate the cloudinit disk
+                   die "skip\n";
                }
                vmconfig_update_disk($storecfg, $conf, $hotplug_features->{disk},
                                     $vmid, $opt, $value, $arch, $machine_type);
@@ -5009,6 +5126,24 @@ sub vmconfig_hotplug_pending {
                die "skip\n";  # skip non-hot-pluggable options
            }
        };
+       if (my $err = $@) {
+           &$add_error($opt, $err) if $err ne "skip\n";
+       } else {
+           $cloudinit_record_changed->($conf, $opt, $conf->{$opt}, $value);
+           $conf->{$opt} = $value;
+           delete $conf->{pending}->{$opt};
+       }
+    }
+
+    if (defined($cloudinit_opt)) {
+       my ($opt, $drive) = @$cloudinit_opt;
+       my $value = $conf->{pending}->{$opt};
+       eval {
+           my $temp = {%$conf, $opt => $value};
+           PVE::QemuServer::Cloudinit::apply_cloudinit_config($temp, $vmid);
+           vmconfig_update_disk($storecfg, $conf, $hotplug_features->{disk},
+                                $vmid, $opt, $value, $arch, $machine_type);
+       };
        if (my $err = $@) {
            &$add_error($opt, $err) if $err ne "skip\n";
        } else {
@@ -5016,15 +5151,24 @@ sub vmconfig_hotplug_pending {
            delete $conf->{pending}->{$opt};
        }
     }
-    PVE::QemuConfig->write_config($vmid, $conf);
 
-    if($hotplug_features->{cloudinit}) {
-       my $pending = PVE::QemuServer::Cloudinit::get_pending_config($conf, $vmid);
-       my $regenerate = undef;
-       for my $item (@$pending) {
-           $regenerate = 1 if defined($item->{delete}) or defined($item->{pending});
+    # unplug xhci controller if no usb device is left
+    if ($usb_hotplug) {
+       my $has_usb = 0;
+       for (my $i = 0; $i < $MAX_USB_DEVICES; $i++) {
+           next if !defined($conf->{"usb$i"});
+           $has_usb = 1;
+           last;
+       }
+       if (!$has_usb) {
+           vm_deviceunplug($vmid, $conf, 'xhci');
        }
-       PVE::QemuServer::vmconfig_update_cloudinit_drive($storecfg, $conf, $vmid) if $regenerate;
+    }
+
+    PVE::QemuConfig->write_config($vmid, $conf);
+
+    if ($hotplug_features->{cloudinit} && PVE::QemuServer::Cloudinit::has_changes($conf)) {
+       PVE::QemuServer::vmconfig_update_cloudinit_drive($storecfg, $conf, $vmid);
     }
 }
 
@@ -5070,7 +5214,7 @@ sub vmconfig_delete_or_detach_drive {
 
 
 sub vmconfig_apply_pending {
-    my ($vmid, $conf, $storecfg, $errors) = @_;
+    my ($vmid, $conf, $storecfg, $errors, $skip_cloud_init) = @_;
 
     return if !scalar(keys %{$conf->{pending}});
 
@@ -5103,7 +5247,7 @@ sub vmconfig_apply_pending {
 
     PVE::QemuConfig->cleanup_pending($conf);
 
-    my $generate_cloudnit = undef;
+    my $generate_cloudinit = $skip_cloud_init ? 0 : undef;
 
     foreach my $opt (keys %{$conf->{pending}}) { # add/change
        next if $opt eq 'delete'; # just to be sure
@@ -5118,7 +5262,7 @@ sub vmconfig_apply_pending {
 
            if (is_valid_drivename($opt)) {
                my $drive = parse_drive($opt, $conf->{pending}->{$opt});
-               $generate_cloudnit = 1 if drive_is_cloudinit($drive);
+               $generate_cloudinit //= 1 if drive_is_cloudinit($drive);
            }
 
            $conf->{$opt} = delete $conf->{pending}->{$opt};
@@ -5127,7 +5271,13 @@ sub vmconfig_apply_pending {
 
     # write all changes at once to avoid unnecessary i/o
     PVE::QemuConfig->write_config($vmid, $conf);
-    PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid) if $generate_cloudnit;
+    if ($generate_cloudinit) {
+       if (PVE::QemuServer::Cloudinit::apply_cloudinit_config($conf, $vmid)) {
+           # After successful generation and if there were changes to be applied, update the
+           # config to drop the {cloudinit} entry.
+           PVE::QemuConfig->write_config($vmid, $conf);
+       }
+    }
 }
 
 sub vmconfig_update_net {
@@ -5234,7 +5384,8 @@ sub vmconfig_update_disk {
                    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})) {
+                   safe_string_ne($drive->{ssd}, $old_drive->{ssd}) ||
+                   safe_string_ne($drive->{ro}, $old_drive->{ro})) {
                    die "skip\n";
                }
 
@@ -5330,7 +5481,10 @@ sub vmconfig_update_cloudinit_drive {
 
     return if !$cloudinit_drive;
 
-    PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid);
+    if (PVE::QemuServer::Cloudinit::apply_cloudinit_config($conf, $vmid)) {
+       PVE::QemuConfig->write_config($vmid, $conf);
+    }
+
     my $running = PVE::QemuServer::check_running($vmid);
 
     if ($running) {
@@ -5470,7 +5624,7 @@ sub vm_start {
 #   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)
+#   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)
@@ -5516,7 +5670,14 @@ sub vm_start_nolock {
 
     # don't regenerate the ISO if the VM is started as part of a live migration
     # this way we can reuse the old ISO with the correct config
-    PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid) if !$migratedfrom;
+    if (!$migratedfrom) {
+       if (PVE::QemuServer::Cloudinit::apply_cloudinit_config($conf, $vmid)) {
+           # FIXME: apply_cloudinit_config updates $conf in this case, and it would only drop
+           # $conf->{cloudinit}, so we could just not do this?
+           # But we do it above, so for now let's be consistent.
+           $conf = PVE::QemuConfig->load_config($vmid); # update/reload
+       }
+    }
 
     # override offline migrated volumes, conf is out of date still
     if (my $offline_volumes = $migrate_opts->{offline_volumes}) {
@@ -5530,7 +5691,10 @@ sub vm_start_nolock {
     my $defaults = load_defaults();
 
     # set environment variable useful inside network script
-    $ENV{PVE_MIGRATED_FROM} = $migratedfrom if $migratedfrom;
+    # for remote migration the config is available on the target node!
+    if (!$migrate_opts->{remote_node}) {
+       $ENV{PVE_MIGRATED_FROM} = $migratedfrom;
+    }
 
     PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'pre-start', 1);
 
@@ -5577,10 +5741,10 @@ sub vm_start_nolock {
        return $migration_ip;
     };
 
-    my $migrate_uri;
     if ($statefile) {
        if ($statefile eq 'tcp') {
-           my $localip = "localhost";
+           my $migrate = $res->{migrate} = { proto => 'tcp' };
+           $migrate->{addr} = "localhost";
            my $datacenterconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
            my $nodename = nodename();
 
@@ -5593,26 +5757,26 @@ sub vm_start_nolock {
            }
 
            if ($migration_type eq 'insecure') {
-               $localip = $get_migration_ip->($nodename);
-               $localip = "[$localip]" if Net::IP::ip_is_ipv6($localip);
+               $migrate->{addr} = $get_migration_ip->($nodename);
+               $migrate->{addr} = "[$migrate->{addr}]" if Net::IP::ip_is_ipv6($migrate->{addr});
            }
 
            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;
+           $migrate->{port} = PVE::Tools::next_migrate_port($pfamily);
+           $migrate->{uri} = "tcp:$migrate->{addr}:$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";
+           my $migrate = $res->{migrate} = { proto => 'unix' };
+           $migrate->{addr} = "/run/qemu-server/$vmid.migrate";
+           unlink $migrate->{addr};
 
-           push @$cmd, '-incoming', $migrate_uri;
+           $migrate->{uri} = "unix:$migrate->{addr}";
+           push @$cmd, '-incoming', $migrate->{uri};
            push @$cmd, '-S';
 
        } elsif (-e $statefile) {
@@ -5767,10 +5931,9 @@ sub vm_start_nolock {
     eval { PVE::QemuServer::PCI::reserve_pci_usage($pci_id_list, $vmid, undef, $pid) };
     warn $@ if $@;
 
-    print "migration listens on $migrate_uri\n" if $migrate_uri;
-    $res->{migrate_uri} = $migrate_uri;
-
-    if ($statefile && $statefile ne 'tcp' && $statefile ne 'unix')  {
+    if (defined($res->{migrate})) {
+       print "migration listens on $res->{migrate}->{uri}\n";
+    } elsif ($statefile) {
        eval { mon_cmd($vmid, "cont"); };
        warn $@ if $@;
     }
@@ -5781,10 +5944,11 @@ sub vm_start_nolock {
 
        my $migrate_storage_uri;
        # nbd_protocol_version > 0 for unix socket support
-       if ($nbd_protocol_version > 0 && $migration_type eq 'secure') {
+       if ($nbd_protocol_version > 0 && ($migration_type eq 'secure' || $migration_type eq 'websocket')) {
            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";
+           $res->{migrate}->{unix_sockets} = [$socket_path];
        } else {
            my $nodename = nodename();
            my $localip = $get_migration_ip->($nodename);
@@ -5802,8 +5966,6 @@ sub vm_start_nolock {
            $migrate_storage_uri = "nbd:${localip}:${storage_migrate_port}";
        }
 
-       $res->{migrate_storage_uri} = $migrate_storage_uri;
-
        foreach my $opt (sort keys %$nbd) {
            my $drivestr = $nbd->{$opt}->{drivestr};
            my $volid = $nbd->{$opt}->{volid};
@@ -5843,6 +6005,7 @@ sub vm_start_nolock {
            my $nicconf = parse_net($conf->{$opt});
            qemu_set_link_status($vmid, $opt, 0) if $nicconf->{link_down};
        }
+       add_nets_bridge_fdb($conf, $vmid);
     }
 
     mon_cmd($vmid, 'qom-set',
@@ -6199,6 +6362,9 @@ sub vm_suspend {
     }
 }
 
+# $nocheck is set when called as part of a migration - in this context the
+# location of the config file (source or target node) is not deterministic,
+# since migration cannot wait for pmxcfs to process the rename
 sub vm_resume {
     my ($vmid, $skiplock, $nocheck) = @_;
 
@@ -6206,6 +6372,23 @@ sub vm_resume {
        my $res = mon_cmd($vmid, 'query-status');
        my $resume_cmd = 'cont';
        my $reset = 0;
+       my $conf;
+       if ($nocheck) {
+           $conf = eval { PVE::QemuConfig->load_config($vmid) }; # try on target node
+           if ($@) {
+               my $vmlist = PVE::Cluster::get_vmlist();
+               if (exists($vmlist->{ids}->{$vmid})) {
+                   my $node = $vmlist->{ids}->{$vmid}->{node};
+                   $conf = eval { PVE::QemuConfig->load_config($vmid, $node) }; # try on source node
+               }
+               if (!$conf) {
+                   PVE::Cluster::cfs_update(); # vmlist was wrong, invalidate cache
+                   $conf = PVE::QemuConfig->load_config($vmid); # last try on target node again
+               }
+           }
+       } else {
+           $conf = PVE::QemuConfig->load_config($vmid);
+       }
 
        if ($res->{status}) {
            return if $res->{status} eq 'running'; # job done, go home
@@ -6214,9 +6397,6 @@ sub vm_resume {
        }
 
        if (!$nocheck) {
-
-           my $conf = PVE::QemuConfig->load_config($vmid);
-
            PVE::QemuConfig->check_lock($conf)
                if !($skiplock || PVE::QemuConfig->has_lock($conf, 'backup'));
        }
@@ -6226,6 +6406,9 @@ sub vm_resume {
            # request before the backup finishes for example
            mon_cmd($vmid, "system_reset");
        }
+
+       add_nets_bridge_fdb($conf, $vmid) if $resume_cmd eq 'cont';
+
        mon_cmd($vmid, $resume_cmd);
     });
 }
@@ -7369,7 +7552,7 @@ sub qga_check_running {
 
     eval { mon_cmd($vmid, "guest-ping", timeout => 3); };
     if ($@) {
-       warn "Qemu Guest Agent is not running - $@" if !$nowarn;
+       warn "QEMU Guest Agent is not running - $@" if !$nowarn;
        return 0;
     }
     return 1;
@@ -8270,4 +8453,48 @@ sub check_volume_storage_type {
     return 1;
 }
 
+sub add_nets_bridge_fdb {
+    my ($conf, $vmid) = @_;
+
+    for my $opt (keys %$conf) {
+       next if $opt !~ m/^net(\d+)$/;
+       my $iface = "tap${vmid}i$1";
+       # NOTE: expect setups with learning off to *not* use auto-random-generation of MAC on start
+       my $net = parse_net($conf->{$opt}, 1) or next;
+
+       my $mac = $net->{macaddr};
+       if (!$mac) {
+           log_warn("MAC learning disabled, but vNIC '$iface' has no static MAC to add to forwarding DB!")
+               if !file_read_firstline("/sys/class/net/$iface/brport/learning");
+           next;
+       }
+
+       my $bridge = $net->{bridge};
+       if ($have_sdn) {
+           PVE::Network::SDN::Zones::add_bridge_fdb($iface, $mac, $bridge, $net->{firewall});
+       } elsif (-d "/sys/class/net/$bridge/bridge") { # avoid fdb management with OVS for now
+           PVE::Network::add_bridge_fdb($iface, $mac, $net->{firewall});
+       }
+    }
+}
+
+sub del_nets_bridge_fdb {
+    my ($conf, $vmid) = @_;
+
+    for my $opt (keys %$conf) {
+       next if $opt !~ m/^net(\d+)$/;
+       my $iface = "tap${vmid}i$1";
+
+       my $net = parse_net($conf->{$opt}) or next;
+       my $mac = $net->{macaddr} or next;
+
+       my $bridge = $net->{bridge};
+       if ($have_sdn) {
+           PVE::Network::SDN::Zones::del_bridge_fdb($iface, $mac, $bridge, $net->{firewall});
+       } elsif (-d "/sys/class/net/$bridge/bridge") { # avoid fdb management with OVS for now
+           PVE::Network::del_bridge_fdb($iface, $mac, $net->{firewall});
+       }
+    }
+}
+
 1;