]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/API2/Qemu.pm
Allow VMAdmin to delete disk with Datastore.AllocateSpace permissions
[qemu-server.git] / PVE / API2 / Qemu.pm
index b99045e7066f6bdc7521906c56f85ab4ed530c3f..bf16bf23c2c4b435ddfe5e410f04142a82251dd9 100644 (file)
@@ -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;
@@ -128,27 +129,26 @@ my $create_disks = sub {
 
            my $path = $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);
        }
     });
@@ -498,6 +498,7 @@ __PACKAGE__->register_method({
            { subdir => 'rrddata' },
            { subdir => 'monitor' },
            { subdir => 'snapshot' },
+           { subdir => 'spiceproxy' },
            ];
 
        return $res;
@@ -663,16 +664,16 @@ my $delete_drive = sub {
        my $volid = $drive->{file};
 
        if (&$vm_is_volid_owner($storecfg, $vmid, $volid)) {
-           if ($force || $key =~ m/^unused/) {    
-               eval { 
-                   # check if the disk is really unused 
+           if ($force || $key =~ m/^unused/) {
+               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); 
+                   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); 
+                   PVE::Storage::vdisk_free($storecfg, $volid);
                };
                die $@ if $@;
            } else {
@@ -696,7 +697,7 @@ 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']);
        }
     }
 
@@ -767,9 +768,13 @@ my $vmconfig_update_disk = sub {
                &$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})
+               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)
                   if !PVE::QemuServer::drive_is_cdrom($drive);
             }
        }
@@ -846,11 +851,11 @@ my $vmconfig_update_net = sub {
 # 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 
+# 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 
+# '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) = @_;
 
@@ -895,7 +900,7 @@ my $update_vm_api  = sub {
        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'" });
        }
@@ -930,11 +935,11 @@ my $update_vm_api  = sub {
            if $digest && $digest ne $conf->{digest};
 
        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;
        }
@@ -951,11 +956,11 @@ my $update_vm_api  = sub {
            }
 
            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)) {
@@ -998,7 +1003,7 @@ my $update_vm_api  = sub {
            if ($background_delay) {
 
                # Note: It would be better to do that in the Event based HTTPServer
-               # to avoid blocking call to sleep. 
+               # to avoid blocking call to sleep.
 
                my $end_time = time() + $background_delay;
 
@@ -1016,7 +1021,7 @@ my $update_vm_api  = sub {
                    return undef if $status eq 'OK';
                    die $status;
                }
-           } 
+           }
 
            return $upid;
        }
@@ -1268,6 +1273,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);
@@ -1278,14 +1285,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 {
@@ -1293,12 +1300,24 @@ __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+$/)) {
+
+               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);
+               my $qmstr = join(' ', @$qmcmd);
 
-           # also redirect stderr (else we get RFB protocol errors)
-           my $cmd = ['/bin/nc', '-l', '-p', $port, '-w', $timeout, '-c', "$qmstr 2>/dev/null"];
+               # 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);
 
@@ -1318,6 +1337,83 @@ __PACKAGE__->register_method({
        };
     }});
 
+__PACKAGE__->register_method({
+    name => 'spiceproxy',
+    path => '{vmid}/spiceproxy',
+    method => 'GET',
+    protected => 1,
+    proxyto => 'node', # fixme: use direct connections or ssh tunnel?
+    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 => {
+               description => "This can be used by the client to specify the proxy server. All nodes in a cluster runs 'spiceproxy', so it is up to the client to choose one. By default, we return the node where the VM is currently running. As resonable setting is to use same node you use to connect to the API (This is window.location.hostname for the JS GUI).",
+               type => 'string', format => 'dns-name',
+               optional => 1,
+           },
+       },
+    },
+    returns => {
+       description => "Returned values can be directly passed to the 'remote-viewer' application.",
+       additionalProperties => 1,
+       properties => {
+           type => { type => 'string' },
+           password => { type => 'string' },
+           proxy => { type => 'string' },
+           host => { type => 'string' },
+           'tls-port' => { type => 'integer' },
+       },
+    },
+    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 ($ticket, $proxyticket) = PVE::AccessControl::assemble_spice_ticket($authuser, $vmid, $node);
+
+       my $timeout = 10;
+
+       my $port = PVE::QemuServer::spice_port($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");
+
+       if (!$proxy) {
+           my $host = `hostname -f` || PVE::INotify::nodename();
+           chomp $host;
+           $proxy = $host;
+       }
+
+       my $filename = "/etc/pve/local/pve-ssl.pem";
+       my $subject = PVE::QemuServer::read_x509_subject_spice($filename);
+
+       my $cacert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192);
+       $cacert =~ s/\n/\\n/g;
+
+       return {
+           type => 'spice',
+           title => "VM $vmid",
+           host => $proxyticket, # this break tls hostname verification, so we need to use 'host-subject'
+           proxy => "http://$proxy:3128",
+           'tls-port' => $port,
+           'host-subject' => $subject,
+           ca => $cacert,
+           password => $ticket,
+           'delete-this-file' => 1,
+       };
+    }});
+
 __PACKAGE__->register_method({
     name => 'vmcmdidx',
     path => '{vmid}/status',
@@ -1398,6 +1494,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;
     }});
 
@@ -1450,6 +1548,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 &&
@@ -1478,7 +1585,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;
            };
@@ -1878,7 +1986,7 @@ __PACKAGE__->register_method({
        type => "object",
        properties => {
            hasFeature => { type => 'boolean' },
-           nodes => {  
+           nodes => {
                type => 'array',
                items => { type => 'string' },
            }
@@ -1908,11 +2016,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({
@@ -2189,7 +2297,7 @@ __PACKAGE__->register_method({
     permissions => {
        description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
            "and 'Datastore.AllocateSpace' permissions on the storage.",
-       check => 
+       check =>
        [ 'and',
          ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
          ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]],
@@ -2272,7 +2380,7 @@ __PACKAGE__->register_method({
                $oldfmt = $1;
            }
 
-           die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid && 
+           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");
@@ -2294,7 +2402,7 @@ __PACKAGE__->register_method({
                    $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);
                };
                if (my $err = $@) {