X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FQemuServer.pm;h=a5ee8e2a390e746e53edefb42965222ae1961b43;hb=4ef13a7f9a2e59ccac7401c1645d73e8299f88df;hp=6bdcf55abf3e452fee8819c8b5146fde255429f8;hpb=ba5396b580ff088eefe78f8a5fa34a12438e5eae;p=qemu-server.git diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm index 6bdcf55..a5ee8e2 100644 --- a/PVE/QemuServer.pm +++ b/PVE/QemuServer.pm @@ -26,7 +26,7 @@ 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 qw(safe_string_ne safe_num_ne safe_boolean_ne); @@ -37,18 +37,18 @@ use PVE::RPCEnvironment; use PVE::Storage; use PVE::SysFSTools; use PVE::Systemd; -use PVE::Tools qw(run_command lock_file lock_file_full file_read_firstline file_get_contents dir_glob_foreach get_host_arch $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 foreach_drive foreach_volid); +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::Monitor qw(mon_cmd); -use PVE::QemuServer::PCI qw(print_pci_addr print_pcie_addr print_pcie_root_port); +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; @@ -104,7 +104,7 @@ sub map_storage { return $source if !defined($map); return $map->{entries}->{$source} - if defined($map->{entries}) && $map->{entries}->{$source}; + if $map->{entries} && defined($map->{entries}->{$source}); return $map->{default} if $map->{default}; @@ -589,8 +589,15 @@ EODESCR 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.", @@ -761,7 +768,6 @@ while (my ($k, $v) = each %$confdesc) { my $MAX_USB_DEVICES = 5; my $MAX_NETS = 32; -my $MAX_HOSTPCI_DEVICES = 16; my $MAX_SERIAL_PORTS = 4; my $MAX_PARALLEL_PORTS = 3; my $MAX_NUMA = 8; @@ -839,6 +845,7 @@ my $net_fmt = { type => 'string', description => $net_fmt_bridge_descr, format_description => 'bridge', + pattern => '[-_.\w\d]+', optional => 1, }, queues => { @@ -876,6 +883,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 = { @@ -997,76 +1010,6 @@ my $usbdesc = { }; PVE::JSONSchema::register_standard_option("pve-qm-usb", $usbdesc); -my $PCIRE = qr/([a-f0-9]{4}:)?[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 => < { - 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 => < 1, - type => 'string', format => 'pve-qm-hostpci', - description => "Map host PCI devices into guest.", - verbose_description => < 1, type => 'string', @@ -1105,8 +1048,8 @@ 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 $key (keys %{$PVE::QemuServer::Drive::drivedesc_hash}) { @@ -1585,6 +1528,22 @@ sub print_netdevice_full { } $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') { @@ -1689,6 +1648,11 @@ sub print_vga_device { $memory = ",ram_size=67108864,vram_size=33554432"; } + 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; @@ -1700,7 +1664,7 @@ sub print_vga_device { $pciaddr = print_pci_addr($vgaid, $bridges, $arch, $machine); } - return "$type,id=${vgaid}${memory}${max_outputs}${pciaddr}"; + return "$type,id=${vgaid}${memory}${max_outputs}${pciaddr}${edidoff}"; } sub parse_number_sets { @@ -1726,23 +1690,6 @@ sub parse_numa { 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) { - my $devs = PVE::SysFSTools::lspci($id); - die "no PCI device found for '$id'\n" if !scalar(@$devs); - push @{$res->{pciid}}, @$devs; - } - return $res; -} - # netX: e1000=XX:XX:XX:XX:XX:XX,bridge=vmbr0,rate= sub parse_net { my ($data) = @_; @@ -1970,7 +1917,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}; } @@ -2029,7 +1977,7 @@ 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); @@ -2043,7 +1991,7 @@ 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); @@ -2332,7 +2280,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}; @@ -2355,7 +2303,7 @@ sub shared_nodes { my $nodehash = { map { $_ => 1 } @$nodelist }; my $nodename = nodename(); - foreach_drive($conf, sub { + PVE::QemuConfig->foreach_volume($conf, sub { my ($ds, $drive) = @_; my $volid = $drive->{file}; @@ -2387,7 +2335,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}; @@ -2742,6 +2690,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) = @_; @@ -2933,7 +2907,7 @@ sub query_understood_cpu_flags { } sub config_to_command { - my ($storecfg, $vmid, $conf, $defaults, $forcemachine) = @_; + my ($storecfg, $vmid, $conf, $defaults, $forcemachine, $forcecpu) = @_; my $cmd = []; my $globalFlags = []; @@ -2964,13 +2938,15 @@ sub config_to_command { $machine_version =~ m/(\d+)\.(\d+)/; my ($machine_major, $machine_minor) = ($1, $2); - die "Installed QEMU version '$kvmver' is too old to run machine type '$machine_type', please upgrade node '$nodename'\n" - if !PVE::QemuServer::min_version($kvmver, $machine_major, $machine_minor); - if (!PVE::QemuServer::Machine::can_run_pve_machine_version($machine_version, $kvmver)) { + 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"; + ." 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 @@ -3045,12 +3021,11 @@ sub config_to_command { } } - 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 = parse_drive('efidisk0', $efidisk); my ($storeid, $volname) = PVE::Storage::parse_volume_id($d->{file}, 1); @@ -3131,77 +3106,9 @@ 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/$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 = {}; @@ -3241,22 +3148,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; @@ -3314,7 +3209,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 @@ -3333,13 +3227,17 @@ sub config_to_command { push @$rtcFlags, 'driftfix=slew' if $tdf; - 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, $kvm_off, $machine_version, $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); @@ -3364,20 +3262,16 @@ sub config_to_command { 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"; } - # mostly relevant for /dev/hwrng, but doesn't hurt to check others too - die "cannot create VirtIO RNG device: source file '$rng->{source}' doesn't exist\n" - if ! -e $rng->{source}; - 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"; } @@ -3387,7 +3281,7 @@ sub config_to_command { if ($qxlnum) { if ($qxlnum > 1) { if ($winversion){ - for(my $i = 1; $i < $qxlnum; $i++){ + for (my $i = 1; $i < $qxlnum; $i++){ push @$devices, '-device', print_vga_device($conf, $vga, $arch, $machine_version, $machine_type, $i, $qxlnum, $bridges); } } else { @@ -3450,7 +3344,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)) { @@ -3474,11 +3368,11 @@ 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); @@ -3503,37 +3397,39 @@ sub config_to_command { 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} / $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 $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_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); }); 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); - push @$devices, '-device', $netdevicefull; + my $netdevicefull = print_netdevice_full($vmid, $conf, $d, "net$i", $bridges, $use_old_bios_files, $arch, $machine_type); + push @$devices, '-device', $netdevicefull; } if ($conf->{ivshmem}) { @@ -3569,7 +3465,13 @@ sub config_to_command { for my $k (sort {$b cmp $a} keys %$bridges) { next if $q35 && $k < 4; # q35.cfg already includes bridges up to 3 - $pciaddr = print_pci_addr("pci.$k", undef, $arch, $machine_type); + + 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 @@ -3591,12 +3493,9 @@ sub config_to_command { 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); @@ -3614,6 +3513,24 @@ sub config_to_command { return wantarray ? ($cmd, $vollist, $spice_port) : $cmd; } +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 is 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) = @_; @@ -4027,6 +3944,14 @@ sub qemu_netdevadd { my $netdev = print_netdev_full($vmid, $conf, $arch, $device, $deviceid, 1); my %options = split(/[=,]/, $netdev); + 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; } @@ -4239,7 +4164,7 @@ 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; }); @@ -4277,6 +4202,59 @@ sub set_migration_caps { 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 = { 'lock' => 1, 'name' => 1, @@ -4736,7 +4714,7 @@ sub vm_migrate_get_nbd_disks { my ($storecfg, $conf, $replicated_volumes) = @_; my $local_volumes = {}; - foreach_drive($conf, sub { + PVE::QemuConfig->foreach_volume($conf, sub { my ($ds, $drive) = @_; return if drive_is_cdrom($drive); @@ -4806,15 +4784,18 @@ sub vm_migrate_alloc_nbd_disks { sub vm_start { my ($storecfg, $vmid, $params, $migrate_opts) = @_; - PVE::QemuConfig->lock_config($vmid, sub { + return PVE::QemuConfig->lock_config($vmid, sub { my $conf = PVE::QemuConfig->load_config($vmid, $migrate_opts->{migratedfrom}); - die "you can't start a vm if it's a template\n" if PVE::QemuConfig->is_template($conf); + die "you can't start a vm if it's a template\n" + if !$params->{skiptemplate} && PVE::QemuConfig->is_template($conf); - $params->{resume} = PVE::QemuConfig->has_lock($conf, 'suspended'); + my $has_suspended_lock = PVE::QemuConfig->has_lock($conf, 'suspended'); PVE::QemuConfig->check_lock($conf) - if !($params->{skiplock} || $params->{resume}); + if !($params->{skiplock} || $has_suspended_lock); + + $params->{resume} = $has_suspended_lock || defined($conf->{vmstate}); die "VM $vmid already running\n" if check_running($vmid, undef, $migrate_opts->{migratedfrom}); @@ -4828,7 +4809,7 @@ sub vm_start { } } - vm_start_nolock($storecfg, $vmid, $conf, $params, $migrate_opts); + return vm_start_nolock($storecfg, $vmid, $conf, $params, $migrate_opts); }); } @@ -4836,7 +4817,9 @@ sub vm_start { # 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 @@ -4857,6 +4840,8 @@ sub vm_start_nolock { my $migratedfrom = $migrate_opts->{migratedfrom}; my $migration_type = $migrate_opts->{type}; + my $res = {}; + # clean up leftover reboot request files eval { clear_reboot_request($vmid); }; warn $@ if $@; @@ -4876,13 +4861,16 @@ sub vm_start_nolock { PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'pre-start', 1); my $forcemachine = $params->{forcemachine}; + my $forcecpu = $params->{forcecpu}; if ($resume) { - # enforce machine type on suspended vm to ensure HW compatibility + # enforce machine and CPU type on suspended vm to ensure HW compatibility $forcemachine = $conf->{runningmachine}; + $forcecpu = $conf->{runningcpu}; print "Resuming suspended VM\n"; } - my ($cmd, $vollist, $spice_port) = config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine); + my ($cmd, $vollist, $spice_port) = + config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine, $forcecpu); my $migration_ip; my $get_migration_ip = sub { @@ -4965,7 +4953,7 @@ sub vm_start_nolock { } # host pci devices - for (my $i = 0; $i < $MAX_HOSTPCI_DEVICES; $i++) { + 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}; @@ -5065,6 +5053,7 @@ sub vm_start_nolock { } print "migration listens on $migrate_uri\n" if $migrate_uri; + $res->{migrate_uri} = $migrate_uri; if ($statefile && $statefile ne 'tcp' && $statefile ne 'unix') { eval { mon_cmd($vmid, "cont"); }; @@ -5092,13 +5081,19 @@ 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}; mon_cmd($vmid, "nbd-server-add", device => "drive-$opt", writable => JSON::true ); - print "storage migration listens on $migrate_storage_uri:exportname=drive-$opt volume:$drivestr\n"; + 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}; + + $res->{drives}->{$opt} = $nbd->{$opt}; + $res->{drives}->{$opt}->{nbd_uri} = $nbd_uri; } } @@ -5110,6 +5105,7 @@ sub vm_start_nolock { 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"); @@ -5138,11 +5134,13 @@ sub vm_start_nolock { PVE::Storage::deactivate_volumes($storecfg, [$vmstate]); PVE::Storage::vdisk_free($storecfg, $vmstate); } - delete $conf->@{qw(lock vmstate runningmachine)}; + delete $conf->@{qw(lock vmstate runningmachine runningcpu)}; PVE::QemuConfig->write_config($vmid, $conf); } PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-start'); + + return $res; } sub vm_commandline { @@ -5150,13 +5148,15 @@ sub vm_commandline { 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 a 'runningmachine' in snapshot - $forcemachine = $snapshot->{runningmachine} if $snapshot->{runningmachine}; + # check for machine or CPU overrides in snapshot + $forcemachine = $snapshot->{runningmachine}; + $forcecpu = $snapshot->{runningcpu}; $snapshot->{digest} = $conf->{digest}; # keep file digest for API @@ -5165,7 +5165,8 @@ sub vm_commandline { my $defaults = load_defaults(); - my $cmd = config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine); + my $cmd = config_to_command($storecfg, $vmid, $conf, $defaults, + $forcemachine, $forcecpu); return PVE::Tools::cmd2string($cmd); } @@ -5293,7 +5294,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 { @@ -5439,7 +5444,7 @@ sub vm_suspend { 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 $@; @@ -5541,28 +5546,12 @@ sub tar_restore_cleanup { 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') { @@ -5576,7 +5565,7 @@ sub restore_file_archive { my $restore_cleanup_oldconf = sub { my ($storecfg, $vmid, $oldconf, $virtdev_hash) = @_; - foreach_drive($oldconf, sub { + PVE::QemuConfig->foreach_volume($oldconf, sub { my ($ds, $drive) = @_; return if drive_is_cdrom($drive, 1); @@ -5653,12 +5642,13 @@ my $parse_backup_hints = sub { 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 => $options->{storage} // $storeid, + storeid => $storeid, size => PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE, is_cloudinit => 1, }; @@ -5825,7 +5815,7 @@ 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 = {}; @@ -5853,11 +5843,11 @@ sub update_disk_config { return if drive_is_cdrom($drive); return if !$volid_hash->{$volid}; - my ($updated, $old_size, $new_size) = PVE::QemuServer::Drive::update_disksize($drive, $volid_hash); + my ($updated, $msg) = PVE::QemuServer::Drive::update_disksize($drive, $volid_hash->{$volid}->{size}); if (defined($updated)) { $changes = 1; $conf->{$opt} = print_drive($updated); - print "$prefix size of disk '$volid' ($opt) updated from $old_size to $new_size\n"; + print "$prefix ($opt): $msg\n"; } }); @@ -5956,8 +5946,11 @@ sub restore_proxmox_backup_archive { 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); @@ -6038,7 +6031,7 @@ sub restore_proxmox_backup_archive { } my $fh = IO::File->new($cfgfn, "r") || - "unable to read qemu-server.conf - $!\n"; + die "unable to read qemu-server.conf - $!\n"; my $virtdev_hash = $parse_backup_hints->($rpcenv, $user, $storecfg, $fh, $devinfo, $options); @@ -6060,6 +6053,7 @@ sub restore_proxmox_backup_archive { my $path = PVE::Storage::path($storecfg, $volid); + # 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, @@ -6069,6 +6063,9 @@ sub restore_proxmox_backup_archive { '--verbose', ]; + 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'; } @@ -6149,14 +6146,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); } @@ -6200,7 +6192,7 @@ sub restore_vma_archive { # 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) { @@ -6446,7 +6438,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); @@ -6497,7 +6489,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); @@ -7103,7 +7095,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}); } }