[pve-devel] [PATCH] Implement pending change

Alexandre Derumier aderumier at odiso.com
Wed Oct 22 10:03:51 CEST 2014


This add a new section pending

[PENDING]

When we change an option, it's always write in pending section first,
then
   if the vm is not running,
   or if the vm is running and the option|device can be change online
we remove it from pending and update the main config

On delete,
  -disks as marked as unused in pending
  -options are marked as 'delete' in pending,

The pending changes are applied on vm_stop and vm_start.
(maybe could we add an api to manually apply pending changes when vm is stopped)

some examples:

delete option
-------------
[CONF]
ostype: l26

qm set -delete ostype

[CONF]
ostype: l26
[PENDING]
osdtype : delete

delete disk
-----------
[CONF]
virtio1 : local:100/vm-100-disk-1.raw,format=raw

qm set -delete virtio1

[CONF]
virtio1 : local:100/vm-100-disk-1.raw,format=raw
[PENDING]
unused0 : local:100/vm-100-disk-1.raw,format=raw

swap disk with unused disk
--------------------------
[CONF]
unused0 : local:100/vm-100-disk-2.raw,format=raw
virtio1 : local:100/vm-100-disk-1.raw,format=raw

qm set 100 -virtio1 local:100/vm-100-disk-2.raw

[CONF]
unused0 : local:100/vm-100-disk-2.raw,format=raw
virtio1 : local:100/vm-100-disk-1.raw,format=raw
[PENDING]
unused1 : local:100/vm-100-disk-1.raw,format=raw
virtio1 : local:100/vm-100-disk-2.raw,format=raw

update: swap disk with new disk
--------------------------
[CONF]
virtio1 : local:100/vm-100-disk-1.raw,format=raw

qm set 100 -virtio1 local:1

[CONF]
virtio1 : local:100/vm-100-disk-1.raw,format=raw
[PENDING]
unused1 : local:100/vm-100-disk-1.raw,format=raw
virtio1 : local:100/vm-100-disk-2.raw,format=raw

update option
---------------
[CONF]
ostype : l26

qm set -ostype win8
[CONF]
ostype : l26

[PENDING]
ostype: win8

update net
----------
[CONF]
net0: e1000=96:36:DB:21:74:47,bridge=vmbr0

qm set -net0 virtio=96:36:DB:21:74:47,bridge=vmbr0

[CONF]
net0: e1000=96:36:DB:21:74:47,bridge=vmbr0
[PENDING]
net0: virtio=96:36:DB:21:74:47,bridge=vmbr0

Signed-off-by: Alexandre Derumier <aderumier at odiso.com>
---
 PVE/API2/Qemu.pm  |  318 ++++++++++++++++++++++++++++++++++++++---------------
 PVE/QemuServer.pm |  164 +++++++++++++++++++++------
 pve-bridge        |    4 +
 3 files changed, 366 insertions(+), 120 deletions(-)

diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
index a0fcd28..59c8957 100644
--- a/PVE/API2/Qemu.pm
+++ b/PVE/API2/Qemu.pm
@@ -102,7 +102,7 @@ my $check_storage_access_clone = sub {
 # Note: $pool is only needed when creating a VM, because pool permissions
 # are automatically inherited if VM already exists inside a pool.
 my $create_disks = sub {
-    my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
+    my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $settings, $default_storage, $running) = @_;
 
     my $vollist = [];
 
@@ -167,7 +167,12 @@ my $create_disks = sub {
 
     # modify vm config if everything went well
     foreach my $ds (keys %$res) {
-	$conf->{$ds} = $res->{$ds};
+	if($conf->{pending}->{$ds}){
+	    my $drive = PVE::QemuServer::parse_drive($ds, $conf->{pending}->{$ds});
+	    my $volid = $drive->{file};
+	    PVE::QemuServer::add_unused_volume($conf, $volid, $running);
+	}
+	$conf->{pending}->{$ds} = $res->{$ds};
     }
 
     return $vollist;
@@ -669,9 +674,27 @@ my $test_deallocate_drive = sub {
     return undef;
 };
 
+my $is_hotpluggable = sub {
+   my ($opt) = @_;
+
+    #online value change
+    my $hotplug_options = {
+	name => 1,
+	balloon => 1,
+	shares => 1,
+	hotplug => 1,
+	shares => 1,
+	onboot => 1
+    };
+
+    return 1 if $hotplug_options->{$opt};
+};
+
 my $delete_drive = sub {
     my ($conf, $storecfg, $vmid, $key, $drive, $force) = @_;
 
+    my $running = PVE::QemuServer::check_running($vmid);
+
     if (!PVE::QemuServer::drive_is_cdrom($drive)) {
 	my $volid = $drive->{file};
 
@@ -686,15 +709,17 @@ my $delete_drive = sub {
 			if $used_paths->{$path};
 
 		    PVE::Storage::vdisk_free($storecfg, $volid);
+		    delete $conf->{$key};
+
 		};
 		die $@ if $@;
 	    } else {
-		PVE::QemuServer::add_unused_volume($conf, $volid, $vmid);
+		    PVE::QemuServer::add_unused_volume($conf, $volid, $running);
 	    }
 	}
     }
 
-    delete $conf->{$key};
+    delete $conf->{$key} if !$running;
 };
 
 my $vmconfig_delete_option = sub {
@@ -702,40 +727,70 @@ my $vmconfig_delete_option = sub {
 
     return if !defined($conf->{$opt});
 
+    my $running = PVE::QemuServer::check_running($vmid);
+
     my $isDisk = PVE::QemuServer::valid_drivename($opt)|| ($opt =~ m/^unused/);
+    my $drive = "";
 
-    if ($isDisk) {
+    if ($isDisk || ($opt =~ m/^unused/)) {
 	$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
 
-	my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
+	$drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
 	if (my $sid = &$test_deallocate_drive($storecfg, $vmid, $opt, $drive, $force)) {
 	    $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
 	}
     }
 
-    my $unplugwarning = "";
-    if ($conf->{ostype} && $conf->{ostype} eq 'l26') {
-	$unplugwarning = "<br>verify that you have acpiphp && pci_hotplug modules loaded in your guest VM";
-    } elsif ($conf->{ostype} && $conf->{ostype} eq 'l24') {
-	$unplugwarning = "<br>kernel 2.4 don't support hotplug, please disable hotplug in options";
-    } elsif (!$conf->{ostype} || ($conf->{ostype} && $conf->{ostype} eq 'other')) {
-	$unplugwarning = "<br>verify that your guest support acpi hotplug";
-    }
-
-    if ($opt eq 'tablet') {
-	PVE::QemuServer::vm_deviceplug(undef, $conf, $vmid, $opt);
-    } else {
-        die "error hot-unplug $opt $unplugwarning" if !PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt);
-    }
-
+    #add to pending the device to delete
     if ($isDisk) {
-	my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
 	&$delete_drive($conf, $storecfg, $vmid, $opt, $drive, $force);
     } else {
+	$conf->{pending}->{$opt} = 'delete';
+    }
+    PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+
+    #online value change
+    if(&$is_hotpluggable($opt)){
 	delete $conf->{$opt};
+	delete $conf->{pending}->{$opt};
+	PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+	return;
+    }
+
+    #do the unplug
+    if($running){
+	if ($opt eq 'tablet') {
+	    if($conf->{$opt} == 0 && !PVE::QemuServer::vm_deviceplug(undef, $conf, $vmid, $opt)){
+		warn "error hotplug tablet";
+		return;
+	    }
+	} else {
+
+	    if($conf->{hotplug} && $opt !~ m/^(ide|sata)(\d+)$/){
+        
+		if(!PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt)){
+		    warn "error hot-unplug $opt";
+		    return;
+		}else{
+
+		    #if disk, delete unused pending disk
+		    if ($opt =~ m/^(virtio|scsi)(\d+)$/) {
+			my $pendingconf = $conf->{pending};
+			my $key = PVE::QemuServer::find_unused_volume($pendingconf, $drive->{file}, $storecfg);
+			if($key){
+				delete $pendingconf->{$key};
+				PVE::QemuServer::add_unused_volume($conf, $drive->{file});
+                                PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+		        }
+		    }
+		    delete $conf->{$opt};
+		    delete $conf->{pending}->{$opt};
+		    PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+		}
+	    }
+	}
     }
 
-    PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
 };
 
 my $safe_num_ne = sub {
@@ -749,44 +804,45 @@ my $safe_num_ne = sub {
 };
 
 my $vmconfig_update_disk = sub {
-    my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $opt, $value, $force) = @_;
-
-    my $drive = PVE::QemuServer::parse_drive($opt, $value);
+    my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $opt, $force) = @_;
 
-    if (PVE::QemuServer::drive_is_cdrom($drive)) { #cdrom
-	$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
-    } else {
-	$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
-    }
+    my $running = PVE::QemuServer::check_running($vmid);
+    my $pendingvalue = $conf->{pending}->{$opt};
+    my $drive = PVE::QemuServer::parse_drive($opt, $pendingvalue);
 
     if ($conf->{$opt}) {
 
-	if (my $old_drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt}))  {
+	if (my $old_drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt})) {
 
 	    my $media = $drive->{media} || 'disk';
 	    my $oldmedia = $old_drive->{media} || 'disk';
 	    die "unable to change media type\n" if $media ne $oldmedia;
+	    if (!PVE::QemuServer::drive_is_cdrom($old_drive)){
+	        #swap : unplug and put it in pending unusused
+		if ($drive->{file} ne $old_drive->{file}) {  # delete old disks
+		    &$vmconfig_delete_option($rpcenv, $authuser, $conf, $storecfg, $vmid, $opt, $force);
+		    $conf->{pending}->{$opt} = $pendingvalue;
+		} elsif ($running) { #update existing disk
+
+		    #non hotpluggable value
+		    if (&$safe_num_ne($drive->{discard}, $old_drive->{discard}) || &$safe_num_ne($drive->{cache}, $old_drive->{cache})) {
+			return;	
+		    }
 
-	    if (!PVE::QemuServer::drive_is_cdrom($old_drive) &&
-		($drive->{file} ne $old_drive->{file})) {  # delete old disks
-
-		&$vmconfig_delete_option($rpcenv, $authuser, $conf, $storecfg, $vmid, $opt, $force);
-		$conf = PVE::QemuServer::load_config($vmid); # update/reload
-	    }
-
-            if(&$safe_num_ne($drive->{mbps}, $old_drive->{mbps}) ||
-               &$safe_num_ne($drive->{mbps_rd}, $old_drive->{mbps_rd}) ||
-               &$safe_num_ne($drive->{mbps_wr}, $old_drive->{mbps_wr}) ||
-               &$safe_num_ne($drive->{iops}, $old_drive->{iops}) ||
-               &$safe_num_ne($drive->{iops_rd}, $old_drive->{iops_rd}) ||
-               &$safe_num_ne($drive->{iops_wr}, $old_drive->{iops_wr}) ||
-               &$safe_num_ne($drive->{mbps_max}, $old_drive->{mbps_max}) ||
-               &$safe_num_ne($drive->{mbps_rd_max}, $old_drive->{mbps_rd_max}) ||
-               &$safe_num_ne($drive->{mbps_wr_max}, $old_drive->{mbps_wr_max}) ||
-               &$safe_num_ne($drive->{iops_max}, $old_drive->{iops_max}) ||
-               &$safe_num_ne($drive->{iops_rd_max}, $old_drive->{iops_rd_max}) ||
-               &$safe_num_ne($drive->{iops_wr_max}, $old_drive->{iops_wr_max})) {
-               PVE::QemuServer::qemu_block_set_io_throttle($vmid,"drive-$opt",
+		    #apply throttle
+		    if(&$safe_num_ne($drive->{mbps}, $old_drive->{mbps}) ||
+	               &$safe_num_ne($drive->{mbps_rd}, $old_drive->{mbps_rd}) ||
+	               &$safe_num_ne($drive->{mbps_wr}, $old_drive->{mbps_wr}) ||
+	               &$safe_num_ne($drive->{iops}, $old_drive->{iops}) ||
+	               &$safe_num_ne($drive->{iops_rd}, $old_drive->{iops_rd}) ||
+	               &$safe_num_ne($drive->{iops_wr}, $old_drive->{iops_wr}) ||
+	               &$safe_num_ne($drive->{mbps_max}, $old_drive->{mbps_max}) ||
+	               &$safe_num_ne($drive->{mbps_rd_max}, $old_drive->{mbps_rd_max}) ||
+	               &$safe_num_ne($drive->{mbps_wr_max}, $old_drive->{mbps_wr_max}) ||
+	               &$safe_num_ne($drive->{iops_max}, $old_drive->{iops_max}) ||
+	               &$safe_num_ne($drive->{iops_rd_max}, $old_drive->{iops_rd_max}) ||
+	               &$safe_num_ne($drive->{iops_wr_max}, $old_drive->{iops_wr_max})) {
+	               PVE::QemuServer::qemu_block_set_io_throttle($vmid,"drive-$opt",
 							   ($drive->{mbps} || 0)*1024*1024,
 							   ($drive->{mbps_rd} || 0)*1024*1024,
 							   ($drive->{mbps_wr} || 0)*1024*1024,
@@ -798,21 +854,21 @@ my $vmconfig_update_disk = sub {
 							   ($drive->{mbps_wr_max} || 0)*1024*1024,
 							   $drive->{iops_max} || 0,
 							   $drive->{iops_rd_max} || 0,
-							   $drive->{iops_wr_max} || 0)
-		   if !PVE::QemuServer::drive_is_cdrom($drive);
-            }
+							   $drive->{iops_wr_max} || 0);
+
+		    }
+
+		}
+	    }
+
 	}
     }
 
-    &$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, undef, {$opt => $value});
     PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
 
-    $conf = PVE::QemuServer::load_config($vmid); # update/reload
-    $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
-
     if (PVE::QemuServer::drive_is_cdrom($drive)) { # cdrom
 
-	if (PVE::QemuServer::check_running($vmid)) {
+	if ($running) {
 	    if ($drive->{file} eq 'none') {
 		PVE::QemuServer::vm_mon_cmd($vmid, "eject",force => JSON::true,device => "drive-$opt");
 	    } else {
@@ -821,51 +877,85 @@ my $vmconfig_update_disk = sub {
 		PVE::QemuServer::vm_mon_cmd($vmid, "change",device => "drive-$opt",target => "$path") if $path;
 	    }
 	}
+	$conf->{$opt} = $conf->{pending}->{$opt};
+	delete $conf->{pending}->{$opt};	
 
-    } else { # hotplug new disks
+    } elsif ($opt !~ m/^(ide|sata)(\d+)$/) { # hotplug new disks
+	if (!PVE::QemuServer::vm_deviceplug($storecfg, $conf, $vmid, $opt, $drive, $pendingvalue)){
+		warn "error hotplug $opt";
+		return;
+	}
 
-	die "error hotplug $opt" if !PVE::QemuServer::vm_deviceplug($storecfg, $conf, $vmid, $opt, $drive);
     }
+
+    
+    #if added disk was unused before, delete it from conf
+    my $key = PVE::QemuServer::find_unused_volume($conf, $drive->{file},$storecfg);
+    if($key){
+	delete $conf->{$key};
+	PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+    }
+
 };
 
 my $vmconfig_update_net = sub {
-    my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $opt, $value) = @_;
+    my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $opt) = @_;
+
+    if ($conf->{$opt}) {
+	my $running = PVE::QemuServer::check_running($vmid);
 
-    if ($conf->{$opt} && PVE::QemuServer::check_running($vmid)) {
 	my $oldnet = PVE::QemuServer::parse_net($conf->{$opt});
-	my $newnet = PVE::QemuServer::parse_net($value);
+	my $newnet = PVE::QemuServer::parse_net($conf->{pending}->{$opt});
 
 	if($oldnet->{model} ne $newnet->{model}){
 	    #if model change, we try to hot-unplug
-            die "error hot-unplug $opt for update" if !PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt);
+	    if($running && $conf->{hotplug}){
+		if(!PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt)){
+		    warn "error hot-unplug $opt for update";
+		    return;
+		}
+	    }
 	}else{
 
 	    if($newnet->{bridge} && $oldnet->{bridge}){
 		my $iface = "tap".$vmid."i".$1 if $opt =~ m/net(\d+)/;
 
-		if($newnet->{rate} ne $oldnet->{rate}){
+		if($newnet->{rate} && $oldnet->{rate} && $newnet->{rate} ne $oldnet->{rate}){
 		    PVE::Network::tap_rate_limit($iface, $newnet->{rate});
 		}
 
-		if(($newnet->{bridge} ne $oldnet->{bridge}) || ($newnet->{tag} ne $oldnet->{tag}) || ($newnet->{firewall} ne $oldnet->{firewall})){
+		if(($newnet->{bridge} && $oldnet->{bridge} &&$newnet->{bridge} ne $oldnet->{bridge}) || 
+		   ($newnet->{tag} && $oldnet->{tag} &&$newnet->{tag} ne $oldnet->{tag}) || 
+		   ($newnet->{firewall} && $oldnet->{firewall} && $newnet->{firewall} ne $oldnet->{firewall})){
 		    PVE::Network::tap_unplug($iface);
 		    PVE::Network::tap_plug($iface, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall});
 		}
 
+		$conf->{$opt} = $conf->{pending}->{$opt};
+		delete $conf->{pending}->{$opt};
+		PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+		return;
+
 	    }else{
+
 		#if bridge/nat mode change, we try to hot-unplug
-		die "error hot-unplug $opt for update" if !PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt);
+		if($running && $conf->{hotplug}){
+		    if(!PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt)){
+			warn "error hot-unplug $opt for update";
+			return;
+		    }
+		}
+
 	    }
 	}
 
     }
-    $conf->{$opt} = $value;
-    PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
-    $conf = PVE::QemuServer::load_config($vmid); # update/reload
-
-    my $net = PVE::QemuServer::parse_net($conf->{$opt});
 
-    die "error hotplug $opt" if !PVE::QemuServer::vm_deviceplug($storecfg, $conf, $vmid, $opt, $net);
+    if($conf->{pending}->{$opt}){
+	my $net = PVE::QemuServer::parse_net($conf->{pending}->{$opt});
+	warn "error hotplug $opt" if(!PVE::QemuServer::vm_deviceplug($storecfg, $conf, $vmid, $opt, $net, $conf->{pending}->{$opt}));
+    }
+    
 };
 
 # POST/PUT {vmid}/config implementation
@@ -981,37 +1071,87 @@ my $update_vm_api  = sub {
 
 	    my $running = PVE::QemuServer::check_running($vmid);
 
+
+
+	    #load values in pending and create disks
 	    foreach my $opt (keys %$param) { # add/change
 
-		$conf = PVE::QemuServer::load_config($vmid); # update/reload
+		    $conf = PVE::QemuServer::load_config($vmid);
 
-		next if $conf->{$opt} && ($param->{$opt} eq $conf->{$opt}); # skip if nothing changed
+		    if (PVE::QemuServer::valid_drivename($opt)) {
+			my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
+
+			if (PVE::QemuServer::drive_is_cdrom($drive)) { #cdrom
+			    $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
+			} else {
+			    $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
+			}
+
+			&$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, undef, {$opt => $param->{$opt}}, undef, $running);
+		    }else{
+			if($running){
+			     $conf->{pending}->{$opt} = $param->{$opt};
+			}else{
+			     $conf->{$opt} = $param->{$opt};
+			}
+		    }
+		    PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+	    }
+	    
+
+	    #hotplug pending devices first
+	    foreach my $opt (keys %$param){
+
+		next if !$conf->{pending}->{$opt};
+	
+		# delete pending if nothing changed
+		if ($conf->{$opt} && $param->{$opt} eq $conf->{$opt}){
+		    delete $conf->{pending}->{$opt};
+		    PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+		    next;
+		}
 
 		if (PVE::QemuServer::valid_drivename($opt)) {
 
 		    &$vmconfig_update_disk($rpcenv, $authuser, $conf, $storecfg, $vmid,
-					   $opt, $param->{$opt}, $force);
+					   $opt, $force);
 
 		} elsif ($opt =~ m/^net(\d+)$/) { #nics
 
-		    &$vmconfig_update_net($rpcenv, $authuser, $conf, $storecfg, $vmid,
-					  $opt, $param->{$opt});
+		    &$vmconfig_update_net($rpcenv, $authuser, $conf, $storecfg, $vmid, $opt);
 
-		} else {
+		} elsif ($opt eq 'tablet'){
 
-		    if($opt eq 'tablet' && $param->{$opt} == 1){
-			PVE::QemuServer::vm_deviceplug(undef, $conf, $vmid, $opt);
-		    } elsif($opt eq 'tablet' && $param->{$opt} == 0){
+		    if($param->{$opt} == 1){
+			PVE::QemuServer::vm_deviceplug(undef, $conf, $vmid, $opt, $param->{$opt});
+		    } elsif($param->{$opt} == 0){
 			PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt);
 		    }
-		
-		    if($opt eq 'cores' && $conf->{maxcpus}){
-			PVE::QemuServer::qemu_cpu_hotplug($vmid, $conf, $param->{$opt});
-		    }
 
+		} elsif($opt eq 'cores'){
+
+		    PVE::QemuServer::qemu_cpu_hotplug($vmid, $conf, $param->{$opt});
+		}
+
+		PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+	    }
+
+	    #then update pending options
+
+	    foreach my $opt (keys %$param){
+
+		next if !$conf->{pending}->{$opt};
+
+		next if (PVE::QemuServer::valid_drivename($opt));
+		next if $opt =~ m/^net(\d+)$/;
+		next if $opt eq 'cores' || $opt eq 'tablet';
+
+		if(&$is_hotpluggable($opt)){
 		    $conf->{$opt} = $param->{$opt};
-		    PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+		    delete $conf->{pending}->{$opt};
 		}
+
+		PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
 	    }
 
 	    # allow manual ballooning if shares is set to zero
diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm
index 98264d1..f0f53a0 100644
--- a/PVE/QemuServer.pm
+++ b/PVE/QemuServer.pm
@@ -1370,25 +1370,43 @@ sub add_random_macs {
 }
 
 sub add_unused_volume {
-    my ($config, $volid) = @_;
+    my ($config, $volid, $running) = @_;
 
     my $key;
     for (my $ind = $MAX_UNUSED_DISKS - 1; $ind >= 0; $ind--) {
 	my $test = "unused$ind";
 	if (my $vid = $config->{$test}) {
 	    return if $vid eq $volid; # do not add duplicates
+	} elsif (my $vidpending = $config->{pending}->{$test}) {
+	    return if $vidpending eq $volid; # do not add duplicates
 	} else {
 	    $key = $test;
 	}
     }
 
     die "To many unused volume - please delete them first.\n" if !$key;
-
-    $config->{$key} = $volid;
+    if($running){
+	$config->{pending}->{$key} = $volid;
+    }else{
+	$config->{$key} = $volid;
+    }
 
     return $key;
 }
 
+sub find_unused_volume {
+    my ($conf, $volid, $storecfg) = @_;
+
+    foreach my $key (keys %$conf) {
+        next if($key !~ m/^unused/);
+        my $confdrive = PVE::QemuServer::parse_drive($key, $conf->{$key});
+        if (!PVE::QemuServer::drive_is_cdrom($confdrive)) {
+            PVE::QemuServer::cleanup_drive_path($key, $storecfg, $confdrive);
+	    return $key if $confdrive->{file} eq $volid;
+        }
+    }
+}
+
 my $valid_smbios1_options = {
     manufacturer => '\S+',
     product => '\S+',
@@ -1611,6 +1629,8 @@ sub check_type {
 
     die "unknown setting '$key'\n" if !$confdesc->{$key};
 
+    return $value if $value eq 'delete';
+
     my $type = $confdesc->{$key}->{type};
 
     if (!defined($value)) {
@@ -1782,6 +1802,7 @@ sub parse_vm_config {
     my $res = {
 	digest => Digest::SHA::sha1_hex($raw),
 	snapshots => {},
+	pending => {}
     };
 
     $filename =~ m|/qemu-server/(\d+)\.conf$|
@@ -1796,7 +1817,11 @@ sub parse_vm_config {
     foreach my $line (@lines) {
 	next if $line =~ m/^\s*$/;
 
-	if ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
+	if ($line =~ m/^\[PENDING\]\s*$/i) {
+	    $conf = $res->{pending} = {};
+	    next;
+
+	}elsif ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
 	    my $snapname = $1;
 	    $conf->{description} = $descr if $descr;
 	    $descr = '';
@@ -1852,8 +1877,8 @@ sub parse_vm_config {
     return $res;
 }
 
-sub write_vm_config {
-    my ($filename, $conf) = @_;
+sub write_vm_config_cleanup {
+    my ($conf) = @_;
 
     delete $conf->{snapstate}; # just to be sure
 
@@ -1883,7 +1908,7 @@ sub write_vm_config {
 
 	foreach my $key (keys %$cref) {
 	    next if $key eq 'digest' || $key eq 'description' || $key eq 'snapshots' ||
-		$key eq 'snapstate';
+		$key eq 'snapstate' || $key eq 'pending';
 	    my $value = $cref->{$key};
 	    eval { $value = check_type($key, $value); };
 	    die "unable to parse value of '$key' - $@" if $@;
@@ -1910,6 +1935,14 @@ sub write_vm_config {
 	}
     }
 
+}
+
+sub write_vm_config {
+    my ($filename, $conf) = @_;
+
+    write_vm_config_cleanup($conf);
+    write_vm_config_cleanup($conf->{pending});
+
     my $generate_raw_config = sub {
 	my ($conf) = @_;
 
@@ -1922,18 +1955,25 @@ sub write_vm_config {
 	}
 
 	foreach my $key (sort keys %$conf) {
-	    next if $key eq 'digest' || $key eq 'description' || $key eq 'snapshots';
+	    next if $key eq 'digest' || $key eq 'description' || $key eq 'snapshots' || $key eq 'pending';
 	    $raw .= "$key: $conf->{$key}\n";
 	}
 	return $raw;
     };
 
     my $raw = &$generate_raw_config($conf);
+
+    if(keys %{$conf->{pending}}){
+	    $raw .= "\n[PENDING]\n";
+	    $raw .= &$generate_raw_config($conf->{pending});
+    }
+
     foreach my $snapname (sort keys %{$conf->{snapshots}}) {
 	$raw .= "\n[$snapname]\n";
 	$raw .= &$generate_raw_config($conf->{snapshots}->{$snapname});
     }
 
+
     return $raw;
 }
 
@@ -2903,14 +2943,27 @@ sub vm_devices_list {
 }
 
 sub vm_deviceplug {
-    my ($storecfg, $conf, $vmid, $deviceid, $device) = @_;
+    my ($storecfg, $conf, $vmid, $deviceid, $device, $optvalue) = @_;
 
-    return 1 if !check_running($vmid);
+    if (!check_running($vmid)){
+	if($conf->{pending}->{$deviceid}){
+	    $conf->{$deviceid} = $optvalue;
+	    delete $conf->{pending}->{$deviceid};
+	    PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+	} 
+    }
 
     my $q35 = machine_type_is_q35($conf);
 
     if ($deviceid eq 'tablet') {
-	qemu_deviceadd($vmid, print_tabletdevice_full($conf));
+
+	eval { qemu_deviceadd($vmid, print_tabletdevice_full($conf->{pending}))};
+	#if tablet don't yet exist
+	if (!$@){
+	    delete $conf->{tablet};
+	}
+	delete $conf->{pending}->{$deviceid};
+	PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
 	return 1;
     }
 
@@ -2923,7 +2976,7 @@ sub vm_deviceplug {
 
     if ($deviceid =~ m/^(virtio)(\d+)$/) {
         return undef if !qemu_driveadd($storecfg, $vmid, $device);
-        my $devicefull = print_drivedevice_full($storecfg, $conf, $vmid, $device);
+        my $devicefull = print_drivedevice_full($storecfg, $conf->{pending}, $vmid, $device);
         qemu_deviceadd($vmid, $devicefull);
         if(!qemu_deviceaddverify($vmid, $deviceid)) {
            qemu_drivedel($vmid, $deviceid);
@@ -2942,7 +2995,7 @@ sub vm_deviceplug {
     if ($deviceid =~ m/^(scsi)(\d+)$/) {
         return undef if !qemu_findorcreatescsihw($storecfg,$conf, $vmid, $device);
         return undef if !qemu_driveadd($storecfg, $vmid, $device);
-        my $devicefull = print_drivedevice_full($storecfg, $conf, $vmid, $device);
+        my $devicefull = print_drivedevice_full($storecfg, $conf->{pending}, $vmid, $device);
         if(!qemu_deviceadd($vmid, $devicefull)) {
            qemu_drivedel($vmid, $deviceid);
            return undef;
@@ -2950,8 +3003,8 @@ sub vm_deviceplug {
     }
 
     if ($deviceid =~ m/^(net)(\d+)$/) {
-        return undef if !qemu_netdevadd($vmid, $conf, $device, $deviceid);
-        my $netdevicefull = print_netdevice_full($vmid, $conf, $device, $deviceid);
+        return undef if !qemu_netdevadd($vmid, $conf->{pending}, $device, $deviceid);
+        my $netdevicefull = print_netdevice_full($vmid, $conf->{pending}, $device, $deviceid);
         qemu_deviceadd($vmid, $netdevicefull);
         if(!qemu_deviceaddverify($vmid, $deviceid)) {
            qemu_netdevdel($vmid, $deviceid);
@@ -2968,21 +3021,27 @@ sub vm_deviceplug {
 	return undef if !qemu_deviceaddverify($vmid, $deviceid);
     }
 
+    #delete pending device after hotplug
+    if($conf->{pending}->{$deviceid}){
+	$conf->{$deviceid} = $optvalue;
+	delete $conf->{pending}->{$deviceid};
+	PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+    } 
+
     return 1;
 }
 
 sub vm_deviceunplug {
-    my ($vmid, $conf, $deviceid) = @_;
-
-    return 1 if !check_running ($vmid);
+    my ($vmid, $conf, $deviceid,$optvalue) = @_;
 
     if ($deviceid eq 'tablet') {
 	qemu_devicedel($vmid, $deviceid);
+	$conf->{tablet} = 0;
+	delete $conf->{pending}->{tablet};
+	PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
 	return 1;
     }
 
-    return 1 if !$conf->{hotplug};
-
     my $devices_list = vm_devices_list($vmid);
     return 1 if !defined($devices_list->{$deviceid});
 
@@ -3009,6 +3068,11 @@ sub vm_deviceunplug {
         return undef if !qemu_netdevdel($vmid, $deviceid);
     }
 
+    if($conf->{$deviceid}){
+	delete $conf->{$deviceid};
+	PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+    }
+
     return 1;
 }
 
@@ -3136,24 +3200,28 @@ sub qemu_netdevdel {
 sub qemu_cpu_hotplug {
     my ($vmid, $conf, $cores) = @_;
 
-    die "new cores config is not defined" if !$cores;
-    die "you can't add more cores than maxcpus"
-	if $conf->{maxcpus} && ($cores > $conf->{maxcpus});
+    return if !$cores;
+    return if !$conf->{maxcpus};
     return if !check_running($vmid);
 
+    return if $cores > $conf->{maxcpus};
+
     my $currentcores = $conf->{cores} if $conf->{cores};
-    die "current cores is not defined" if !$currentcores;
-    die "maxcpus is not defined" if !$conf->{maxcpus};
-    raise_param_exc({ 'cores' => "online cpu unplug is not yet possible" })
-	if($cores < $currentcores);
+    return if !$currentcores;
+
+    return if($cores < $currentcores); # unplug is not yet possible
 
     my $currentrunningcores = vm_mon_cmd($vmid, "query-cpus");
-    raise_param_exc({ 'cores' => "cores number if running vm is different than configuration" })
-	if scalar (@{$currentrunningcores}) != $currentcores;
+    return if scalar (@{$currentrunningcores}) != $currentcores;
 
     for(my $i = $currentcores; $i < $cores; $i++) {
 	vm_mon_cmd($vmid, "cpu-add", id => int($i));
     }
+
+    $conf->{cores} = $cores;
+    delete $conf->{pending}->{cores};
+    PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+
 }
 
 sub qemu_block_set_io_throttle {
@@ -3364,6 +3432,8 @@ sub vm_start {
 	# set environment variable useful inside network script
 	$ENV{PVE_MIGRATED_FROM} = $migratedfrom if $migratedfrom;
 
+        apply_pending_conf($conf, $vmid, $storecfg) if !$migratedfrom;
+
 	my ($cmd, $vollist, $spice_port) = config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine);
 
 	my $migrate_port = 0;
@@ -3557,7 +3627,7 @@ sub get_vm_volumes {
 }
 
 sub vm_stop_cleanup {
-    my ($storecfg, $vmid, $conf, $keepActive) = @_;
+    my ($storecfg, $vmid, $conf, $keepActive, $migratedfrom) = @_;
 
     eval {
 	fairsched_rmnod($vmid); # try to destroy group
@@ -3572,6 +3642,8 @@ sub vm_stop_cleanup {
 	}
     };
     warn $@ if $@; # avoid errors - just warn
+    apply_pending_conf($conf, $vmid, $storecfg) if !$migratedfrom;
+
 }
 
 # Note: use $nockeck to skip tests if VM configuration file exists.
@@ -3586,7 +3658,7 @@ sub vm_stop {
 	my $pid = check_running($vmid, $nocheck, $migratedfrom);
 	kill 15, $pid if $pid;
 	my $conf = load_config($vmid, $migratedfrom);
-	vm_stop_cleanup($storecfg, $vmid, $conf, $keepActive);
+	vm_stop_cleanup($storecfg, $vmid, $conf, $keepActive, $migratedfrom);
 	return;
     }
 
@@ -5301,4 +5373,34 @@ sub lspci {
     return $devices;
 }
 
+sub apply_pending_conf {
+    my ($conf, $vmid, $storecfg) = @_;
+
+    my $pendingconf = $conf->{pending};
+
+    #first unused pending disk
+    foreach my $pendingopt (keys %$pendingconf) {
+        if($pendingopt =~ m/^unused/) {
+	    my $opt = PVE::QemuServer::find_unused_volume($conf, $conf->{pending}->{$pendingopt}, $storecfg);
+	    delete $conf->{$opt} if $opt;
+	    $conf->{$pendingopt} = $conf->{pending}->{$pendingopt};
+	    delete $conf->{pending}->{$pendingopt};
+	}
+    }
+
+    foreach my $pendingopt (keys %$pendingconf) {
+
+	next if $pendingopt =~ m/^unused/;
+
+	if ($conf->{pending}->{$pendingopt} eq 'delete'){
+	    delete $conf->{$pendingopt};
+	}else{
+	    $conf->{$pendingopt} = $conf->{pending}->{$pendingopt};
+	}
+	delete $conf->{pending}->{$pendingopt};
+    }
+
+    PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+}
+
 1;
diff --git a/pve-bridge b/pve-bridge
index d6c5eb8..caee33b 100755
--- a/pve-bridge
+++ b/pve-bridge
@@ -20,6 +20,10 @@ my $migratedfrom = $ENV{PVE_MIGRATED_FROM};
 
 my $conf = PVE::QemuServer::load_config($vmid, $migratedfrom);
 
+if ($conf->{pending}->{$netid}){
+    $conf = $conf->{pending};
+}
+
 die "unable to get network config '$netid'\n"
     if !$conf->{$netid};
 
-- 
1.7.10.4




More information about the pve-devel mailing list