X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FAPI2%2FQemu.pm;h=2f07ae57449565400afb9c4a34d9083459fba1e6;hb=3e7567e05e1df6b11c2d3c4dee86aec9c8f57a14;hp=5d9cbb92cfec0f1be4ba380dd1980ae79374dfeb;hpb=8bd0cbf5c3b52be6acd85b79d142113f1fd11636;p=qemu-server.git diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm index 5d9cbb92..2f07ae57 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; @@ -17,6 +18,7 @@ use PVE::RPCEnvironment; use PVE::AccessControl; use PVE::INotify; use PVE::Network; +use PVE::API2::Firewall::VM; use Data::Dumper; # fixme: remove @@ -126,7 +128,7 @@ 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 $volid_is_new = 1; @@ -371,13 +373,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); } } @@ -498,11 +495,18 @@ __PACKAGE__->register_method({ { subdir => 'monitor' }, { subdir => 'snapshot' }, { subdir => 'spiceproxy' }, + { subdir => 'sendkey' }, + { subdir => 'firewall' }, ]; return $res; }}); +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Firewall::VM", + path => '{vmid}/firewall', +}); + __PACKAGE__->register_method({ name => 'rrd', path => '{vmid}/rrd', @@ -696,22 +700,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); } @@ -766,14 +770,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", + &$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_rd} || 0)*1024*1024, ($drive->{mbps_wr} || 0)*1024*1024, - $drive->{iops} || 0, - $drive->{iops_rd} || 0, - $drive->{iops_wr} || 0) + $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); } } @@ -822,9 +838,9 @@ my $vmconfig_update_net = sub { PVE::Network::tap_rate_limit($iface, $newnet->{rate}); } - if(($newnet->{bridge} ne $oldnet->{bridge}) || ($newnet->{tag} ne $oldnet->{tag})){ - eval{PVE::Network::tap_unplug($iface, $oldnet->{bridge}, $oldnet->{tag});}; - PVE::Network::tap_plug($iface, $newnet->{bridge}, $newnet->{tag}); + if(($newnet->{bridge} ne $oldnet->{bridge}) || ($newnet->{tag} ne $oldnet->{tag}) || ($newnet->{firewall} ne $oldnet->{firewall})){ + PVE::Network::tap_unplug($iface); + PVE::Network::tap_plug($iface, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}); } }else{ @@ -979,6 +995,10 @@ my $update_vm_api = sub { } 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); @@ -1250,6 +1270,11 @@ __PACKAGE__->register_method({ properties => { node => get_standard_option('pve-node'), vmid => get_standard_option('pve-vmid'), + websocket => { + optional => 1, + type => 'boolean', + description => "starts websockify instead of vncproxy", + }, }, }, returns => { @@ -1271,6 +1296,9 @@ __PACKAGE__->register_method({ my $vmid = $param->{vmid}; my $node = $param->{node}; + my $websocket = $param->{websocket}; + + my $conf = PVE::QemuServer::load_config($vmid, $node); # check if VM exists my $authpath = "/vms/$vmid"; @@ -1282,14 +1310,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 or is known unsecure + $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 { @@ -1297,12 +1325,28 @@ __PACKAGE__->register_method({ syslog('info', "starting vnc proxy $upid\n"); - my $qmcmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid]; + my $cmd; + + if ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/)) { + + die "Websocket mode is not supported in vga serial mode!" if $websocket; + + 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 { + + $ENV{LC_PVE_TICKET} = $ticket if $websocket; # set ticket with "qm vncproxy" - my $qmstr = join(' ', @$qmcmd); + my $qmcmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid]; - # also redirect stderr (else we get RFB protocol errors) - my $cmd = ['/bin/nc', '-l', '-p', $port, '-w', $timeout, '-c', "$qmstr 2>/dev/null"]; + 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); @@ -1323,29 +1367,31 @@ __PACKAGE__->register_method({ }}); __PACKAGE__->register_method({ - name => 'spiceproxy', - path => '{vmid}/spiceproxy', - method => 'GET', # fixme: should be POST, but howto handle that in the HTML client - protected => 1, - proxyto => 'node', # fixme: use direct connections or ssh tunnel? + name => 'vncwebsocket', + path => '{vmid}/vncwebsocket', + method => 'GET', + proxyto => 'node', permissions => { check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]], }, - description => "Returns a SPICE configuration to connect to the VM.", + description => "Opens a weksocket for VNV traffic.", parameters => { additionalProperties => 0, properties => { node => get_standard_option('pve-node'), vmid => get_standard_option('pve-vmid'), + port => { + description => "Port number returned by previous vncproxy call.", + type => 'integer', + minimum => 5900, + maximum => 5999, + }, }, }, returns => { - additionalProperties => 1, + type => "object", properties => { - type => { type => 'string' }, - password => { type => 'string' }, - host => { type => 'string' }, - port => { type => 'integer' }, + port => { type => 'string' }, }, }, code => sub { @@ -1358,82 +1404,59 @@ __PACKAGE__->register_method({ my $vmid = $param->{vmid}; my $node = $param->{node}; - my $port = PVE::Tools::next_vnc_port(); - - my $remip; + my $conf = PVE::QemuServer::load_config($vmid, $node); # VM exists ? - # Note: we currectly use "proxyto => 'node'", so this code will never trigger - if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) { - $remip = PVE::Cluster::remote_node_ip($node); - } + # Note: VNC ports are acessible from outside, so we do not gain any + # security if we verify that $param->{port} belongs to VM $vmid. This + # check is done by verifying the VNC ticket (inside VNC protocol). - my $authpath = "/vms/$vmid"; + my $port = $param->{port}; + + return { port => $port }; + }}); - my $ticket = PVE::AccessControl::assemble_spice_ticket($authuser, $authpath); +__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) = @_; - # limit ticket length to 59 charachters - $ticket = substr($ticket, 0, 59); + my $rpcenv = PVE::RPCEnvironment::get(); - my $timeout = 10; + my $authuser = $rpcenv->get_user(); - # Note: this only works if VM is on local node - PVE::QemuServer::vm_mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket); - PVE::QemuServer::vm_mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30"); + my $vmid = $param->{vmid}; + my $node = $param->{node}; + my $proxy = $param->{proxy}; - my $remcmd = []; #fixme + my $conf = PVE::QemuServer::load_config($vmid, $node); + my $title = "VM $vmid - $conf->{'name'}", - my $realcmd = sub { - my $upid = shift; + my $port = PVE::QemuServer::spice_port($vmid); - syslog('info', "starting spice proxy $upid\n"); + my ($ticket, undef, $remote_viewer_config) = + PVE::AccessControl::remote_viewer_config($authuser, $vmid, $node, $proxy, $title, $port); - my $socket = PVE::QemuServer::spice_socket($vmid); + PVE::QemuServer::vm_mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket); + PVE::QemuServer::vm_mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30"); - my $cmd = ['/usr/bin/socat', '-d', '-d', - "TCP-LISTEN:$port,reuseaddr,fork" ]; - - if ($remip) { - push @$cmd, "EXEC:'ssh root@$remip socat STDIO UNIX-CONNECT:$socket"; - } else { - push @$cmd, "UNIX-CONNECT:$socket"; - } - - my $conn_count = 0; - - my $parser = sub { - my $line = shift; - print "$line\n"; - if ($line =~ /successfully connected from/) { - $conn_count++; - } elsif ($line =~ /N exiting with status/ || $line =~ m/N exit\(/) { - $conn_count--; - die "client exit\n" if $conn_count <= 0; - } - }; - - eval { PVE::Tools::run_command($cmd, errfunc => $parser, outfunc => sub{}); }; - if (my $err = $@) { - die $err if $err !~ m/client exit$/; - } - - return; - }; - - my $upid = $rpcenv->fork_worker('spiceproxy', $vmid, $authuser, $realcmd); - - PVE::Tools::wait_for_vnc_port($port); - - # fimxe: ?? - my $host = `hostname -f` || PVE::INotify::nodename(); - chomp $host; - - return { - type => 'spice', - host => $host, - port => $port, - password => $ticket, - upid => $upid, - }; + return $remote_viewer_config; }}); __PACKAGE__->register_method({ @@ -1516,6 +1539,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; }}); @@ -1568,6 +1593,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 && @@ -1596,7 +1630,8 @@ __PACKAGE__->register_method({ syslog('info', "start VM $vmid: $upid\n"); - PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef, $machine); + PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef, + $machine, $spice_ticket); return; }; @@ -2203,7 +2238,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 { @@ -2265,6 +2302,9 @@ __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); @@ -2414,6 +2454,13 @@ __PACKAGE__->register_method({ 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 = $@) { @@ -2425,8 +2472,16 @@ __PACKAGE__->register_method({ } if ($param->{delete}) { - eval { PVE::Storage::vdisk_free($storecfg, $old_volid); }; - warn $@ if $@; + 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 $@; + } } };