X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;ds=sidebyside;f=PVE%2FAPI2%2FQemu.pm;h=c06e5c7a7b33e9325431ee75770c7fd352744e58;hb=3cf90d7a40554b4c353e389209d6ef36a89b96a7;hp=f7ae89dbffdd107c285b580a4335a5452715e28e;hpb=4a5a259072815b2693d8aefa8f0bdb3a245b899d;p=qemu-server.git diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm index f7ae89d..c06e5c7 100644 --- a/PVE/API2/Qemu.pm +++ b/PVE/API2/Qemu.pm @@ -3,6 +3,7 @@ package PVE::API2::Qemu; use strict; use warnings; use Cwd 'abs_path'; +use Net::SSLeay; use PVE::Cluster qw (cfs_read_file cfs_write_file);; use PVE::SafeSyslog; @@ -126,29 +127,28 @@ my $create_disks = sub { $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk); } else { - my $path = $rpcenv->check_volume_access($authuser, $storecfg, $vmid, $volid); + $rpcenv->check_volume_access($authuser, $storecfg, $vmid, $volid); - my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1); + my $volid_is_new = 1; - my $foundvolid = undef; + if ($conf->{$ds}) { + my $olddrive = PVE::QemuServer::parse_drive($ds, $conf->{$ds}); + $volid_is_new = undef if $olddrive->{file} && $olddrive->{file} eq $volid; + } - if ($storeid) { - PVE::Storage::activate_volumes($storecfg, [ $volid ]); - my $dl = PVE::Storage::vdisk_list($storecfg, $storeid, undef); + if ($volid_is_new) { - PVE::Storage::foreach_volid($dl, sub { - my ($volumeid) = @_; - if($volumeid eq $volid) { - $foundvolid = 1; - return; - } - }); - } + my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1); + + PVE::Storage::activate_volumes($storecfg, [ $volid ]) if $storeid; + + my $size = PVE::Storage::volume_size_info($storecfg, $volid); - die "image '$path' does not exists\n" if (!(-f $path || -b $path || $foundvolid)); + die "volume $volid does not exists\n" if !$size; + + $disk->{size} = $size; + } - my ($size) = PVE::Storage::volume_size_info($storecfg, $volid, 1); - $disk->{size} = $size; $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk); } }); @@ -190,7 +190,7 @@ my $check_vm_modify_config_perm = sub { $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']); } elsif ($opt eq 'args' || $opt eq 'lock') { die "only root can set '$opt' config\n"; - } elsif ($opt eq 'cpu' || $opt eq 'kvm' || $opt eq 'acpi' || + } elsif ($opt eq 'cpu' || $opt eq 'kvm' || $opt eq 'acpi' || $opt eq 'machine' || $opt eq 'vga' || $opt eq 'watchdog' || $opt eq 'tablet') { $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']); } elsif ($opt =~ m/^net\d+$/) { @@ -249,37 +249,6 @@ __PACKAGE__->register_method({ }}); -sub add_vm_to_pool { - my ($vmid, $pool) = @_; - - my $addVMtoPoolFn = sub { - my $usercfg = cfs_read_file("user.cfg"); - if (my $data = $usercfg->{pools}->{$pool}) { - $data->{vms}->{$vmid} = 1; - $usercfg->{vms}->{$vmid} = $pool; - cfs_write_file("user.cfg", $usercfg); - } - }; - - PVE::AccessControl::lock_user_config($addVMtoPoolFn, "can't add VM $vmid to pool '$pool'"); -}; - -sub remove_vm_from_pool { - my ($vmid) = @_; - - my $delVMfromPoolFn = sub { - my $usercfg = cfs_read_file("user.cfg"); - if (my $pool = $usercfg->{vms}->{$vmid}) { - if (my $data = $usercfg->{pools}->{$pool}) { - delete $data->{vms}->{$vmid}; - delete $usercfg->{vms}->{$vmid}; - cfs_write_file("user.cfg", $usercfg); - } - } - }; - - PVE::AccessControl::lock_user_config($delVMfromPoolFn, "pool cleanup for VM $vmid failed"); -} __PACKAGE__->register_method({ name => 'create_vm', @@ -403,13 +372,8 @@ __PACKAGE__->register_method({ die "pipe requires cli environment\n" if $rpcenv->{type} ne 'cli'; } else { - my $path = $rpcenv->check_volume_access($authuser, $storecfg, $vmid, $archive); - - PVE::Storage::activate_volumes($storecfg, [ $archive ]) - if PVE::Storage::parse_volume_id ($archive, 1); - - die "can't find archive file '$archive'\n" if !($path && -f $path); - $archive = $path; + $rpcenv->check_volume_access($authuser, $storecfg, $vmid, $archive); + $archive = PVE::Storage::abs_filesystem_path($storecfg, $archive); } } @@ -430,7 +394,7 @@ __PACKAGE__->register_method({ pool => $pool, unique => $unique }); - add_vm_to_pool($vmid, $pool) if $pool; + PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool; }; return $rpcenv->fork_worker('qmrestore', $vmid, $authuser, $realcmd); @@ -479,7 +443,7 @@ __PACKAGE__->register_method({ die "create failed - $err"; } - add_vm_to_pool($vmid, $pool) if $pool; + PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool; }; return $rpcenv->fork_worker('qmcreate', $vmid, $authuser, $realcmd); @@ -524,10 +488,13 @@ __PACKAGE__->register_method({ { subdir => 'vncproxy' }, { subdir => 'migrate' }, { subdir => 'resize' }, + { subdir => 'move' }, { subdir => 'rrd' }, { subdir => 'rrddata' }, { subdir => 'monitor' }, { subdir => 'snapshot' }, + { subdir => 'spiceproxy' }, + { subdir => 'sendkey' }, ]; return $res; @@ -691,9 +658,19 @@ my $delete_drive = sub { if (!PVE::QemuServer::drive_is_cdrom($drive)) { my $volid = $drive->{file}; + if (&$vm_is_volid_owner($storecfg, $vmid, $volid)) { if ($force || $key =~ m/^unused/) { - eval { PVE::Storage::vdisk_free($storecfg, $volid); }; + eval { + # check if the disk is really unused + my $used_paths = PVE::QemuServer::get_used_paths($vmid, $storecfg, $conf, 1, $key); + my $path = PVE::Storage::path($storecfg, $volid); + + die "unable to delete '$volid' - volume is still in use (snapshot?)\n" + if $used_paths->{$path}; + + PVE::Storage::vdisk_free($storecfg, $volid); + }; die $@ if $@; } else { PVE::QemuServer::add_unused_volume($conf, $volid, $vmid); @@ -716,22 +693,22 @@ my $vmconfig_delete_option = sub { my $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.Allocate']); + $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']); } } my $unplugwarning = ""; - if($conf->{ostype} && $conf->{ostype} eq 'l26'){ + if ($conf->{ostype} && $conf->{ostype} eq 'l26') { $unplugwarning = "
verify that you have acpiphp && pci_hotplug modules loaded in your guest VM"; - }elsif($conf->{ostype} && $conf->{ostype} eq 'l24'){ + } elsif ($conf->{ostype} && $conf->{ostype} eq 'l24') { $unplugwarning = "
kernel 2.4 don't support hotplug, please disable hotplug in options"; - }elsif(!$conf->{ostype} || ($conf->{ostype} && $conf->{ostype} eq 'other')){ + } elsif (!$conf->{ostype} || ($conf->{ostype} && $conf->{ostype} eq 'other')) { $unplugwarning = "
verify that your guest support acpi hotplug"; } - if($opt eq 'tablet'){ + if ($opt eq 'tablet') { PVE::QemuServer::vm_deviceplug(undef, $conf, $vmid, $opt); - }else{ + } else { die "error hot-unplug $opt $unplugwarning" if !PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt); } @@ -786,10 +763,26 @@ my $vmconfig_update_disk = sub { &$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})) { - PVE::QemuServer::qemu_block_set_io_throttle($vmid,"drive-$opt", $drive->{mbps}*1024*1024, - $drive->{mbps_rd}*1024*1024, $drive->{mbps_wr}*1024*1024, - $drive->{iops}, $drive->{iops_rd}, $drive->{iops_wr}) + &$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, + $drive->{iops} || 0, + $drive->{iops_rd} || 0, + $drive->{iops_wr} || 0, + ($drive->{mbps_max} || 0)*1024*1024, + ($drive->{mbps_rd_max} || 0)*1024*1024, + ($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); } } @@ -859,140 +852,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', - ]; +# 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. -__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 $update_vm_api = sub { + my ($param, $sync) = @_; - my $rpcenv = PVE::RPCEnvironment::get(); + my $rpcenv = PVE::RPCEnvironment::get(); - my $authuser = $rpcenv->get_user(); + my $authuser = $rpcenv->get_user(); - my $node = extract_param($param, 'node'); + my $node = extract_param($param, 'node'); - my $vmid = extract_param($param, 'vmid'); + my $vmid = extract_param($param, 'vmid'); - my $digest = extract_param($param, 'digest'); + my $digest = extract_param($param, 'digest'); - my @paramarr = (); # used for log message - foreach my $key (keys %$param) { - push @paramarr, "-$key", $param->{$key}; - } + my $background_delay = extract_param($param, 'background_delay'); - my $skiplock = extract_param($param, 'skiplock'); - raise_param_exc({ skiplock => "Only root may use this option." }) - if $skiplock && $authuser ne 'root@pam'; + my @paramarr = (); # used for log message + foreach my $key (keys %$param) { + push @paramarr, "-$key", $param->{$key}; + } - my $delete_str = extract_param($param, 'delete'); + my $skiplock = extract_param($param, 'skiplock'); + raise_param_exc({ skiplock => "Only root may use this option." }) + if $skiplock && $authuser ne 'root@pam'; - my $force = extract_param($param, 'force'); + my $delete_str = extract_param($param, 'delete'); - die "no options specified\n" if !$delete_str && !scalar(keys %$param); + my $force = extract_param($param, 'force'); - my $storecfg = PVE::Storage::config(); + die "no options specified\n" if !$delete_str && !scalar(keys %$param); - my $defaults = PVE::QemuServer::load_defaults(); + my $storecfg = PVE::Storage::config(); - &$resolve_cdrom_alias($param); + my $defaults = PVE::QemuServer::load_defaults(); - # now try to verify all parameters + &$resolve_cdrom_alias($param); - 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}); + # now try to verify all parameters - if (!PVE::QemuServer::option_exists($opt)) { - raise_param_exc({ delete => "unknown option '$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}); - push @delete, $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}; + 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 > $maxmem; - } + die "balloon value too large (must be smaller than assigned memory)\n" + if $balloon && $balloon > $maxmem; + } + + PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join (' ', @paramarr)); - PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join (' ', @paramarr)); + my $worker = sub { + + print "update VM $vmid: " . join (' ', @paramarr) . "\n"; foreach my $opt (@delete) { # delete $conf = PVE::QemuServer::load_config($vmid); # update/reload @@ -1021,9 +985,13 @@ __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); } + + if($opt eq 'cores' && $conf->{maxcpus}){ + PVE::QemuServer::qemu_cpu_hotplug($vmid, $conf, $param->{$opt}); + } $conf->{$opt} = $param->{$opt}; PVE::QemuServer::update_config_nolock($vmid, $conf, 1); @@ -1036,13 +1004,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({ @@ -1102,7 +1204,7 @@ __PACKAGE__->register_method({ PVE::QemuServer::vm_destroy($storecfg, $vmid, $skiplock); - remove_vm_from_pool($vmid); + PVE::AccessControl::remove_vm_from_pool($vmid); }; return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd); @@ -1183,6 +1285,8 @@ __PACKAGE__->register_method({ my $vmid = $param->{vmid}; my $node = $param->{node}; + my $conf = PVE::QemuServer::load_config($vmid, $node); # check if VM exists + my $authpath = "/vms/$vmid"; my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath); @@ -1193,14 +1297,14 @@ __PACKAGE__->register_method({ my $port = PVE::Tools::next_vnc_port(); my $remip; + my $remcmd = []; if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) { $remip = PVE::Cluster::remote_node_ip($node); + # NOTE: kvm VNC traffic is already TLS encrypted + $remcmd = ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes', $remip]; } - # NOTE: kvm VNC traffic is already TLS encrypted - my $remcmd = $remip ? ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes', $remip] : []; - my $timeout = 10; my $realcmd = sub { @@ -1208,12 +1312,24 @@ __PACKAGE__->register_method({ syslog('info', "starting vnc proxy $upid\n"); - my $qmcmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid]; + my $cmd; - my $qmstr = join(' ', @$qmcmd); + if ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/)) { - # also redirect stderr (else we get RFB protocol errors) - my $cmd = ['/bin/nc', '-l', '-p', $port, '-w', $timeout, '-c', "$qmstr 2>/dev/null"]; + my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga} ]; + #my $termcmd = "/usr/bin/qm terminal -iface $conf->{vga}"; + $cmd = ['/usr/bin/vncterm', '-rfbport', $port, + '-timeout', $timeout, '-authpath', $authpath, + '-perm', 'Sys.Console', '-c', @$remcmd, @$termcmd]; + } else { + + my $qmcmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid]; + + my $qmstr = join(' ', @$qmcmd); + + # also redirect stderr (else we get RFB protocol errors) + $cmd = ['/bin/nc', '-l', '-p', $port, '-w', $timeout, '-c', "$qmstr 2>/dev/null"]; + } PVE::Tools::run_command($cmd); @@ -1233,6 +1349,50 @@ __PACKAGE__->register_method({ }; }}); +__PACKAGE__->register_method({ + name => 'spiceproxy', + path => '{vmid}/spiceproxy', + method => 'POST', + protected => 1, + proxyto => 'node', + permissions => { + check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]], + }, + description => "Returns a SPICE configuration to connect to the VM.", + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid'), + proxy => get_standard_option('spice-proxy', { optional => 1 }), + }, + }, + returns => get_standard_option('remote-viewer-config'), + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + my $vmid = $param->{vmid}; + my $node = $param->{node}; + my $proxy = $param->{proxy}; + + my $conf = PVE::QemuServer::load_config($vmid, $node); + my $title = "VM $vmid - $conf->{'name'}", + + my $port = PVE::QemuServer::spice_port($vmid); + + my ($ticket, undef, $remote_viewer_config) = + PVE::AccessControl::remote_viewer_config($authuser, $vmid, $node, $proxy, $title, $port); + + PVE::QemuServer::vm_mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket); + PVE::QemuServer::vm_mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30"); + + return $remote_viewer_config; + }}); + __PACKAGE__->register_method({ name => 'vmcmdidx', path => '{vmid}/status', @@ -1313,6 +1473,8 @@ __PACKAGE__->register_method({ $status->{ha} = &$vm_is_ha_managed($param->{vmid}); + $status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga}); + return $status; }}); @@ -1334,7 +1496,7 @@ __PACKAGE__->register_method({ skiplock => get_standard_option('skiplock'), stateuri => get_standard_option('pve-qm-stateuri'), migratedfrom => get_standard_option('pve-node',{ optional => 1 }), - + machine => get_standard_option('pve-qm-machine'), }, }, returns => { @@ -1351,6 +1513,8 @@ __PACKAGE__->register_method({ my $vmid = extract_param($param, 'vmid'); + my $machine = extract_param($param, 'machine'); + my $stateuri = extract_param($param, 'stateuri'); raise_param_exc({ stateuri => "Only root may use this option." }) if $stateuri && $authuser ne 'root@pam'; @@ -1363,6 +1527,15 @@ __PACKAGE__->register_method({ raise_param_exc({ migratedfrom => "Only root may use this option." }) if $migratedfrom && $authuser ne 'root@pam'; + # read spice ticket from STDIN + my $spice_ticket; + if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type} eq 'cli')) { + if (defined(my $line = <>)) { + chomp $line; + $spice_ticket = $line; + } + } + my $storecfg = PVE::Storage::config(); if (&$vm_is_ha_managed($vmid) && !$stateuri && @@ -1391,7 +1564,8 @@ __PACKAGE__->register_method({ syslog('info', "start VM $vmid: $upid\n"); - PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom); + PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef, + $machine, $spice_ticket); return; }; @@ -1791,7 +1965,7 @@ __PACKAGE__->register_method({ type => "object", properties => { hasFeature => { type => 'boolean' }, - nodes => { + nodes => { type => 'array', items => { type => 'string' }, } @@ -1821,11 +1995,11 @@ __PACKAGE__->register_method({ my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg); my $hasFeature = PVE::QemuServer::has_feature($feature, $conf, $storecfg, $snapname, $running); - + return { hasFeature => $hasFeature, nodes => [ keys %$nodelist ], - }; + }; }}); __PACKAGE__->register_method({ @@ -1998,7 +2172,9 @@ __PACKAGE__->register_method({ my $net = PVE::QemuServer::parse_net($value); $net->{macaddr} = PVE::Tools::random_ether_addr(); $newconf->{$opt} = PVE::QemuServer::print_net($net); - } elsif (my $drive = PVE::QemuServer::parse_drive($opt, $value)) { + } elsif (PVE::QemuServer::valid_drivename($opt)) { + my $drive = PVE::QemuServer::parse_drive($opt, $value); + die "unable to parse drive options for '$opt'\n" if !$drive; if (PVE::QemuServer::drive_is_cdrom($drive)) { $newconf->{$opt} = $value; # simply copy configuration } else { @@ -2021,7 +2197,11 @@ __PACKAGE__->register_method({ if ($param->{name}) { $newconf->{name} = $param->{name}; } else { - $newconf->{name} = "Copy-of-$oldconf->{name}"; + if ($oldconf->{name}) { + $newconf->{name} = "Copy-of-$oldconf->{name}"; + } else { + $newconf->{name} = "Copy-of-VM-$vmid"; + } } if ($param->{description}) { @@ -2044,41 +2224,10 @@ __PACKAGE__->register_method({ foreach my $opt (keys %$drives) { my $drive = $drives->{$opt}; - my $newvolid; - if (!$drive->{full}) { - print "create linked clone of drive $opt ($drive->{file})\n"; - $newvolid = PVE::Storage::vdisk_clone($storecfg, $drive->{file}, $newid); - push @$newvollist, $newvolid; - - } else { - my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file}); - $storeid = $storage if $storage; - - my $fmt = undef; - if($format){ - $fmt = $format; - }else{ - my $defformat = PVE::Storage::storage_default_format($storecfg, $storeid); - $fmt = $drive->{format} || $defformat; - } - - my ($size) = PVE::Storage::volume_size_info($storecfg, $drive->{file}, 3); - - print "create full clone of drive $opt ($drive->{file})\n"; - $newvolid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $newid, $fmt, undef, ($size/1024)); - push @$newvollist, $newvolid; - - if(!$running || $snapname){ - PVE::QemuServer::qemu_img_convert($drive->{file}, $newvolid, $size, $snapname); - }else{ - PVE::QemuServer::qemu_drive_mirror($vmid, $opt, $newvolid, $newid); - } + my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $opt, $drive, $snapname, + $newid, $storage, $format, $drive->{full}, $newvollist); - } - - my ($size) = PVE::Storage::volume_size_info($storecfg, $newvolid, 3); - my $disk = { file => $newvolid, size => $size }; - $newconf->{$opt} = PVE::QemuServer::print_drive($vmid, $disk); + $newconf->{$opt} = PVE::QemuServer::print_drive($vmid, $newdrive); PVE::QemuServer::update_config_nolock($newid, $newconf, 1); } @@ -2087,12 +2236,15 @@ __PACKAGE__->register_method({ PVE::QemuServer::update_config_nolock($newid, $newconf, 1); if ($target) { + # always deactivate volumes - avoid lvm LVs to be active on several nodes + PVE::Storage::deactivate_volumes($storecfg, $vollist); + my $newconffile = PVE::QemuServer::config_file($newid, $target); die "Failed to move config to node '$target' - rename failed: $!\n" if !rename($conffile, $newconffile); } - add_vm_to_pool($newid, $pool) if $pool; + PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool; }; if (my $err = $@) { unlink $conffile; @@ -2119,6 +2271,160 @@ __PACKAGE__->register_method({ }}); +__PACKAGE__->register_method({ + name => 'move_vm_disk', + path => '{vmid}/move_disk', + method => 'POST', + protected => 1, + proxyto => 'node', + description => "Move volume to different storage.", + permissions => { + description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " . + "and 'Datastore.AllocateSpace' permissions on the storage.", + check => + [ 'and', + ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]], + ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]], + ], + }, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid'), + disk => { + type => 'string', + description => "The disk you want to move.", + enum => [ PVE::QemuServer::disknames() ], + }, + storage => get_standard_option('pve-storage-id', { description => "Target Storage." }), + 'format' => { + type => 'string', + description => "Target Format.", + enum => [ 'raw', 'qcow2', 'vmdk' ], + optional => 1, + }, + delete => { + type => 'boolean', + description => "Delete the original disk after successful copy. By default the original disk is kept as unused disk.", + optional => 1, + default => 0, + }, + 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 => 'string', + description => "the task ID.", + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + my $node = extract_param($param, 'node'); + + my $vmid = extract_param($param, 'vmid'); + + my $digest = extract_param($param, 'digest'); + + my $disk = extract_param($param, 'disk'); + + my $storeid = extract_param($param, 'storage'); + + my $format = extract_param($param, 'format'); + + my $storecfg = PVE::Storage::config(); + + my $updatefn = sub { + + my $conf = PVE::QemuServer::load_config($vmid); + + die "checksum missmatch (file change by other user?)\n" + if $digest && $digest ne $conf->{digest}; + + die "disk '$disk' does not exist\n" if !$conf->{$disk}; + + my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk}); + + my $old_volid = $drive->{file} || die "disk '$disk' has no associated volume\n"; + + die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive); + + my $oldfmt; + my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid); + if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){ + $oldfmt = $1; + } + + die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid && + (!$format || !$oldfmt || $oldfmt eq $format); + + PVE::Cluster::log_msg('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid"); + + my $running = PVE::QemuServer::check_running($vmid); + + PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]); + + my $realcmd = sub { + + my $newvollist = []; + + eval { + local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; }; + + my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $disk, $drive, undef, + $vmid, $storeid, $format, 1, $newvollist); + + $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $newdrive); + + PVE::QemuServer::add_unused_volume($conf, $old_volid) if !$param->{delete}; + + PVE::QemuServer::update_config_nolock($vmid, $conf, 1); + + eval { + # try to deactivate volumes - avoid lvm LVs to be active on several nodes + PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ]) + if !$running; + }; + warn $@ if $@; + }; + if (my $err = $@) { + + foreach my $volid (@$newvollist) { + eval { PVE::Storage::vdisk_free($storecfg, $volid); }; + warn $@ if $@; + } + die "storage migration failed: $err"; + } + + if ($param->{delete}) { + my $used_paths = PVE::QemuServer::get_used_paths($vmid, $storecfg, $conf, 1, 1); + my $path = PVE::Storage::path($storecfg, $old_volid); + if ($used_paths->{$path}){ + warn "volume $old_volid have snapshots. Can't delete it\n"; + PVE::QemuServer::add_unused_volume($conf, $old_volid); + PVE::QemuServer::update_config_nolock($vmid, $conf, 1); + } else { + eval { PVE::Storage::vdisk_free($storecfg, $old_volid); }; + warn $@ if $@; + } + } + }; + + return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd); + }; + + return PVE::QemuServer::lock_config($vmid, $updatefn); + }}); + __PACKAGE__->register_method({ name => 'migrate_vm', path => '{vmid}/migrate',