From 5555edea95d07f78b7281703270f185f5b9f105a Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Fri, 7 Jun 2013 11:41:58 +0200 Subject: [PATCH] update_vm_async: new asynchronous API --- PVE/API2/Qemu.pm | 329 +++++++++++++++++++++++++++++++---------------- 1 file changed, 217 insertions(+), 112 deletions(-) diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm index ecad915..b99045e 100644 --- a/PVE/API2/Qemu.pm +++ b/PVE/API2/Qemu.pm @@ -839,140 +839,111 @@ my $vmconfig_update_net = sub { die "error hotplug $opt" if !PVE::QemuServer::vm_deviceplug($storecfg, $conf, $vmid, $opt, $net); }; -my $vm_config_perm_list = [ - 'VM.Config.Disk', - 'VM.Config.CDROM', - 'VM.Config.CPU', - 'VM.Config.Memory', - 'VM.Config.Network', - 'VM.Config.HWType', - 'VM.Config.Options', - ]; - -__PACKAGE__->register_method({ - name => 'update_vm', - path => '{vmid}/config', - method => 'PUT', - protected => 1, - proxyto => 'node', - description => "Set virtual machine options.", - permissions => { - check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1], - }, - parameters => { - additionalProperties => 0, - properties => PVE::QemuServer::json_config_properties( - { - node => get_standard_option('pve-node'), - vmid => get_standard_option('pve-vmid'), - skiplock => get_standard_option('skiplock'), - delete => { - type => 'string', format => 'pve-configid-list', - description => "A list of settings you want to delete.", - optional => 1, - }, - force => { - type => 'boolean', - description => $opt_force_description, - optional => 1, - requires => 'delete', - }, - digest => { - type => 'string', - description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.', - maxLength => 40, - optional => 1, - } - }), - }, - returns => { type => 'null'}, - code => sub { - my ($param) = @_; - - my $rpcenv = PVE::RPCEnvironment::get(); +# POST/PUT {vmid}/config implementation +# +# The original API used PUT (idempotent) an we assumed that all operations +# are fast. But it turned out that almost any configuration change can +# involve hot-plug actions, or disk alloc/free. Such actions can take long +# time to complete and have side effects (not idempotent). +# +# The new implementation uses POST and forks a worker process. We added +# a new option 'background_delay'. If specified we wait up to +# 'background_delay' second for the worker task to complete. It returns null +# if the task is finished within that time, else we return the UPID. + +my $update_vm_api = sub { + my ($param, $sync) = @_; - my $authuser = $rpcenv->get_user(); + my $rpcenv = PVE::RPCEnvironment::get(); - my $node = extract_param($param, 'node'); + my $authuser = $rpcenv->get_user(); - my $vmid = extract_param($param, 'vmid'); + my $node = extract_param($param, 'node'); - my $digest = extract_param($param, 'digest'); + my $vmid = extract_param($param, 'vmid'); - my @paramarr = (); # used for log message - foreach my $key (keys %$param) { - push @paramarr, "-$key", $param->{$key}; - } + my $digest = extract_param($param, 'digest'); - my $skiplock = extract_param($param, 'skiplock'); - raise_param_exc({ skiplock => "Only root may use this option." }) - if $skiplock && $authuser ne 'root@pam'; + my $background_delay = extract_param($param, 'background_delay'); - my $delete_str = extract_param($param, 'delete'); + my @paramarr = (); # used for log message + foreach my $key (keys %$param) { + push @paramarr, "-$key", $param->{$key}; + } - my $force = extract_param($param, 'force'); + my $skiplock = extract_param($param, 'skiplock'); + raise_param_exc({ skiplock => "Only root may use this option." }) + if $skiplock && $authuser ne 'root@pam'; - die "no options specified\n" if !$delete_str && !scalar(keys %$param); + my $delete_str = extract_param($param, 'delete'); - my $storecfg = PVE::Storage::config(); + my $force = extract_param($param, 'force'); - my $defaults = PVE::QemuServer::load_defaults(); + die "no options specified\n" if !$delete_str && !scalar(keys %$param); - &$resolve_cdrom_alias($param); + my $storecfg = PVE::Storage::config(); - # now try to verify all parameters + my $defaults = PVE::QemuServer::load_defaults(); - my @delete = (); - foreach my $opt (PVE::Tools::split_list($delete_str)) { - $opt = 'ide2' if $opt eq 'cdrom'; - raise_param_exc({ delete => "you can't use '-$opt' and " . - "-delete $opt' at the same time" }) - if defined($param->{$opt}); + &$resolve_cdrom_alias($param); - if (!PVE::QemuServer::option_exists($opt)) { - raise_param_exc({ delete => "unknown option '$opt'" }); - } + # now try to verify all parameters - push @delete, $opt; + my @delete = (); + foreach my $opt (PVE::Tools::split_list($delete_str)) { + $opt = 'ide2' if $opt eq 'cdrom'; + raise_param_exc({ delete => "you can't use '-$opt' and " . + "-delete $opt' at the same time" }) + if defined($param->{$opt}); + + if (!PVE::QemuServer::option_exists($opt)) { + raise_param_exc({ delete => "unknown option '$opt'" }); } - foreach my $opt (keys %$param) { - if (PVE::QemuServer::valid_drivename($opt)) { - # cleanup drive path - my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}); - PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive); - $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive); - } elsif ($opt =~ m/^net(\d+)$/) { - # add macaddr - my $net = PVE::QemuServer::parse_net($param->{$opt}); - $param->{$opt} = PVE::QemuServer::print_net($net); - } + push @delete, $opt; + } + + foreach my $opt (keys %$param) { + if (PVE::QemuServer::valid_drivename($opt)) { + # cleanup drive path + my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}); + PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive); + $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive); + } elsif ($opt =~ m/^net(\d+)$/) { + # add macaddr + my $net = PVE::QemuServer::parse_net($param->{$opt}); + $param->{$opt} = PVE::QemuServer::print_net($net); } + } - &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]); + &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]); - &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]); + &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]); - &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param); + &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param); - my $updatefn = sub { + my $updatefn = sub { - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuServer::load_config($vmid); - die "checksum missmatch (file change by other user?)\n" - if $digest && $digest ne $conf->{digest}; + die "checksum missmatch (file change by other user?)\n" + if $digest && $digest ne $conf->{digest}; - PVE::QemuServer::check_lock($conf) if !$skiplock; + PVE::QemuServer::check_lock($conf) if !$skiplock; + + if ($param->{memory} || defined($param->{balloon})) { + my $maxmem = $param->{memory} || $conf->{memory} || $defaults->{memory}; + my $balloon = defined($param->{balloon}) ? $param->{balloon} : $conf->{balloon}; + + die "balloon value too large (must be smaller than assigned memory)\n" + if $balloon && $balloon > $maxmem; + } - if ($param->{memory} || defined($param->{balloon})) { - my $maxmem = $param->{memory} || $conf->{memory} || $defaults->{memory}; - my $balloon = defined($param->{balloon}) ? $param->{balloon} : $conf->{balloon}; + PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join (' ', @paramarr)); - die "balloon value too large (must be smaller than assigned memory)\n" - if $balloon && $balloon > $maxmem; - } + my $worker = sub { - PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join (' ', @paramarr)); + print "update VM $vmid: " . join (' ', @paramarr) . "\n"; foreach my $opt (@delete) { # delete $conf = PVE::QemuServer::load_config($vmid); # update/reload @@ -980,11 +951,11 @@ __PACKAGE__->register_method({ } my $running = PVE::QemuServer::check_running($vmid); - + foreach my $opt (keys %$param) { # add/change - + $conf = PVE::QemuServer::load_config($vmid); # update/reload - + next if $conf->{$opt} && ($param->{$opt} eq $conf->{$opt}); # skip if nothing changed if (PVE::QemuServer::valid_drivename($opt)) { @@ -1001,7 +972,7 @@ __PACKAGE__->register_method({ if($opt eq 'tablet' && $param->{$opt} == 1){ PVE::QemuServer::vm_deviceplug(undef, $conf, $vmid, $opt); - }elsif($opt eq 'tablet' && $param->{$opt} == 0){ + } elsif($opt eq 'tablet' && $param->{$opt} == 0){ PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt); } @@ -1016,13 +987,147 @@ __PACKAGE__->register_method({ my $balloon = $param->{'balloon'} || $conf->{memory} || $defaults->{memory}; PVE::QemuServer::vm_mon_cmd($vmid, "balloon", value => $balloon*1024*1024); } - }; - PVE::QemuServer::lock_config($vmid, $updatefn); + if ($sync) { + &$worker(); + return undef; + } 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 undef if $status eq 'OK'; + die $status; + } + } + + return $upid; + } + }; + + return PVE::QemuServer::lock_config($vmid, $updatefn); +}; + +my $vm_config_perm_list = [ + 'VM.Config.Disk', + 'VM.Config.CDROM', + 'VM.Config.CPU', + 'VM.Config.Memory', + 'VM.Config.Network', + 'VM.Config.HWType', + 'VM.Config.Options', + ]; + +__PACKAGE__->register_method({ + name => 'update_vm_async', + path => '{vmid}/config', + method => 'POST', + protected => 1, + proxyto => 'node', + description => "Set virtual machine options (asynchrounous API).", + permissions => { + check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1], + }, + parameters => { + additionalProperties => 0, + properties => PVE::QemuServer::json_config_properties( + { + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid'), + skiplock => get_standard_option('skiplock'), + delete => { + type => 'string', format => 'pve-configid-list', + description => "A list of settings you want to delete.", + optional => 1, + }, + force => { + type => 'boolean', + description => $opt_force_description, + optional => 1, + requires => 'delete', + }, + digest => { + type => 'string', + description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.', + maxLength => 40, + optional => 1, + }, + background_delay => { + type => 'integer', + description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.", + minimum => 1, + maximum => 30, + optional => 1, + }, + }), + }, + returns => { + type => 'string', + optional => 1, + }, + code => $update_vm_api, +}); + +__PACKAGE__->register_method({ + name => 'update_vm', + path => '{vmid}/config', + method => 'PUT', + protected => 1, + proxyto => 'node', + description => "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.", + permissions => { + check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1], + }, + parameters => { + additionalProperties => 0, + properties => PVE::QemuServer::json_config_properties( + { + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid'), + skiplock => get_standard_option('skiplock'), + delete => { + type => 'string', format => 'pve-configid-list', + description => "A list of settings you want to delete.", + optional => 1, + }, + force => { + type => 'boolean', + description => $opt_force_description, + optional => 1, + requires => 'delete', + }, + digest => { + type => 'string', + description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.', + maxLength => 40, + optional => 1, + }, + }), + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + &$update_vm_api($param, 1); return undef; - }}); + } +}); __PACKAGE__->register_method({ -- 2.39.2