use PVE::Storage;
use PVE::SysFSTools;
use PVE::Systemd;
-use PVE::Tools qw(run_command lock_file lock_file_full file_read_firstline dir_glob_foreach get_host_arch $IPV6RE);
+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::QMPClient;
use PVE::QemuConfig;
return $cmd;
}
+# To use query_supported_cpu_flags and query_understood_cpu_flags to get flags
+# to use in a QEMU command line (-cpu element), first array_intersect the result
+# of query_supported_ with query_understood_. This is necessary because:
+#
+# a) query_understood_ returns flags the host cannot use and
+# b) query_supported_ (rather the QMP call) doesn't actually return CPU
+# flags, but CPU settings - with most of them being flags. Those settings
+# (and some flags, curiously) cannot be specified as a "-cpu" argument.
+#
+# query_supported_ needs to start up to 2 temporary VMs and is therefore rather
+# expensive. If you need the value returned from this, you can get it much
+# cheaper from pmxcfs using PVE::Cluster::get_node_kv('cpuflags-$accel') with
+# $accel being 'kvm' or 'tcg'.
+#
+# pvestatd calls this function on startup and whenever the QEMU/KVM version
+# changes, automatically populating pmxcfs.
+#
+# Returns: { kvm => [ flagX, flagY, ... ], tcg => [ flag1, flag2, ... ] }
+# since kvm and tcg machines support different flags
+#
+sub query_supported_cpu_flags {
+ my $flags = {};
+
+ my ($arch, $default_machine) = get_basic_machine_info();
+
+ # FIXME: Once this is merged, the code below should work for ARM as well:
+ # https://lists.nongnu.org/archive/html/qemu-devel/2019-06/msg04947.html
+ die "QEMU/KVM cannot detect CPU flags on ARM (aarch64)\n" if
+ $arch eq "aarch64";
+
+ my $kvm_supported = defined(kvm_version());
+ my $qemu_cmd = get_command_for_arch($arch);
+ my $fakevmid = -1;
+ my $pidfile = PVE::QemuServer::Helpers::pidfile_name($fakevmid);
+
+ # Start a temporary (frozen) VM with vmid -1 to allow sending a QMP command
+ my $query_supported_run_qemu = sub {
+ my ($kvm) = @_;
+
+ my $flags = {};
+ my $cmd = [
+ $qemu_cmd,
+ '-machine', $default_machine,
+ '-display', 'none',
+ '-chardev', "socket,id=qmp,path=/var/run/qemu-server/$fakevmid.qmp,server,nowait",
+ '-mon', 'chardev=qmp,mode=control',
+ '-pidfile', $pidfile,
+ '-S', '-daemonize'
+ ];
+
+ if (!$kvm) {
+ push @$cmd, '-accel', 'tcg';
+ }
+
+ my $rc = run_command($cmd, noerr => 1, quiet => 0);
+ die "QEMU flag querying VM exited with code " . $rc if $rc;
+
+ eval {
+ my $cmd_result = mon_cmd(
+ $fakevmid,
+ 'query-cpu-model-expansion',
+ type => 'full',
+ model => { name => 'host' }
+ );
+
+ my $props = $cmd_result->{model}->{props};
+ foreach my $prop (keys %$props) {
+ next if $props->{$prop} ne '1';
+ # QEMU returns some flags multiple times, with '_', '.' or '-'
+ # (e.g. lahf_lm and lahf-lm; sse4.2, sse4-2 and sse4_2; ...).
+ # We only keep those with underscores, to match /proc/cpuinfo
+ $prop =~ s/\.|-/_/g;
+ $flags->{$prop} = 1;
+ }
+ };
+ my $err = $@;
+
+ # force stop with 10 sec timeout and 'nocheck'
+ # always stop, even if QMP failed
+ vm_stop(undef, $fakevmid, 1, 1, 10, 0, 1);
+
+ die $err if $err;
+
+ return [ sort keys %$flags ];
+ };
+
+ # We need to query QEMU twice, since KVM and TCG have different supported flags
+ PVE::QemuConfig->lock_config($fakevmid, sub {
+ $flags->{tcg} = eval { $query_supported_run_qemu->(0) };
+ warn "warning: failed querying supported tcg flags: $@\n" if $@;
+
+ if ($kvm_supported) {
+ $flags->{kvm} = eval { $query_supported_run_qemu->(1) };
+ warn "warning: failed querying supported kvm flags: $@\n" if $@;
+ }
+ });
+
+ return $flags;
+}
+
+# Understood CPU flags are written to a file at 'pve-qemu' compile time
+my $understood_cpu_flag_dir = "/usr/share/kvm";
+sub query_understood_cpu_flags {
+ my $arch = get_host_arch();
+ my $filepath = "$understood_cpu_flag_dir/recognized-CPUID-flags-$arch";
+
+ die "Cannot query understood QEMU CPU flags for architecture: $arch (file not found)\n"
+ if ! -e $filepath;
+
+ my $raw = file_get_contents($filepath);
+ $raw =~ s/^\s+|\s+$//g;
+ my @flags = split(/\s+/, $raw);
+
+ return \@flags;
+}
+
sub get_cpu_options {
my ($conf, $arch, $kvm, $kvm_off, $machine_version, $winversion, $gpu_passthrough) = @_;
if ($changes) {
PVE::QemuConfig->write_config($vmid, $conf);
- $conf = PVE::QemuConfig->load_config($vmid); # update/reload
}
my $hotplug_features = parse_hotplug_features(defined($conf->{hotplug}) ? $conf->{hotplug} : '1');
if (my $err = $@) {
&$add_error($opt, $err) if $err ne "skip\n";
} else {
- # save new config if hotplug was successful
delete $conf->{$opt};
PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
- PVE::QemuConfig->write_config($vmid, $conf);
- $conf = PVE::QemuConfig->load_config($vmid); # update/reload
}
}
if (my $err = $@) {
&$add_error($opt, $err) if $err ne "skip\n";
} else {
- # save new config if hotplug was successful
$conf->{$opt} = $value;
delete $conf->{pending}->{$opt};
- PVE::QemuConfig->write_config($vmid, $conf);
- $conf = PVE::QemuConfig->load_config($vmid); # update/reload
}
}
+
+ PVE::QemuConfig->write_config($vmid, $conf);
}
sub try_deallocate_drive {
sub vmconfig_apply_pending {
- my ($vmid, $conf, $storecfg) = @_;
+ my ($vmid, $conf, $storecfg, $errors) = @_;
+
+ my $add_apply_error = sub {
+ my ($opt, $msg) = @_;
+ my $err_msg = "unable to apply pending change $opt : $msg";
+ $errors->{$opt} = $err_msg;
+ warn $err_msg;
+ };
# cold plug
my $pending_delete_hash = PVE::QemuConfig->parse_pending_delete($conf->{pending}->{delete});
foreach my $opt (sort keys %$pending_delete_hash) {
- die "internal error" if $opt =~ m/^unused/;
my $force = $pending_delete_hash->{$opt}->{force};
- $conf = PVE::QemuConfig->load_config($vmid); # update/reload
- if (!defined($conf->{$opt})) {
- PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
- PVE::QemuConfig->write_config($vmid, $conf);
- } elsif (is_valid_drivename($opt)) {
- vmconfig_delete_or_detach_drive($vmid, $storecfg, $conf, $opt, $force);
- PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
- delete $conf->{$opt};
- PVE::QemuConfig->write_config($vmid, $conf);
+ eval {
+ die "internal error" if $opt =~ m/^unused/;
+ $conf = PVE::QemuConfig->load_config($vmid); # update/reload
+ if (!defined($conf->{$opt})) {
+ PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
+ PVE::QemuConfig->write_config($vmid, $conf);
+ } elsif (is_valid_drivename($opt)) {
+ vmconfig_delete_or_detach_drive($vmid, $storecfg, $conf, $opt, $force);
+ PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
+ delete $conf->{$opt};
+ PVE::QemuConfig->write_config($vmid, $conf);
+ } else {
+ PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
+ delete $conf->{$opt};
+ PVE::QemuConfig->write_config($vmid, $conf);
+ }
+ };
+ if (my $err = $@) {
+ $add_apply_error->($opt, $err);
} else {
PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
delete $conf->{$opt};
foreach my $opt (keys %{$conf->{pending}}) { # add/change
$conf = PVE::QemuConfig->load_config($vmid); # update/reload
- if (defined($conf->{$opt}) && ($conf->{$opt} eq $conf->{pending}->{$opt})) {
- # skip if nothing changed
- } elsif (is_valid_drivename($opt)) {
- vmconfig_register_unused_drive($storecfg, $vmid, $conf, parse_drive($opt, $conf->{$opt}))
- if defined($conf->{$opt});
- $conf->{$opt} = $conf->{pending}->{$opt};
+ eval {
+ if (defined($conf->{$opt}) && ($conf->{$opt} eq $conf->{pending}->{$opt})) {
+ # skip if nothing changed
+ } elsif (is_valid_drivename($opt)) {
+ vmconfig_register_unused_drive($storecfg, $vmid, $conf, parse_drive($opt, $conf->{$opt}))
+ if defined($conf->{$opt});
+ $conf->{$opt} = $conf->{pending}->{$opt};
+ } else {
+ $conf->{$opt} = $conf->{pending}->{$opt};
+ }
+ };
+ if (my $err = $@) {
+ $add_apply_error->($opt, $err);
} else {
- $conf->{$opt} = $conf->{pending}->{$opt};
+ $conf->{$opt} = delete $conf->{pending}->{$opt};
+ PVE::QemuConfig->cleanup_pending($conf);
}
- delete $conf->{pending}->{$opt};
PVE::QemuConfig->write_config($vmid, $conf);
}
}
: $defaults->{cpuunits};
my $start_timeout = ($conf->{hugepages} || $is_suspended) ? 300 : 30;
- my %run_params = (timeout => $statefile ? undef : $start_timeout, umask => 0077);
+ my %run_params = (
+ timeout => $statefile ? undef : $start_timeout,
+ umask => 0077,
+ noerr => 1,
+ );
+
+ # when migrating, prefix QEMU output so other side can pick up any
+ # errors that might occur and show the user
+ if ($migratedfrom) {
+ $run_params{quiet} = 1;
+ $run_params{logfunc} = sub { print "QEMU: $_[0]\n" };
+ }
my %properties = (
Slice => 'qemu.slice',
my $run_qemu = sub {
PVE::Tools::run_fork sub {
PVE::Systemd::enter_systemd_scope($vmid, "Proxmox VE VM $vmid", %properties);
- run_command($cmd, %run_params);
+
+ my $exitcode = run_command($cmd, %run_params);
+ die "QEMU exited with code $exitcode\n" if $exitcode;
};
};
}
sub update_disksize {
+ my ($drive, $volid_hash) = @_;
+
+ my $volid = $drive->{file};
+ return undef if !defined($volid);
+
+ my $oldsize = $drive->{size};
+ my $newsize = $volid_hash->{$volid}->{size};
+
+ if (defined($newsize) && defined($oldsize) && $newsize != $oldsize) {
+ $drive->{size} = $newsize;
+
+ my $old_fmt = PVE::JSONSchema::format_size($oldsize);
+ my $new_fmt = PVE::JSONSchema::format_size($newsize);
+
+ return wantarray ? ($drive, $old_fmt, $new_fmt) : $drive;
+ }
+
+ return undef;
+}
+
+sub update_disk_config {
my ($vmid, $conf, $volid_hash) = @_;
my $changes;
my $volid = $drive->{file};
next if !$volid;
+ # mark volid as "in-use" for next step
$referenced->{$volid} = 1;
if ($volid_hash->{$volid} &&
(my $path = $volid_hash->{$volid}->{path})) {
next if drive_is_cdrom($drive);
next if !$volid_hash->{$volid};
- $drive->{size} = $volid_hash->{$volid}->{size};
- my $new = print_drive($drive);
- if ($new ne $conf->{$opt}) {
+ my ($updated, $old_size, $new_size) = update_disksize($drive, $volid_hash);
+ if (defined($updated)) {
$changes = 1;
- $conf->{$opt} = $new;
- print "$prefix update disk '$opt' information.\n";
+ $conf->{$opt} = print_drive($updated);
+ print "$prefix size of disk '$volid' ($opt) updated from $old_size to $new_size\n";
}
}
}
my $volid = $conf->{$opt};
my $path = $volid_hash->{$volid}->{path} if $volid_hash->{$volid};
if ($referenced->{$volid} || ($path && $referencedpath->{$path})) {
- print "$prefix remove entry '$opt', its volume '$volid' is in use.\n";
+ print "$prefix remove entry '$opt', its volume '$volid' is in use\n";
$changes = 1;
delete $conf->{$opt};
}
next if $referencedpath->{$path};
$changes = 1;
my $key = PVE::QemuConfig->add_unused_volume($conf, $volid);
- print "$prefix add unreferenced volume '$volid' as '$key' to config.\n";
+ print "$prefix add unreferenced volume '$volid' as '$key' to config\n";
$referencedpath->{$path} = 1; # avoid to add more than once (aliases)
}
$vm_volids->{$volid} = $info if $info->{vmid} && $info->{vmid} == $vmid;
}
- my $changes = update_disksize($vmid, $conf, $vm_volids);
+ my $changes = update_disk_config($vmid, $conf, $vm_volids);
PVE::QemuConfig->write_config($vmid, $conf) if $changes && !$dryrun;
};
my $conffile = PVE::QemuConfig->config_file($vmid);
my $tmpfn = "$conffile.$$.tmp";
- # Note: $oldconf is undef if VM does not exists
+ # Note: $oldconf is undef if VM does not exist
my $cfs_path = PVE::QemuConfig->cfs_config_path($vmid);
my $oldconf = PVE::Cluster::cfs_read_file($cfs_path);