use PVE::Exception qw(raise raise_param_exc);
use PVE::Format qw(render_duration render_bytes);
use PVE::GuestHelpers qw(safe_string_ne safe_num_ne safe_boolean_ne);
+use PVE::HA::Config;
use PVE::Mapping::PCI;
use PVE::Mapping::USB;
use PVE::INotify;
use PVE::QemuServer::Helpers qw(config_aware_timeout min_version windows_version);
use PVE::QemuServer::Cloudinit;
use PVE::QemuServer::CGroup;
-use PVE::QemuServer::CPUConfig qw(print_cpu_device get_cpu_options);
+use PVE::QemuServer::CPUConfig qw(print_cpu_device get_cpu_options get_cpu_bitness is_native_arch);
use PVE::QemuServer::Drive qw(is_valid_drivename drive_is_cloudinit drive_is_cdrom drive_is_read_only parse_drive print_drive);
use PVE::QemuServer::Machine;
use PVE::QemuServer::Memory qw(get_current_memory);
my $have_sdn;
eval {
require PVE::Network::SDN::Zones;
+ require PVE::Network::SDN::Vnets;
$have_sdn = 1;
};
"$EDK2_FW_BASE/OVMF_CODE_4M.secboot.fd",
"$EDK2_FW_BASE/OVMF_VARS_4M.ms.fd",
],
+ # FIXME: These are legacy 2MB-sized images that modern OVMF doesn't supports to build
+ # anymore. how can we deperacate this sanely without breaking existing instances, or using
+ # older backups and snapshot?
default => [
"$EDK2_FW_BASE/OVMF_CODE.fd",
"$EDK2_FW_BASE/OVMF_VARS.fd",
minimum => 4,
maximum => 512,
},
+ clipboard => {
+ description => 'Enable a specific clipboard. If not set, depending on the display type the'
+ .' SPICE one will be added. Migration with VNC clipboard is not yet supported!',
+ type => 'string',
+ enum => ['vnc'],
+ optional => 1,
+ },
};
my $ivshmem_fmt = {
ostype => {
optional => 1,
type => 'string',
+ # NOTE: When extending, also consider extending `%guest_types` in `Import/ESXi.pm`.
enum => [qw(other wxp w2k w2k3 w2k8 wvista win7 win8 win10 win11 l24 l26 solaris)],
description => "Specify guest operating system.",
verbose_description => <<EODESC,
die "unable to parse hotplug option\n";
}
-sub scsi_inquiry {
- my($fh, $noerr) = @_;
-
- my $SG_IO = 0x2285;
- my $SG_GET_VERSION_NUM = 0x2282;
-
- my $versionbuf = "\x00" x 8;
- my $ret = ioctl($fh, $SG_GET_VERSION_NUM, $versionbuf);
- if (!$ret) {
- die "scsi ioctl SG_GET_VERSION_NUM failoed - $!\n" if !$noerr;
- return;
- }
- my $version = unpack("I", $versionbuf);
- if ($version < 30000) {
- die "scsi generic interface too old\n" if !$noerr;
- return;
- }
-
- my $buf = "\x00" x 36;
- my $sensebuf = "\x00" x 8;
- my $cmd = pack("C x3 C x1", 0x12, 36);
-
- # 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
- );
+sub assert_clipboard_config {
+ my ($vga) = @_;
- $ret = ioctl($fh, $SG_IO, $packet);
- if (!$ret) {
- die "scsi ioctl SG_IO failed - $!\n" if !$noerr;
- return;
- }
+ my $clipboard_regex = qr/^(std|cirrus|vmware|virtio|qxl)/;
- my @res = unpack($sg_io_hdr_t, $packet);
- if ($res[17] || $res[18]) {
- die "scsi ioctl SG_IO status error - $!\n" if !$noerr;
- return;
+ if (
+ $vga->{'clipboard'}
+ && $vga->{'clipboard'} eq 'vnc'
+ && $vga->{type}
+ && $vga->{type} !~ $clipboard_regex
+ ) {
+ die "vga type $vga->{type} is not compatible with VNC clipboard\n";
}
-
- my $res = {};
- $res->@{qw(type removable vendor product revision)} = unpack("C C x6 A8 A16 A4", $buf);
-
- $res->{removable} = $res->{removable} & 128 ? 1 : 0;
- $res->{type} &= 0x1F;
-
- return $res;
-}
-
-sub path_is_scsi {
- my ($path) = @_;
-
- my $fh = IO::File->new("+<$path") || return;
- my $res = scsi_inquiry($fh, 1);
- close($fh);
-
- return $res;
}
sub print_tabletdevice_full {
my ($maxdev, $controller, $controller_prefix) = scsihw_infos($conf, $drive);
my $unit = $drive->{index} % $maxdev;
- my $devicetype = 'hd';
- my $path = '';
- if (drive_is_cdrom($drive)) {
- $devicetype = 'cd';
- } else {
- if ($drive->{file} =~ m|^/|) {
- $path = $drive->{file};
- if (my $info = path_is_scsi($path)) {
- if ($info->{type} == 0 && $drive->{scsiblock}) {
- $devicetype = 'block';
- } elsif ($info->{type} == 1) { # tape
- $devicetype = 'generic';
- }
- }
- } else {
- $path = PVE::Storage::path($storecfg, $drive->{file});
- }
- # for compatibility only, we prefer scsi-hd (#2408, #2355, #2380)
- my $version = extract_version($machine_type, kvm_user_version());
- if ($path =~ m/^iscsi\:\/\// &&
- !min_version($version, 4, 1)) {
- $devicetype = 'generic';
- }
- }
+ my $machine_version = extract_version($machine_type, kvm_user_version());
+ my $device_type = PVE::QemuServer::Drive::get_scsi_device_type(
+ $drive, $storecfg, $machine_version);
if (!$conf->{scsihw} || $conf->{scsihw} =~ m/^lsi/ || $conf->{scsihw} eq 'pvscsi') {
- $device = "scsi-$devicetype,bus=$controller_prefix$controller.0,scsi-id=$unit";
+ $device = "scsi-$device_type,bus=$controller_prefix$controller.0,scsi-id=$unit";
} else {
- $device = "scsi-$devicetype,bus=$controller_prefix$controller.0,channel=0,scsi-id=0"
+ $device = "scsi-$device_type,bus=$controller_prefix$controller.0,channel=0,scsi-id=0"
.",lun=$drive->{index}";
}
$device .= ",drive=drive-$drive_id,id=$drive_id";
- if ($drive->{ssd} && ($devicetype eq 'block' || $devicetype eq 'hd')) {
+ if ($drive->{ssd} && ($device_type eq 'block' || $device_type eq 'hd')) {
$device .= ",rotation_rate=1";
}
$device .= ",wwn=$drive->{wwn}" if $drive->{wwn};
+ # only scsi-hd and scsi-cd support passing vendor and product information
+ if ($device_type eq 'hd' || $device_type eq 'cd') {
+ if (my $vendor = $drive->{vendor}) {
+ $device .= ",vendor=$vendor";
+ }
+ if (my $product = $drive->{product}) {
+ $device .= ",product=$product";
+ }
+ }
+
} elsif ($drive->{interface} eq 'ide' || $drive->{interface} eq 'sata') {
my $maxdev = ($drive->{interface} eq 'sata') ? $PVE::QemuServer::Drive::MAX_SATA_DISKS : 2;
my $controller = int($drive->{index} / $maxdev);
$unit = 0;
}
- my $devicetype = ($drive->{media} && $drive->{media} eq 'cdrom') ? "cd" : "hd";
+ my $device_type = ($drive->{media} && $drive->{media} eq 'cdrom') ? "cd" : "hd";
- $device = "ide-$devicetype";
+ $device = "ide-$device_type";
if ($drive->{interface} eq 'ide') {
$device .= ",bus=ide.$controller,unit=$unit";
} else {
}
$device .= ",drive=drive-$drive_id,id=$drive_id";
- if ($devicetype eq 'hd') {
+ if ($device_type eq 'hd') {
if (my $model = $drive->{model}) {
$model = URI::Escape::uri_unescape($model);
$device .= ",model=$model";
}
sub print_drive_commandline_full {
- my ($storecfg, $vmid, $drive, $pbs_name, $io_uring) = @_;
+ my ($storecfg, $vmid, $drive, $live_restore_name, $io_uring) = @_;
my $path;
my $volid = $drive->{file};
if (drive_is_cdrom($drive)) {
$path = get_iso_path($storecfg, $vmid, $volid);
- die "$drive_id: cannot back cdrom drive with PBS snapshot\n" if $pbs_name;
+ die "$drive_id: cannot back cdrom drive with a live restore image\n" if $live_restore_name;
} else {
if ($storeid) {
$path = PVE::Storage::path($storecfg, $volid);
}
}
- if ($pbs_name) {
+ if ($live_restore_name) {
$format = "rbd" if $is_rbd;
die "$drive_id: Proxmox Backup Server backed drive cannot auto-detect the format\n"
if !$format;
# note: 'detect-zeroes' works per blockdev and we want it to persist
# after the alloc-track is removed, so put it on 'file' directly
- my $dz_param = $pbs_name ? "file.detect-zeroes" : "detect-zeroes";
+ my $dz_param = $live_restore_name ? "file.detect-zeroes" : "detect-zeroes";
$opts .= ",$dz_param=$detectzeroes" if $detectzeroes;
}
- if ($pbs_name) {
- $opts .= ",backing=$pbs_name";
+ if ($live_restore_name) {
+ $opts .= ",backing=$live_restore_name";
$opts .= ",auto-remove=on";
}
- # my $file_param = $pbs_name ? "file.file.filename" : "file";
+ # my $file_param = $live_restore_name ? "file.file.filename" : "file";
my $file_param = "file";
- if ($pbs_name) {
+ if ($live_restore_name) {
# non-rbd drivers require the underlying file to be a seperate block
# node, so add a second .file indirection
$file_param .= ".file" if !$is_rbd;
if length($ifname) >= 16;
my $vhostparam = '';
- if (is_native($arch)) {
+ if (is_native_arch($arch)) {
$vhostparam = ',vhost=on' if kernel_has_vhost_net() && $net->{model} eq 'virtio';
}
}
}
- die "no devicetype for $vga->{type}\n" if !$type;
+ die "no device-type for $vga->{type}\n" if !$type;
my $memory = "";
if ($vgamem_mb) {
});
}
+ eval { delete_ifaces_ipams_ips($conf, $vmid)};
+ warn $@ if $@;
+
if (defined $replacement_conf) {
PVE::QemuConfig->write_config($vmid, $replacement_conf);
} else {
foreach my $k (keys %$conf) {
if ($k =~ m/^usb/) {
my $entry = parse_property_string('pve-qm-usb', $conf->{$k});
- next if $entry->{host} =~ m/^spice$/i;
+ next if $entry->{host} && $entry->{host} =~ m/^spice$/i;
if ($entry->{mapping}) {
$add_missing_mapping->('usb', $k, $entry->{mapping});
push @$mapped_res, $k;
return $1 || 1;
}
-sub is_native($) {
- my ($arch) = @_;
- return get_host_arch() eq $arch;
-}
-
sub get_vm_arch {
my ($conf) = @_;
return $conf->{arch} // get_host_arch();
or die "no OVMF images known for architecture '$arch'\n";
my $type = 'default';
- if ($arch ne "aarch64" && defined($efidisk->{efitype}) && $efidisk->{efitype} eq '4m') {
- $type = $smm ? "4m" : "4m-no-smm";
- $type .= '-ms' if $efidisk->{'pre-enrolled-keys'};
+ if ($arch eq 'x86_64') {
+ if (defined($efidisk->{efitype}) && $efidisk->{efitype} eq '4m') {
+ $type = $smm ? "4m" : "4m-no-smm";
+ $type .= '-ms' if $efidisk->{'pre-enrolled-keys'};
+ } else {
+ # TODO: log_warn about use of legacy images for x86_64 with Promxox VE 9
+ }
}
my ($ovmf_code, $ovmf_vars) = $types->{$type}->@*;
};
sub get_command_for_arch($) {
my ($arch) = @_;
- return '/usr/bin/kvm' if is_native($arch);
+ return '/usr/bin/kvm' if is_native_arch($arch);
my $cmd = $Arch2Qemu->{$arch}
or die "don't know how to emulate architecture '$arch'\n";
sub config_to_command {
my ($storecfg, $vmid, $conf, $defaults, $forcemachine, $forcecpu,
- $pbs_backing) = @_;
+ $live_restore_backing) = @_;
my ($globalFlags, $machineFlags, $rtcFlags) = ([], [], []);
my $devices = [];
my $machine_type = get_vm_machine($conf, $forcemachine, $arch, $add_pve_version);
my $machine_version = extract_version($machine_type, $kvmver);
- $kvm //= 1 if is_native($arch);
+ $kvm //= 1 if is_native_arch($arch);
$machine_version =~ m/(\d+)\.(\d+)/;
my ($machine_major, $machine_minor) = ($1, $2);
}
if ($conf->{bios} && $conf->{bios} eq 'ovmf') {
+ die "OVMF (UEFI) BIOS is not supported on 32-bit CPU types\n"
+ if !$forcecpu && get_cpu_bitness($conf->{cpu}, $arch) == 32;
+
my ($code_drive_str, $var_drive_str) =
print_ovmf_drive_commandlines($conf, $storecfg, $vmid, $arch, $q35, $version_guard);
push $cmd->@*, '-drive', $code_drive_str;
if ($hotplug_features->{cpu} && min_version($machine_version, 2, 7)) {
push @$cmd, '-smp', "1,sockets=$sockets,cores=$cores,maxcpus=$maxcpus";
for (my $i = 2; $i <= $vcpus; $i++) {
- my $cpustr = print_cpu_device($conf,$i);
+ my $cpustr = print_cpu_device($conf, $arch, $i);
push @$cmd, '-device', $cpustr;
}
my $spice_port;
- if ($qxlnum || $vga->{type} =~ /^virtio/) {
+ assert_clipboard_config($vga);
+ my $is_spice = $qxlnum || $vga->{type} =~ /^virtio/;
+
+ if ($is_spice || ($vga->{'clipboard'} && $vga->{'clipboard'} eq 'vnc')) {
if ($qxlnum > 1) {
if ($winversion){
for (my $i = 1; $i < $qxlnum; $i++){
my $pciaddr = print_pci_addr("spice", $bridges, $arch, $machine_type);
- my $pfamily = PVE::Tools::get_host_address_family($nodename);
- my @nodeaddrs = PVE::Tools::getaddrinfo_all('localhost', family => $pfamily);
- die "failed to get an ip address of type $pfamily for 'localhost'\n" if !@nodeaddrs;
-
push @$devices, '-device', "virtio-serial,id=spice$pciaddr";
- push @$devices, '-chardev', "spicevmc,id=vdagent,name=vdagent";
+ if ($vga->{'clipboard'} && $vga->{'clipboard'} eq 'vnc') {
+ push @$devices, '-chardev', 'qemu-vdagent,id=vdagent,name=vdagent,clipboard=on';
+ } else {
+ push @$devices, '-chardev', 'spicevmc,id=vdagent,name=vdagent';
+ }
push @$devices, '-device', "virtserialport,chardev=vdagent,name=com.redhat.spice.0";
- my $localhost = PVE::Network::addr_to_ip($nodeaddrs[0]->{addr});
- $spice_port = PVE::Tools::next_spice_port($pfamily, $localhost);
+ if ($is_spice) {
+ my $pfamily = PVE::Tools::get_host_address_family($nodename);
+ my @nodeaddrs = PVE::Tools::getaddrinfo_all('localhost', family => $pfamily);
+ die "failed to get an ip address of type $pfamily for 'localhost'\n" if !@nodeaddrs;
- my $spice_enhancement_str = $conf->{spice_enhancements} // '';
- my $spice_enhancement = parse_property_string($spice_enhancements_fmt, $spice_enhancement_str);
- if ($spice_enhancement->{foldersharing}) {
- push @$devices, '-chardev', "spiceport,id=foldershare,name=org.spice-space.webdav.0";
- push @$devices, '-device', "virtserialport,chardev=foldershare,name=org.spice-space.webdav.0";
- }
+ my $localhost = PVE::Network::addr_to_ip($nodeaddrs[0]->{addr});
+ $spice_port = PVE::Tools::next_spice_port($pfamily, $localhost);
- my $spice_opts = "tls-port=${spice_port},addr=$localhost,tls-ciphers=HIGH,seamless-migration=on";
- $spice_opts .= ",streaming-video=$spice_enhancement->{videostreaming}"
- if $spice_enhancement->{videostreaming};
+ my $spice_enhancement_str = $conf->{spice_enhancements} // '';
+ my $spice_enhancement = parse_property_string($spice_enhancements_fmt, $spice_enhancement_str);
+ if ($spice_enhancement->{foldersharing}) {
+ push @$devices, '-chardev', "spiceport,id=foldershare,name=org.spice-space.webdav.0";
+ push @$devices, '-device', "virtserialport,chardev=foldershare,name=org.spice-space.webdav.0";
+ }
- push @$devices, '-spice', "$spice_opts";
+ my $spice_opts = "tls-port=${spice_port},addr=$localhost,tls-ciphers=HIGH,seamless-migration=on";
+ $spice_opts .= ",streaming-video=$spice_enhancement->{videostreaming}"
+ if $spice_enhancement->{videostreaming};
+ push @$devices, '-spice', "$spice_opts";
+ }
}
# enable balloon by default, unless explicitly disabled
$ahcicontroller->{$controller}=1;
}
- my $pbs_conf = $pbs_backing->{$ds};
- my $pbs_name = undef;
- if ($pbs_conf) {
- $pbs_name = "drive-$ds-pbs";
- push @$devices, '-blockdev', print_pbs_blockdev($pbs_conf, $pbs_name);
+ my $live_restore = $live_restore_backing->{$ds};
+ my $live_blockdev_name = undef;
+ if ($live_restore) {
+ $live_blockdev_name = $live_restore->{name};
+ push @$devices, '-blockdev', $live_restore->{blockdev};
}
my $drive_cmd = print_drive_commandline_full(
- $storecfg, $vmid, $drive, $pbs_name, min_version($kvmver, 6, 0));
+ $storecfg, $vmid, $drive, $live_blockdev_name, min_version($kvmver, 6, 0));
# extra protection for templates, but SATA and IDE don't support it..
$drive_cmd .= ',readonly=on' if drive_is_read_only($conf, $drive);
if scalar(@{$currentrunningvcpus}) != $currentvcpus;
if (PVE::QemuServer::Machine::machine_version($machine_type, 2, 7)) {
+ my $arch = get_vm_arch($conf);
for (my $i = $currentvcpus+1; $i <= $vcpus; $i++) {
- my $cpustr = print_cpu_device($conf, $i);
+ my $cpustr = print_cpu_device($conf, $arch, $i);
qemu_deviceadd($vmid, $cpustr);
my $retry = 0;
}
sub qemu_volume_snapshot_delete {
- my ($vmid, $deviceid, $storecfg, $volid, $snap) = @_;
+ my ($vmid, $storecfg, $volid, $snap) = @_;
my $running = check_running($vmid);
+ my $attached_deviceid;
- if($running) {
-
- $running = undef;
+ if ($running) {
my $conf = PVE::QemuConfig->load_config($vmid);
PVE::QemuConfig->foreach_volume($conf, sub {
my ($ds, $drive) = @_;
- $running = 1 if $drive->{file} eq $volid;
+ $attached_deviceid = "drive-$ds" if $drive->{file} eq $volid;
});
}
- if ($running && do_snapshots_with_qemu($storecfg, $volid, $deviceid)) {
- mon_cmd($vmid, 'blockdev-snapshot-delete-internal-sync', device => $deviceid, name => $snap);
+ if ($attached_deviceid && do_snapshots_with_qemu($storecfg, $volid, $attached_deviceid)) {
+ mon_cmd(
+ $vmid,
+ 'blockdev-snapshot-delete-internal-sync',
+ device => $attached_deviceid,
+ name => $snap,
+ );
} else {
- PVE::Storage::volume_snapshot_delete($storecfg, $volid, $snap, $running);
+ PVE::Storage::volume_snapshot_delete(
+ $storecfg, $volid, $snap, $attached_deviceid ? 1 : undef);
}
}
my $force = $pending_delete_hash->{$opt}->{force};
eval {
if ($opt eq 'hotplug') {
- die "skip\n" if ($conf->{hotplug} =~ /memory/);
+ die "skip\n" if ($conf->{hotplug} =~ /(cpu|memory)/);
} elsif ($opt eq 'tablet') {
die "skip\n" if !$hotplug_features->{usb};
if ($defaults->{tablet}) {
} elsif ($opt =~ m/^net(\d+)$/) {
die "skip\n" if !$hotplug_features->{network};
vm_deviceunplug($vmid, $conf, $opt);
+ if($have_sdn) {
+ my $net = PVE::QemuServer::parse_net($conf->{$opt});
+ PVE::Network::SDN::Vnets::del_ips_from_mac($net->{bridge}, $net->{macaddr}, $conf->{name});
+ }
} elsif (is_valid_drivename($opt)) {
die "skip\n" if !$hotplug_features->{disk} || $opt =~ m/(ide|sata)(\d+)/;
vm_deviceunplug($vmid, $conf, $opt);
eval {
if ($opt eq 'hotplug') {
die "skip\n" if ($value =~ /memory/) || ($value !~ /memory/ && $conf->{hotplug} =~ /memory/);
+ die "skip\n" if ($value =~ /cpu/) || ($value !~ /cpu/ && $conf->{hotplug} =~ /cpu/);
} elsif ($opt eq 'tablet') {
die "skip\n" if !$hotplug_features->{usb};
if ($value == 1) {
die "internal error";
} elsif (defined($conf->{$opt}) && is_valid_drivename($opt)) {
vmconfig_delete_or_detach_drive($vmid, $storecfg, $conf, $opt, $force);
+ } elsif (defined($conf->{$opt}) && $opt =~ m/^net\d+$/) {
+ if($have_sdn) {
+ my $net = PVE::QemuServer::parse_net($conf->{$opt});
+ eval { PVE::Network::SDN::Vnets::del_ips_from_mac($net->{bridge}, $net->{macaddr}, $conf->{name}) };
+ warn if $@;
+ }
}
};
if (my $err = $@) {
eval {
if (defined($conf->{$opt}) && is_valid_drivename($opt)) {
vmconfig_register_unused_drive($storecfg, $vmid, $conf, parse_drive($opt, $conf->{$opt}))
+ } elsif (defined($conf->{pending}->{$opt}) && $opt =~ m/^net\d+$/) {
+ return if !$have_sdn; # return from eval if SDN is not available
+
+ my $new_net = PVE::QemuServer::parse_net($conf->{pending}->{$opt});
+ if ($conf->{$opt}) {
+ my $old_net = PVE::QemuServer::parse_net($conf->{$opt});
+
+ if (defined($old_net->{bridge}) && defined($old_net->{macaddr}) && (
+ safe_string_ne($old_net->{bridge}, $new_net->{bridge}) ||
+ safe_string_ne($old_net->{macaddr}, $new_net->{macaddr})
+ )) {
+ PVE::Network::SDN::Vnets::del_ips_from_mac($old_net->{bridge}, $old_net->{macaddr}, $conf->{name});
+ }
+ }
+ #fixme: reuse ip if mac change && same bridge
+ PVE::Network::SDN::Vnets::add_next_free_cidr($new_net->{bridge}, $conf->{name}, $new_net->{macaddr}, $vmid, undef, 1);
}
};
if (my $err = $@) {
safe_string_ne($oldnet->{macaddr}, $newnet->{macaddr}) ||
safe_num_ne($oldnet->{queues}, $newnet->{queues}) ||
safe_num_ne($oldnet->{mtu}, $newnet->{mtu}) ||
- !($newnet->{bridge} && $oldnet->{bridge})) { # bridge/nat mode change
+ !($newnet->{bridge} && $oldnet->{bridge})
+ ) { # bridge/nat mode change
# for non online change, we try to hot-unplug
die "skip\n" if !$hotplug;
vm_deviceunplug($vmid, $conf, $opt);
+
+ if ($have_sdn) {
+ PVE::Network::SDN::Vnets::del_ips_from_mac($oldnet->{bridge}, $oldnet->{macaddr}, $conf->{name});
+ }
+
} else {
die "internal error" if $opt !~ m/net(\d+)/;
if (safe_string_ne($oldnet->{bridge}, $newnet->{bridge}) ||
safe_num_ne($oldnet->{tag}, $newnet->{tag}) ||
safe_string_ne($oldnet->{trunks}, $newnet->{trunks}) ||
- safe_num_ne($oldnet->{firewall}, $newnet->{firewall})) {
+ safe_num_ne($oldnet->{firewall}, $newnet->{firewall})
+ ) {
PVE::Network::tap_unplug($iface);
+ #set link_down in guest if bridge or vlan change to notify guest (dhcp renew for example)
+ if (safe_string_ne($oldnet->{bridge}, $newnet->{bridge}) ||
+ safe_num_ne($oldnet->{tag}, $newnet->{tag})
+ ) {
+ qemu_set_link_status($vmid, $opt, 0);
+ }
+
+ if (safe_string_ne($oldnet->{bridge}, $newnet->{bridge})) {
+ if ($have_sdn) {
+ PVE::Network::SDN::Vnets::del_ips_from_mac($oldnet->{bridge}, $oldnet->{macaddr}, $conf->{name});
+ PVE::Network::SDN::Vnets::add_next_free_cidr($newnet->{bridge}, $conf->{name}, $newnet->{macaddr}, $vmid, undef, 1);
+ }
+ }
+
if ($have_sdn) {
PVE::Network::SDN::Zones::tap_plug($iface, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}, $newnet->{trunks}, $newnet->{rate});
} else {
PVE::Network::tap_plug($iface, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}, $newnet->{trunks}, $newnet->{rate});
}
+
+ #set link_up in guest if bridge or vlan change to notify guest (dhcp renew for example)
+ if (safe_string_ne($oldnet->{bridge}, $newnet->{bridge}) ||
+ safe_num_ne($oldnet->{tag}, $newnet->{tag})
+ ) {
+ qemu_set_link_status($vmid, $opt, 1);
+ }
+
} elsif (safe_num_ne($oldnet->{rate}, $newnet->{rate})) {
# Rate can be applied on its own but any change above needs to
# include the rate in tap_plug since OVS resets everything.
}
if ($hotplug) {
+ if ($have_sdn) {
+ PVE::Network::SDN::Vnets::add_next_free_cidr($newnet->{bridge}, $conf->{name}, $newnet->{macaddr}, $vmid, undef, 1);
+ PVE::Network::SDN::Vnets::add_dhcp_mapping($newnet->{bridge}, $newnet->{macaddr}, $vmid, $conf->{name});
+ }
vm_deviceplug($storecfg, $conf, $vmid, $opt, $newnet, $arch, $machine_type);
} else {
die "skip\n";
safe_string_ne($drive->{discard}, $old_drive->{discard}) ||
safe_string_ne($drive->{iothread}, $old_drive->{iothread}) ||
safe_string_ne($drive->{queues}, $old_drive->{queues}) ||
+ safe_string_ne($drive->{product}, $old_drive->{product}) ||
safe_string_ne($drive->{cache}, $old_drive->{cache}) ||
safe_string_ne($drive->{ssd}, $old_drive->{ssd}) ||
+ safe_string_ne($drive->{vendor}, $old_drive->{vendor}) ||
safe_string_ne($drive->{ro}, $old_drive->{ro})) {
die "skip\n";
}
# timeout => in seconds
# paused => start VM in paused state (backup)
# resume => resume from hibernation
-# pbs-backing => {
+# live-restore-backing => {
# sata0 => {
-# repository
-# snapshot
-# keyfile
-# archive
+# name => blockdev-name,
+# blockdev => "arg to the -blockdev command instantiating device named 'name'",
# },
# virtio2 => ...
# }
}
my ($cmd, $vollist, $spice_port, $pci_devices) = config_to_command($storecfg, $vmid,
- $conf, $defaults, $forcemachine, $forcecpu, $params->{'pbs-backing'});
+ $conf, $defaults, $forcemachine, $forcecpu, $params->{'live-restore-backing'});
my $migration_ip;
my $get_migration_ip = sub {
$migrate->{addr} = "[$migrate->{addr}]" if Net::IP::ip_is_ipv6($migrate->{addr});
}
- my $pfamily = PVE::Tools::get_host_address_family($nodename);
- $migrate->{port} = PVE::Tools::next_migrate_port($pfamily);
- $migrate->{uri} = "tcp:$migrate->{addr}:$migrate->{port}";
- push @$cmd, '-incoming', $migrate->{uri};
+ # see #4501: port reservation should be done close to usage - tell QEMU where to listen
+ # via QMP later
+ push @$cmd, '-incoming', 'defer';
push @$cmd, '-S';
} elsif ($statefile eq 'unix') {
eval { PVE::QemuServer::PCI::reserve_pci_usage($pci_reserve_list, $vmid, undef, $pid) };
warn $@ if $@;
- if (defined($res->{migrate})) {
- print "migration listens on $res->{migrate}->{uri}\n";
+ if (defined(my $migrate = $res->{migrate})) {
+ if ($migrate->{proto} eq 'tcp') {
+ my $nodename = nodename();
+ my $pfamily = PVE::Tools::get_host_address_family($nodename);
+ $migrate->{port} = PVE::Tools::next_migrate_port($pfamily);
+ $migrate->{uri} = "tcp:$migrate->{addr}:$migrate->{port}";
+ mon_cmd($vmid, "migrate-incoming", uri => $migrate->{uri});
+ }
+ print "migration listens on $migrate->{uri}\n";
} elsif ($statefile) {
eval { mon_cmd($vmid, "cont"); };
warn $@ if $@;
PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-start');
+ my ($current_machine, $is_deprecated) =
+ PVE::QemuServer::Machine::get_current_qemu_machine($vmid);
+ if ($is_deprecated) {
+ log_warn(
+ "current machine version '$current_machine' is deprecated - see the documentation and ".
+ "change to a newer one",
+ );
+ }
+
return $res;
}
my $dev_sysfs_dir = "/sys/bus/mdev/devices/$uuid";
# some nvidia vgpu driver versions want to clean the mdevs up themselves, and error
- # out when we do it first. so wait for 10 seconds and then try it
- if ($d->{ids}->[0]->[0]->{vendor} =~ m/^(0x)?10de$/) {
- sleep 10;
+ # out when we do it first. so wait for up to 10 seconds and then try it manually
+ if ($d->{ids}->[0]->[0]->{vendor} =~ m/^(0x)?10de$/ && -e $dev_sysfs_dir) {
+ my $count = 0;
+ while (-e $dev_sysfs_dir && $count < 10) {
+ sleep 1;
+ $count++;
+ }
+ print "waited $count seconds for mediated device driver finishing clean up\n";
}
- PVE::SysFSTools::file_write("$dev_sysfs_dir/remove", "1") if -e $dev_sysfs_dir;
+ if (-e $dev_sysfs_dir) {
+ print "actively clean up mediated device with UUID $uuid\n";
+ PVE::SysFSTools::file_write("$dev_sysfs_dir/remove", "1");
+ }
}
}
PVE::QemuServer::PCI::remove_pci_reservation($vmid);
print "starting VM for live-restore\n";
print "repository: '$opts->{repo}', snapshot: '$opts->{snapshot}'\n";
- my $pbs_backing = {};
+ my $live_restore_backing = {};
for my $ds (keys %$restored_disks) {
$ds =~ m/^drive-(.*)$/;
my $confname = $1;
- $pbs_backing->{$confname} = {
+ my $pbs_conf = {};
+ $pbs_conf = {
repository => $opts->{repo},
snapshot => $opts->{snapshot},
archive => "$ds.img.fidx",
};
- $pbs_backing->{$confname}->{keyfile} = $opts->{keyfile} if -e $opts->{keyfile};
- $pbs_backing->{$confname}->{namespace} = $opts->{namespace} if defined($opts->{namespace});
+ $pbs_conf->{keyfile} = $opts->{keyfile} if -e $opts->{keyfile};
+ $pbs_conf->{namespace} = $opts->{namespace} if defined($opts->{namespace});
my $drive = parse_drive($confname, $conf->{$confname});
print "restoring '$ds' to '$drive->{file}'\n";
+
+ my $pbs_name = "drive-${confname}-pbs";
+ $live_restore_backing->{$confname} = {
+ name => $pbs_name,
+ blockdev => print_pbs_blockdev($pbs_conf, $pbs_name),
+ };
}
my $drives_streamed = 0;
# start VM with backing chain pointing to PBS backup, environment vars for PBS driver
# in QEMU (PBS_PASSWORD and PBS_FINGERPRINT) are already set by our caller
- vm_start_nolock($storecfg, $vmid, $conf, {paused => 1, 'pbs-backing' => $pbs_backing}, {});
+ vm_start_nolock($storecfg, $vmid, $conf, {paused => 1, 'live-restore-backing' => $live_restore_backing}, {});
my $qmeventd_fd = register_qmeventd_handle($vmid);
}
}
+# Inspired by pbs live-restore, this restores with the disks being available as files.
+# Theoretically this can also be used to quick-start a full-clone vm if the
+# disks are all available as files.
+#
+# The mapping should provide a path by config entry, such as
+# `{ scsi0 => { format => <qcow2|raw|...>, path => "/path/to/file", sata1 => ... } }`
+#
+# This is used when doing a `create` call with the `--live-import` parameter,
+# where the disks get an `import-from=` property. The non-live part is
+# therefore already handled in the `$create_disks()` call happening in the
+# `create` api call
+sub live_import_from_files {
+ my ($mapping, $vmid, $conf, $restore_options) = @_;
+
+ my $live_restore_backing = {};
+ for my $dev (keys %$mapping) {
+ die "disk not support for live-restoring: '$dev'\n"
+ if !is_valid_drivename($dev) || $dev =~ /^(?:efidisk|tpmstate)/;
+
+ die "mapping contains disk '$dev' which does not exist in the config\n"
+ if !exists($conf->{$dev});
+
+ my $info = $mapping->{$dev};
+ my ($format, $path) = $info->@{qw(format path)};
+ die "missing path for '$dev' mapping\n" if !$path;
+ die "missing format for '$dev' mapping\n" if !$format;
+ die "invalid format '$format' for '$dev' mapping\n"
+ if !grep { $format eq $_ } qw(raw qcow2 vmdk);
+
+ $live_restore_backing->{$dev} = {
+ name => "drive-$dev-restore",
+ blockdev => "driver=$format,node-name=drive-$dev-restore"
+ . ",read-only=on"
+ . ",file.driver=file,file.filename=$path"
+ };
+ };
+
+ my $storecfg = PVE::Storage::config();
+ eval {
+
+ # make sure HA doesn't interrupt our restore by stopping the VM
+ if (PVE::HA::Config::vm_is_ha_managed($vmid)) {
+ run_command(['ha-manager', 'set', "vm:$vmid", '--state', 'started']);
+ }
+
+ vm_start_nolock($storecfg, $vmid, $conf, {paused => 1, 'live-restore-backing' => $live_restore_backing}, {});
+
+ # prevent shutdowns from qmeventd when the VM powers off from the inside
+ my $qmeventd_fd = register_qmeventd_handle($vmid);
+
+ # begin streaming, i.e. data copy from PBS to target disk for every vol,
+ # this will effectively collapse the backing image chain consisting of
+ # [target <- alloc-track -> PBS snapshot] to just [target] (alloc-track
+ # removes itself once all backing images vanish with 'auto-remove=on')
+ my $jobs = {};
+ for my $ds (sort keys %$live_restore_backing) {
+ my $job_id = "restore-$ds";
+ mon_cmd($vmid, 'block-stream',
+ 'job-id' => $job_id,
+ device => "drive-$ds",
+ );
+ $jobs->{$job_id} = {};
+ }
+
+ mon_cmd($vmid, 'cont');
+ qemu_drive_mirror_monitor($vmid, undef, $jobs, 'auto', 0, 'stream');
+
+ print "restore-drive jobs finished successfully, removing all tracking block devices\n";
+
+ for my $ds (sort keys %$live_restore_backing) {
+ mon_cmd($vmid, 'blockdev-del', 'node-name' => "drive-$ds-restore");
+ }
+
+ close($qmeventd_fd);
+ };
+
+ my $err = $@;
+
+ if ($err) {
+ warn "An error occurred during live-restore: $err\n";
+ _do_vm_stop($storecfg, $vmid, 1, 1, 10, 0, 1);
+ die "live-restore failed\n";
+ }
+
+ PVE::QemuConfig->remove_lock($vmid, "import");
+}
+
sub restore_vma_archive {
my ($archive, $vmid, $user, $opts, $comp) = @_;
sub qemu_img_format {
my ($scfg, $volname) = @_;
- if ($scfg->{path} && $volname =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
+ # FIXME: this entire function is kind of weird given that `parse_volname`
+ # also already gives us a format?
+ my $is_path_storage = $scfg->{path} || $scfg->{type} eq 'esxi';
+
+ if ($is_path_storage && $volname =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
return $1;
} else {
return "raw";
sub nbd_stop {
my ($vmid) = @_;
- mon_cmd($vmid, 'nbd-server-stop');
+ mon_cmd($vmid, 'nbd-server-stop', timeout => 25);
}
sub create_reboot_request {
}
sub vm_is_paused {
- my ($vmid) = @_;
+ my ($vmid, $include_suspended) = @_;
my $qmpstatus = eval {
PVE::QemuConfig::assert_config_exists_on_node($vmid);
mon_cmd($vmid, "query-status");
warn "$@\n" if $@;
return $qmpstatus && (
$qmpstatus->{status} eq "paused" ||
- $qmpstatus->{status} eq "suspended" ||
- $qmpstatus->{status} eq "prelaunch"
+ $qmpstatus->{status} eq "prelaunch" ||
+ ($include_suspended && $qmpstatus->{status} eq "suspended")
);
}
next;
}
if ($have_sdn) {
- PVE::Network::SDN::Zones::add_bridge_fdb($iface, $mac, $bridge, $net->{firewall});
+ PVE::Network::SDN::Zones::add_bridge_fdb($iface, $mac, $bridge);
} elsif (-d "/sys/class/net/$bridge/bridge") { # avoid fdb management with OVS for now
- PVE::Network::add_bridge_fdb($iface, $mac, $net->{firewall});
+ PVE::Network::add_bridge_fdb($iface, $mac);
}
}
}
my $bridge = $net->{bridge};
if ($have_sdn) {
- PVE::Network::SDN::Zones::del_bridge_fdb($iface, $mac, $bridge, $net->{firewall});
+ PVE::Network::SDN::Zones::del_bridge_fdb($iface, $mac, $bridge);
} elsif (-d "/sys/class/net/$bridge/bridge") { # avoid fdb management with OVS for now
- PVE::Network::del_bridge_fdb($iface, $mac, $net->{firewall});
+ PVE::Network::del_bridge_fdb($iface, $mac);
+ }
+ }
+}
+
+sub create_ifaces_ipams_ips {
+ my ($conf, $vmid) = @_;
+
+ return if !$have_sdn;
+
+ foreach my $opt (keys %$conf) {
+ if ($opt =~ m/^net(\d+)$/) {
+ my $value = $conf->{$opt};
+ my $net = PVE::QemuServer::parse_net($value);
+ eval { PVE::Network::SDN::Vnets::add_next_free_cidr($net->{bridge}, $conf->{name}, $net->{macaddr}, $vmid, undef, 1) };
+ warn $@ if $@;
+ }
+ }
+}
+
+sub delete_ifaces_ipams_ips {
+ my ($conf, $vmid) = @_;
+
+ return if !$have_sdn;
+
+ foreach my $opt (keys %$conf) {
+ if ($opt =~ m/^net(\d+)$/) {
+ my $net = PVE::QemuServer::parse_net($conf->{$opt});
+ eval { PVE::Network::SDN::Vnets::del_ips_from_mac($net->{bridge}, $net->{macaddr}, $conf->{name}) };
+ warn $@ if $@;
}
}
}