From: Dominik Csapak <d.csapak@proxmox.com>
To: Fiona Ebner <f.ebner@proxmox.com>, pve-devel@lists.proxmox.com
Subject: Re: [PATCH qemu-server 3/3] api: update vm: fork before locking
Date: Fri, 3 Jul 2026 11:41:53 +0200 [thread overview]
Message-ID: <4d763f1f-59e0-4ad5-b7c1-72b4e5b65dd1@proxmox.com> (raw)
In-Reply-To: <20260629124625.115457-4-f.ebner@proxmox.com>
one remark/question:
would it make for a nicer interface for the load_and_check_config
if we'd pass the delete list into it instead of using
catching the outer closure variable?
that way we wouldn't have to guard the 'push @delete' statements
and could simply pass an empty list into it when we don't want
to add them.
the only difference it would make is that inside, we wouldn't
delete the $conf->{lock} there but since we discard that
before using it when we don't want to update delete this
should be fine?
but this is not super important and the code is fine as is too
so with or without this changed consider this
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
On 6/29/26 2:47 PM, Fiona Ebner wrote:
> Use the familiar early+repeated checks pattern from other API calls to
> do the forking before locking. The only intended functional changes
> are with regard to locking/forking.
>
> Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
> ---
>
> NOTE: best viewed with --ignore-all-space
>
> src/PVE/API2/Qemu.pm | 767 ++++++++++++++++++++++---------------------
> 1 file changed, 389 insertions(+), 378 deletions(-)
>
> diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
> index b2a15d96..2e818127 100644
> --- a/src/PVE/API2/Qemu.pm
> +++ b/src/PVE/API2/Qemu.pm
> @@ -2172,7 +2172,8 @@ my $update_vm_api = sub {
>
> PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $param);
>
> - my $updatefn = sub {
> + my $load_and_check_config = sub {
> + my ($update_delete) = @_;
>
> my $conf = PVE::QemuConfig->load_config($vmid);
>
> @@ -2181,15 +2182,17 @@ my $update_vm_api = sub {
>
> &$check_cpu_model_access($rpcenv, $authuser, $param, $conf);
>
> - # FIXME: 'suspended' lock should probabyl be a state or "weak" lock?!
> + # FIXME: 'suspended' lock should probably be a state or "weak" lock?!
> if (scalar(@delete) && grep { $_ eq 'vmstate' } @delete) {
> if (defined($conf->{lock}) && $conf->{lock} eq 'suspended') {
> delete $conf->{lock}; # for check lock check, not written out
> - push @delete, 'lock'; # this is the real deal to write it out
> + push @delete, 'lock' if $update_delete; # this is the real deal to write it out
> + }
> + if ($update_delete) {
> + push @delete, 'runningmachine' if $conf->{runningmachine};
> + push @delete, 'runningcpu' if $conf->{runningcpu};
> + push @delete, 'running-nets-host-mtu' if $conf->{'running-nets-host-mtu'};
> }
> - push @delete, 'runningmachine' if $conf->{runningmachine};
> - push @delete, 'runningcpu' if $conf->{runningcpu};
> - push @delete, 'running-nets-host-mtu' if $conf->{'running-nets-host-mtu'};
> }
>
> PVE::QemuConfig->check_lock($conf) if !$skiplock;
> @@ -2198,7 +2201,7 @@ my $update_vm_api = sub {
> if (defined($conf->{$opt})) {
> $param->{$opt} = $conf->{$opt};
> } elsif (defined($conf->{pending}->{$opt})) {
> - push @delete, $opt;
> + push @delete, $opt if $update_delete;
> }
> }
>
> @@ -2214,379 +2217,387 @@ my $update_vm_api = sub {
> if $balloon && $balloon > $maxmem;
> }
>
> - PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join(' ', @paramarr));
> -
> - my $worker = sub {
> -
> - print "update VM $vmid: " . join(' ', @paramarr) . "\n";
> -
> - # write updates to pending section
> -
> - my $modified = {}; # record what $option we modify
> -
> - my @bootorder;
> - if (my $boot = $conf->{boot}) {
> - my $bootcfg = PVE::JSONSchema::parse_property_string('pve-qm-boot', $boot);
> - @bootorder = PVE::Tools::split_list($bootcfg->{order})
> - if $bootcfg && $bootcfg->{order};
> - }
> - my $bootorder_deleted = grep { $_ eq 'bootorder' } @delete;
> -
> - my $check_drive_perms = sub {
> - my ($opt, $val) = @_;
> - my $drive = PVE::QemuServer::parse_drive($opt, $val, 1);
> - if (PVE::QemuServer::drive_is_cloudinit($drive)) {
> - $rpcenv->check_vm_perm(
> - $authuser,
> - $vmid,
> - undef,
> - ['VM.Config.Cloudinit', 'VM.Config.CDROM'],
> - );
> - } elsif (PVE::QemuServer::drive_is_cdrom($drive, 1)) { # CDROM
> - $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
> - } else {
> - $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
> -
> - }
> - };
> -
> - foreach my $opt (@delete) {
> - $modified->{$opt} = 1;
> - $conf = PVE::QemuConfig->load_config($vmid); # update/reload
> -
> - # value of what we want to delete, independent if pending or not
> - my $val = $conf->{$opt} // $conf->{pending}->{$opt};
> - if (!defined($val)) {
> - warn "cannot delete '$opt' - not set in current configuration!\n";
> - $modified->{$opt} = 0;
> - next;
> - }
> - my $is_pending_val = defined($conf->{pending}->{$opt});
> - delete $conf->{pending}->{$opt};
> -
> - # remove from bootorder if necessary
> - if (!$bootorder_deleted && @bootorder && grep { $_ eq $opt } @bootorder) {
> - @bootorder = grep { $_ ne $opt } @bootorder;
> - $conf->{pending}->{boot} = PVE::QemuServer::print_bootorder(\@bootorder);
> - $modified->{boot} = 1;
> - }
> -
> - if ($opt =~ m/^unused/) {
> - my $drive = PVE::QemuServer::parse_drive($opt, $val);
> - PVE::QemuConfig->check_protection(
> - $conf,
> - "can't remove unused disk '$drive->{file}'",
> - );
> - $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
> - if (PVE::QemuServer::try_deallocate_drive(
> - $storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser,
> - )) {
> - delete $conf->{$opt};
> - PVE::QemuConfig->write_config($vmid, $conf);
> - }
> - } elsif ($opt eq 'vmstate') {
> - PVE::QemuConfig->check_protection($conf, "can't remove vmstate '$val'");
> - if (PVE::QemuServer::try_deallocate_drive(
> - $storecfg,
> - $vmid,
> - $conf,
> - $opt,
> - { file => $val },
> - $rpcenv,
> - $authuser,
> - 1,
> - )) {
> - delete $conf->{$opt};
> - PVE::QemuConfig->write_config($vmid, $conf);
> - }
> - } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
> - PVE::QemuConfig->check_protection($conf, "can't remove drive '$opt'");
> - $check_drive_perms->($opt, $val);
> - PVE::QemuServer::vmconfig_register_unused_drive(
> - $storecfg,
> - $vmid,
> - $conf,
> - PVE::QemuServer::parse_drive($opt, $val),
> - ) if $is_pending_val;
> - PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> - PVE::QemuConfig->write_config($vmid, $conf);
> - } elsif ($opt =~ m/^serial\d+$/) {
> - if ($val eq 'socket') {
> - $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
> - } elsif ($authuser ne 'root@pam') {
> - die "only root can delete '$opt' config for real devices\n";
> - }
> - PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> - PVE::QemuConfig->write_config($vmid, $conf);
> - } elsif ($opt =~ m/^usb\d+$/) {
> - check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
> - PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> - PVE::QemuConfig->write_config($vmid, $conf);
> - } elsif ($opt =~ m/^hostpci\d+$/) {
> - check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
> - PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> - PVE::QemuConfig->write_config($vmid, $conf);
> - } elsif ($opt =~ m/^rng\d+$/) {
> - check_rng_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
> - PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> - PVE::QemuConfig->write_config($vmid, $conf);
> - } elsif ($opt =~ m/^virtiofs\d$/) {
> - check_dir_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
> - PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> - PVE::QemuConfig->write_config($vmid, $conf);
> - } elsif ($opt eq 'tags') {
> - assert_tag_permissions($vmid, $val, '', $rpcenv, $authuser);
> - delete $conf->{$opt};
> - PVE::QemuConfig->write_config($vmid, $conf);
> - } elsif ($opt =~ m/^net\d+$/) {
> - if ($conf->{$opt}) {
> - PVE::QemuServer::check_bridge_access(
> - $rpcenv,
> - $authuser,
> - { $opt => $conf->{$opt} },
> - );
> - }
> - PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> - PVE::QemuConfig->write_config($vmid, $conf);
> - } else {
> - PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> - PVE::QemuConfig->write_config($vmid, $conf);
> - }
> - }
> -
> - foreach my $opt (keys %$param) { # add/change
> - $modified->{$opt} = 1;
> - $conf = PVE::QemuConfig->load_config($vmid); # update/reload
> - next
> - if defined($conf->{pending}->{$opt})
> - && ($param->{$opt} eq $conf->{pending}->{$opt}); # skip if nothing changed
> -
> - my $arch = PVE::QemuServer::Helpers::get_vm_arch($conf);
> -
> - if (PVE::QemuServer::is_valid_drivename($opt)) {
> - # old drive
> - if ($conf->{$opt}) {
> - $check_drive_perms->($opt, $conf->{$opt});
> - prohibit_tpm_version_change($conf->{$opt}, $param->{$opt})
> - if $opt eq 'tpmstate0';
> - }
> -
> - # new drive
> - $check_drive_perms->($opt, $param->{$opt});
> - PVE::QemuServer::vmconfig_register_unused_drive(
> - $storecfg,
> - $vmid,
> - $conf,
> - PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}),
> - ) if defined($conf->{pending}->{$opt});
> -
> - assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt})
> - if $opt =~ m/^scsi\d+$/;
> -
> - my (undef, $created_opts) = create_disks(
> - $rpcenv,
> - $authuser,
> - $conf,
> - $arch,
> - $storecfg,
> - $vmid,
> - undef,
> - { $opt => $param->{$opt} },
> - undef,
> - undef,
> - $extraction_storage,
> - );
> - $conf->{pending}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
> -
> - # default legacy boot order implies all cdroms anyway
> - if (@bootorder) {
> - # append new CD drives to bootorder to mark them bootable
> - my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}, 1);
> - if (
> - PVE::QemuServer::drive_is_cdrom($drive, 1)
> - && !grep(/^$opt$/, @bootorder)
> - ) {
> - push @bootorder, $opt;
> - $conf->{pending}->{boot} =
> - PVE::QemuServer::print_bootorder(\@bootorder);
> - $modified->{boot} = 1;
> - }
> - }
> - } elsif ($opt =~ m/^serial\d+/) {
> - if (
> - (!defined($conf->{$opt}) || $conf->{$opt} eq 'socket')
> - && $param->{$opt} eq 'socket'
> - ) {
> - $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
> - } elsif ($authuser ne 'root@pam') {
> - die "only root can modify '$opt' config for real devices\n";
> - }
> - $conf->{pending}->{$opt} = $param->{$opt};
> - } elsif ($opt eq 'vga') {
> - my $vga = PVE::QemuServer::parse_vga($param->{$opt});
> - PVE::QemuServer::assert_clipboard_config($vga);
> - $conf->{pending}->{$opt} = $param->{$opt};
> - } elsif ($opt =~ m/^usb\d+/) {
> - if (my $olddevice = $conf->{$opt}) {
> - check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $conf->{$opt});
> - }
> - check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
> - $conf->{pending}->{$opt} = $param->{$opt};
> - } elsif ($opt =~ m/^hostpci\d+$/) {
> - if (my $oldvalue = $conf->{$opt}) {
> - check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
> - }
> - check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
> - $conf->{pending}->{$opt} = $param->{$opt};
> - } elsif ($opt =~ m/^rng\d+$/) {
> - if (my $oldvalue = $conf->{$opt}) {
> - check_rng_perm($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
> - }
> - check_rng_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
> - $conf->{pending}->{$opt} = $param->{$opt};
> - } elsif ($opt =~ m/^virtiofs\d$/) {
> - if (my $oldvalue = $conf->{$opt}) {
> - check_dir_perm($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
> - }
> - check_dir_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
> - $conf->{pending}->{$opt} = $param->{$opt};
> - } elsif ($opt eq 'tags') {
> - assert_tag_permissions(
> - $vmid, $conf->{$opt}, $param->{$opt}, $rpcenv, $authuser,
> - );
> - $conf->{pending}->{$opt} =
> - PVE::GuestHelpers::get_unique_tags($param->{$opt});
> - } elsif ($opt =~ m/^net\d+$/) {
> - if ($conf->{$opt}) {
> - PVE::QemuServer::check_bridge_access(
> - $rpcenv,
> - $authuser,
> - { $opt => $conf->{$opt} },
> - );
> - }
> - $conf->{pending}->{$opt} = $param->{$opt};
> - } elsif ($opt eq 'machine') {
> - my $machine_conf = PVE::QemuServer::Machine::parse_machine($param->{$opt});
> - PVE::QemuServer::Machine::assert_valid_machine_property($machine_conf);
> - $conf->{pending}->{$opt} = $param->{$opt};
> - } elsif ($opt eq 'ostype') {
> - # Check if machine version pinning is needed when switching OS type, just like
> - # upon creation. Skip if 'machine' is explicitly set or removed at the same time
> - # to honor the users request. While it should be enough to look at $modified,
> - # because 'machine' is sorted before 'ostype', be explicit just to be sure.
> - if (
> - !defined($param->{machine})
> - && !defined($conf->{pending}->{machine})
> - && !$modified->{machine} # detects deletion
> - ) {
> - eval {
> - my $machine_string =
> - PVE::QemuServer::Machine::check_and_pin_machine_string(
> - $conf->{machine}, $param->{ostype}, $arch,
> - );
> - $conf->{pending}->{machine} = $machine_string if $machine_string;
> - };
> - print "automatic pinning of machine version failed - $@" if $@;
> - }
> - $conf->{pending}->{$opt} = $param->{$opt};
> - } elsif ($opt eq 'cipassword') {
> - if (!PVE::QemuServer::Helpers::windows_version($conf->{ostype})) {
> - # Same logic as in cloud-init (but with the regex fixed...)
> - $param->{cipassword} = PVE::Tools::encrypt_pw($param->{cipassword})
> - if $param->{cipassword} !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
> - }
> - $conf->{cipassword} = $param->{cipassword};
> - } else {
> - $conf->{pending}->{$opt} = $param->{$opt};
> -
> - if ($opt eq 'boot') {
> - my $new_bootcfg =
> - PVE::JSONSchema::parse_property_string('pve-qm-boot', $param->{$opt});
> - if ($new_bootcfg->{order}) {
> - my @devs = PVE::Tools::split_list($new_bootcfg->{order});
> - for my $dev (@devs) {
> - my $exists =
> - $conf->{$dev} || $conf->{pending}->{$dev} || $param->{$dev};
> - my $deleted = grep { $_ eq $dev } @delete;
> - die "invalid bootorder: device '$dev' does not exist'\n"
> - if !$exists || $deleted;
> - }
> -
> - # remove legacy boot order settings if new one set
> - $conf->{pending}->{$opt} = PVE::QemuServer::print_bootorder(\@devs);
> - PVE::QemuConfig->add_to_pending_delete($conf, "bootdisk")
> - if $conf->{bootdisk};
> - }
> - }
> - }
> - PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
> - PVE::QemuConfig->write_config($vmid, $conf);
> - }
> -
> - # remove pending changes when nothing changed
> - $conf = PVE::QemuConfig->load_config($vmid); # update/reload
> - my $changes = PVE::QemuConfig->cleanup_pending($conf);
> - PVE::QemuConfig->write_config($vmid, $conf) if $changes;
> -
> - return if !scalar(keys %{ $conf->{pending} });
> -
> - my $running = PVE::QemuServer::check_running($vmid);
> -
> - # apply pending changes
> -
> - $conf = PVE::QemuConfig->load_config($vmid); # update/reload
> -
> - my $errors = {};
> - if ($running) {
> - PVE::QemuServer::vmconfig_hotplug_pending(
> - $vmid, $conf, $storecfg, $modified, $errors,
> - );
> - } else {
> - # cloud_init must be skipped if we are in an incoming, remote live migration
> - PVE::QemuServer::vmconfig_apply_pending(
> - $vmid, $conf, $storecfg, $errors, $skip_cloud_init,
> - );
> - }
> - raise_param_exc($errors) if scalar(keys %$errors);
> -
> - return;
> - };
> -
> - if ($sync) {
> - &$worker();
> - return;
> - } else {
> - my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
> -
> - if ($background_delay) {
> -
> - # Note: It would be better to do that in the Event based HTTPServer
> - # to avoid blocking call to sleep.
> -
> - my $end_time = time() + $background_delay;
> -
> - my $task = PVE::Tools::upid_decode($upid);
> -
> - my $running = 1;
> - while (time() < $end_time) {
> - $running =
> - PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
> - last if !$running;
> - sleep(1); # this gets interrupted when child process ends
> - }
> -
> - if (!$running) {
> - my $status = PVE::Tools::upid_read_status($upid);
> - return if !PVE::Tools::upid_status_is_error($status);
> - die "failed to update VM $vmid: $status\n";
> - }
> - }
> -
> - return $upid;
> - }
> + return $conf;
> };
>
> - return PVE::QemuConfig->lock_config($vmid, $updatefn);
> + my $updatefn = sub {
> + my $conf = $load_and_check_config->(1);
> +
> + PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join(' ', @paramarr));
> +
> + print "update VM $vmid: " . join(' ', @paramarr) . "\n";
> +
> + # write updates to pending section
> +
> + my $modified = {}; # record what $option we modify
> +
> + my @bootorder;
> + if (my $boot = $conf->{boot}) {
> + my $bootcfg = PVE::JSONSchema::parse_property_string('pve-qm-boot', $boot);
> + @bootorder = PVE::Tools::split_list($bootcfg->{order})
> + if $bootcfg && $bootcfg->{order};
> + }
> + my $bootorder_deleted = grep { $_ eq 'bootorder' } @delete;
> +
> + my $check_drive_perms = sub {
> + my ($opt, $val) = @_;
> + my $drive = PVE::QemuServer::parse_drive($opt, $val, 1);
> + if (PVE::QemuServer::drive_is_cloudinit($drive)) {
> + $rpcenv->check_vm_perm(
> + $authuser,
> + $vmid,
> + undef,
> + ['VM.Config.Cloudinit', 'VM.Config.CDROM'],
> + );
> + } elsif (PVE::QemuServer::drive_is_cdrom($drive, 1)) { # CDROM
> + $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
> + } else {
> + $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
> +
> + }
> + };
> +
> + foreach my $opt (@delete) {
> + $modified->{$opt} = 1;
> + $conf = PVE::QemuConfig->load_config($vmid); # update/reload
> +
> + # value of what we want to delete, independent if pending or not
> + my $val = $conf->{$opt} // $conf->{pending}->{$opt};
> + if (!defined($val)) {
> + warn "cannot delete '$opt' - not set in current configuration!\n";
> + $modified->{$opt} = 0;
> + next;
> + }
> + my $is_pending_val = defined($conf->{pending}->{$opt});
> + delete $conf->{pending}->{$opt};
> +
> + # remove from bootorder if necessary
> + if (!$bootorder_deleted && @bootorder && grep { $_ eq $opt } @bootorder) {
> + @bootorder = grep { $_ ne $opt } @bootorder;
> + $conf->{pending}->{boot} = PVE::QemuServer::print_bootorder(\@bootorder);
> + $modified->{boot} = 1;
> + }
> +
> + if ($opt =~ m/^unused/) {
> + my $drive = PVE::QemuServer::parse_drive($opt, $val);
> + PVE::QemuConfig->check_protection(
> + $conf,
> + "can't remove unused disk '$drive->{file}'",
> + );
> + $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
> + if (PVE::QemuServer::try_deallocate_drive(
> + $storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser,
> + )) {
> + delete $conf->{$opt};
> + PVE::QemuConfig->write_config($vmid, $conf);
> + }
> + } elsif ($opt eq 'vmstate') {
> + PVE::QemuConfig->check_protection($conf, "can't remove vmstate '$val'");
> + if (PVE::QemuServer::try_deallocate_drive(
> + $storecfg,
> + $vmid,
> + $conf,
> + $opt,
> + { file => $val },
> + $rpcenv,
> + $authuser,
> + 1,
> + )) {
> + delete $conf->{$opt};
> + PVE::QemuConfig->write_config($vmid, $conf);
> + }
> + } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
> + PVE::QemuConfig->check_protection($conf, "can't remove drive '$opt'");
> + $check_drive_perms->($opt, $val);
> + PVE::QemuServer::vmconfig_register_unused_drive(
> + $storecfg,
> + $vmid,
> + $conf,
> + PVE::QemuServer::parse_drive($opt, $val),
> + ) if $is_pending_val;
> + PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> + PVE::QemuConfig->write_config($vmid, $conf);
> + } elsif ($opt =~ m/^serial\d+$/) {
> + if ($val eq 'socket') {
> + $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
> + } elsif ($authuser ne 'root@pam') {
> + die "only root can delete '$opt' config for real devices\n";
> + }
> + PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> + PVE::QemuConfig->write_config($vmid, $conf);
> + } elsif ($opt =~ m/^usb\d+$/) {
> + check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
> + PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> + PVE::QemuConfig->write_config($vmid, $conf);
> + } elsif ($opt =~ m/^hostpci\d+$/) {
> + check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
> + PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> + PVE::QemuConfig->write_config($vmid, $conf);
> + } elsif ($opt =~ m/^rng\d+$/) {
> + check_rng_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
> + PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> + PVE::QemuConfig->write_config($vmid, $conf);
> + } elsif ($opt =~ m/^virtiofs\d$/) {
> + check_dir_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
> + PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> + PVE::QemuConfig->write_config($vmid, $conf);
> + } elsif ($opt eq 'tags') {
> + assert_tag_permissions($vmid, $val, '', $rpcenv, $authuser);
> + delete $conf->{$opt};
> + PVE::QemuConfig->write_config($vmid, $conf);
> + } elsif ($opt =~ m/^net\d+$/) {
> + if ($conf->{$opt}) {
> + PVE::QemuServer::check_bridge_access(
> + $rpcenv,
> + $authuser,
> + { $opt => $conf->{$opt} },
> + );
> + }
> + PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> + PVE::QemuConfig->write_config($vmid, $conf);
> + } else {
> + PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
> + PVE::QemuConfig->write_config($vmid, $conf);
> + }
> + }
> +
> + foreach my $opt (keys %$param) { # add/change
> + $modified->{$opt} = 1;
> + $conf = PVE::QemuConfig->load_config($vmid); # update/reload
> + next
> + if defined($conf->{pending}->{$opt})
> + && ($param->{$opt} eq $conf->{pending}->{$opt}); # skip if nothing changed
> +
> + my $arch = PVE::QemuServer::Helpers::get_vm_arch($conf);
> +
> + if (PVE::QemuServer::is_valid_drivename($opt)) {
> + # old drive
> + if ($conf->{$opt}) {
> + $check_drive_perms->($opt, $conf->{$opt});
> + prohibit_tpm_version_change($conf->{$opt}, $param->{$opt})
> + if $opt eq 'tpmstate0';
> + }
> +
> + # new drive
> + $check_drive_perms->($opt, $param->{$opt});
> + PVE::QemuServer::vmconfig_register_unused_drive(
> + $storecfg,
> + $vmid,
> + $conf,
> + PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}),
> + ) if defined($conf->{pending}->{$opt});
> +
> + assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt})
> + if $opt =~ m/^scsi\d+$/;
> +
> + my (undef, $created_opts) = create_disks(
> + $rpcenv,
> + $authuser,
> + $conf,
> + $arch,
> + $storecfg,
> + $vmid,
> + undef,
> + { $opt => $param->{$opt} },
> + undef,
> + undef,
> + $extraction_storage,
> + );
> + $conf->{pending}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
> +
> + # default legacy boot order implies all cdroms anyway
> + if (@bootorder) {
> + # append new CD drives to bootorder to mark them bootable
> + my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}, 1);
> + if (
> + PVE::QemuServer::drive_is_cdrom($drive, 1)
> + && !grep(/^$opt$/, @bootorder)
> + ) {
> + push @bootorder, $opt;
> + $conf->{pending}->{boot} =
> + PVE::QemuServer::print_bootorder(\@bootorder);
> + $modified->{boot} = 1;
> + }
> + }
> + } elsif ($opt =~ m/^serial\d+/) {
> + if (
> + (!defined($conf->{$opt}) || $conf->{$opt} eq 'socket')
> + && $param->{$opt} eq 'socket'
> + ) {
> + $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
> + } elsif ($authuser ne 'root@pam') {
> + die "only root can modify '$opt' config for real devices\n";
> + }
> + $conf->{pending}->{$opt} = $param->{$opt};
> + } elsif ($opt eq 'vga') {
> + my $vga = PVE::QemuServer::parse_vga($param->{$opt});
> + PVE::QemuServer::assert_clipboard_config($vga);
> + $conf->{pending}->{$opt} = $param->{$opt};
> + } elsif ($opt =~ m/^usb\d+/) {
> + if (my $olddevice = $conf->{$opt}) {
> + check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $conf->{$opt});
> + }
> + check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
> + $conf->{pending}->{$opt} = $param->{$opt};
> + } elsif ($opt =~ m/^hostpci\d+$/) {
> + if (my $oldvalue = $conf->{$opt}) {
> + check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
> + }
> + check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
> + $conf->{pending}->{$opt} = $param->{$opt};
> + } elsif ($opt =~ m/^rng\d+$/) {
> + if (my $oldvalue = $conf->{$opt}) {
> + check_rng_perm($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
> + }
> + check_rng_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
> + $conf->{pending}->{$opt} = $param->{$opt};
> + } elsif ($opt =~ m/^virtiofs\d$/) {
> + if (my $oldvalue = $conf->{$opt}) {
> + check_dir_perm($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
> + }
> + check_dir_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
> + $conf->{pending}->{$opt} = $param->{$opt};
> + } elsif ($opt eq 'tags') {
> + assert_tag_permissions(
> + $vmid, $conf->{$opt}, $param->{$opt}, $rpcenv, $authuser,
> + );
> + $conf->{pending}->{$opt} =
> + PVE::GuestHelpers::get_unique_tags($param->{$opt});
> + } elsif ($opt =~ m/^net\d+$/) {
> + if ($conf->{$opt}) {
> + PVE::QemuServer::check_bridge_access(
> + $rpcenv,
> + $authuser,
> + { $opt => $conf->{$opt} },
> + );
> + }
> + $conf->{pending}->{$opt} = $param->{$opt};
> + } elsif ($opt eq 'machine') {
> + my $machine_conf = PVE::QemuServer::Machine::parse_machine($param->{$opt});
> + PVE::QemuServer::Machine::assert_valid_machine_property($machine_conf);
> + $conf->{pending}->{$opt} = $param->{$opt};
> + } elsif ($opt eq 'ostype') {
> + # Check if machine version pinning is needed when switching OS type, just like
> + # upon creation. Skip if 'machine' is explicitly set or removed at the same time
> + # to honor the users request. While it should be enough to look at $modified,
> + # because 'machine' is sorted before 'ostype', be explicit just to be sure.
> + if (
> + !defined($param->{machine})
> + && !defined($conf->{pending}->{machine})
> + && !$modified->{machine} # detects deletion
> + ) {
> + eval {
> + my $machine_string =
> + PVE::QemuServer::Machine::check_and_pin_machine_string(
> + $conf->{machine}, $param->{ostype}, $arch,
> + );
> + $conf->{pending}->{machine} = $machine_string if $machine_string;
> + };
> + print "automatic pinning of machine version failed - $@" if $@;
> + }
> + $conf->{pending}->{$opt} = $param->{$opt};
> + } elsif ($opt eq 'cipassword') {
> + if (!PVE::QemuServer::Helpers::windows_version($conf->{ostype})) {
> + # Same logic as in cloud-init (but with the regex fixed...)
> + $param->{cipassword} = PVE::Tools::encrypt_pw($param->{cipassword})
> + if $param->{cipassword} !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
> + }
> + $conf->{cipassword} = $param->{cipassword};
> + } else {
> + $conf->{pending}->{$opt} = $param->{$opt};
> +
> + if ($opt eq 'boot') {
> + my $new_bootcfg =
> + PVE::JSONSchema::parse_property_string('pve-qm-boot', $param->{$opt});
> + if ($new_bootcfg->{order}) {
> + my @devs = PVE::Tools::split_list($new_bootcfg->{order});
> + for my $dev (@devs) {
> + my $exists =
> + $conf->{$dev} || $conf->{pending}->{$dev} || $param->{$dev};
> + my $deleted = grep { $_ eq $dev } @delete;
> + die "invalid bootorder: device '$dev' does not exist'\n"
> + if !$exists || $deleted;
> + }
> +
> + # remove legacy boot order settings if new one set
> + $conf->{pending}->{$opt} = PVE::QemuServer::print_bootorder(\@devs);
> + PVE::QemuConfig->add_to_pending_delete($conf, "bootdisk")
> + if $conf->{bootdisk};
> + }
> + }
> + }
> + PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
> + PVE::QemuConfig->write_config($vmid, $conf);
> + }
> +
> + # remove pending changes when nothing changed
> + $conf = PVE::QemuConfig->load_config($vmid); # update/reload
> + my $changes = PVE::QemuConfig->cleanup_pending($conf);
> + PVE::QemuConfig->write_config($vmid, $conf) if $changes;
> +
> + return if !scalar(keys %{ $conf->{pending} });
> +
> + my $running = PVE::QemuServer::check_running($vmid);
> +
> + # apply pending changes
> +
> + $conf = PVE::QemuConfig->load_config($vmid); # update/reload
> +
> + my $errors = {};
> + if ($running) {
> + PVE::QemuServer::vmconfig_hotplug_pending(
> + $vmid, $conf, $storecfg, $modified, $errors,
> + );
> + } else {
> + # cloud_init must be skipped if we are in an incoming, remote live migration
> + PVE::QemuServer::vmconfig_apply_pending(
> + $vmid, $conf, $storecfg, $errors, $skip_cloud_init,
> + );
> + }
> + raise_param_exc($errors) if scalar(keys %$errors);
> +
> + return;
> + };
> +
> + $load_and_check_config->(0); # early checks before forking/locking
> +
> + if ($sync) {
> + PVE::QemuConfig->lock_config($vmid, $updatefn);
> + return;
> + } else {
> + my $upid = $rpcenv->fork_worker(
> + 'qmconfig',
> + $vmid,
> + $authuser,
> + sub { PVE::QemuConfig->lock_config($vmid, $updatefn); },
> + );
> +
> + if ($background_delay) {
> +
> + # Note: It would be better to do that in the Event based HTTPServer
> + # to avoid blocking call to sleep.
> +
> + my $end_time = time() + $background_delay;
> +
> + my $task = PVE::Tools::upid_decode($upid);
> +
> + my $running = 1;
> + while (time() < $end_time) {
> + $running =
> + PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
> + last if !$running;
> + sleep(1); # this gets interrupted when child process ends
> + }
> +
> + if (!$running) {
> + my $status = PVE::Tools::upid_read_status($upid);
> + return if !PVE::Tools::upid_status_is_error($status);
> + die "failed to update VM $vmid: $status\n";
> + }
> + }
> +
> + return $upid;
> + }
> };
>
> my $vm_config_perm_list = [
next prev parent reply other threads:[~2026-07-03 9:42 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-29 12:45 [PATCH-SERIES qemu-server 0/3] fix #7743: api: disk import: avoid locking twice when importing from OVA or same VM Fiona Ebner
2026-06-29 12:45 ` [PATCH qemu-server 1/3] api: disk import: remove unused wrongly named variable Fiona Ebner
2026-07-03 9:32 ` Dominik Csapak
2026-06-29 12:45 ` [PATCH qemu-server 2/3] fix #7743: api: disk import: avoid locking twice when importing from OVA or same VM Fiona Ebner
2026-07-03 9:31 ` Dominik Csapak
2026-06-29 12:45 ` [PATCH qemu-server 3/3] api: update vm: fork before locking Fiona Ebner
2026-07-03 9:41 ` Dominik Csapak [this message]
2026-07-03 11:44 ` Fiona Ebner
2026-07-03 12:00 ` Dominik Csapak
2026-06-30 9:49 ` [PATCH-SERIES qemu-server 0/3] fix #7743: api: disk import: avoid locking twice when importing from OVA or same VM Manuel Federanko
2026-07-03 11:59 ` superseded: " Fiona Ebner
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=4d763f1f-59e0-4ad5-b7c1-72b4e5b65dd1@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=f.ebner@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox