X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FAPI2%2FQemu.pm;h=c4d648e26acb2641aa699612308708d5670997b5;hb=a5d5341cb2defd6c1796dbeafdf08bd66463ce95;hp=39bd248cab1decb71831d10cefe4a597a42bc9b2;hpb=0dbcc8c9a1a43e46bdcf4fae1b1e4b704bffafca;p=qemu-server.git diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm index 39bd248..c4d648e 100644 --- a/PVE/API2/Qemu.pm +++ b/PVE/API2/Qemu.pm @@ -13,6 +13,7 @@ use PVE::Exception qw(raise raise_param_exc raise_perm_exc); use PVE::Storage; use PVE::JSONSchema qw(get_standard_option); use PVE::RESTHandler; +use PVE::QemuConfig; use PVE::QemuServer; use PVE::QemuMigrate; use PVE::RPCEnvironment; @@ -122,10 +123,35 @@ my $create_disks = sub { die "no storage ID specified (and no default storage)\n" if !$storeid; my $defformat = PVE::Storage::storage_default_format($storecfg, $storeid); my $fmt = $disk->{format} || $defformat; - my $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, - $fmt, undef, $size*1024*1024); - $disk->{file} = $volid; - $disk->{size} = $size*1024*1024*1024; + + my $volid; + if ($ds eq 'efidisk0') { + # handle efidisk + my $ovmfvars = '/usr/share/kvm/OVMF_VARS-pure-efi.fd'; + die "uefi vars image not found\n" if ! -f $ovmfvars; + $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, + $fmt, undef, 128); + $disk->{file} = $volid; + $disk->{size} = 128*1024; + my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid); + my $scfg = PVE::Storage::storage_config($storecfg, $storeid); + my $qemufmt = PVE::QemuServer::qemu_img_format($scfg, $volname); + my $path = PVE::Storage::path($storecfg, $volid); + my $efidiskcmd = ['/usr/bin/qemu-img', 'convert', '-n', '-f', 'raw', '-O', $qemufmt]; + push @$efidiskcmd, $ovmfvars; + push @$efidiskcmd, $path; + + PVE::Storage::activate_volumes($storecfg, [$volid]); + + eval { PVE::Tools::run_command($efidiskcmd); }; + my $err = $@; + die "Copying of EFI Vars image failed: $err" if $err; + } else { + $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, + $fmt, undef, $size*1024*1024); + $disk->{file} = $volid; + $disk->{size} = $size*1024*1024*1024; + } push @$vollist, $volid; delete $disk->{format}; # no longer needed $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk); @@ -175,6 +201,64 @@ my $create_disks = sub { return $vollist; }; +my $cpuoptions = { + 'cores' => 1, + 'cpu' => 1, + 'cpulimit' => 1, + 'cpuunits' => 1, + 'numa' => 1, + 'smp' => 1, + 'sockets' => 1, + 'vcpus' => 1, +}; + +my $memoryoptions = { + 'memory' => 1, + 'balloon' => 1, + 'shares' => 1, +}; + +my $hwtypeoptions = { + 'acpi' => 1, + 'hotplug' => 1, + 'kvm' => 1, + 'machine' => 1, + 'scsihw' => 1, + 'smbios1' => 1, + 'tablet' => 1, + 'vga' => 1, + 'watchdog' => 1, +}; + +my $generaloptions = { + 'agent' => 1, + 'autostart' => 1, + 'bios' => 1, + 'description' => 1, + 'keyboard' => 1, + 'localtime' => 1, + 'migrate_downtime' => 1, + 'migrate_speed' => 1, + 'name' => 1, + 'onboot' => 1, + 'ostype' => 1, + 'protection' => 1, + 'reboot' => 1, + 'startdate' => 1, + 'startup' => 1, + 'tdf' => 1, + 'template' => 1, +}; + +my $vmpoweroptions = { + 'freeze' => 1, +}; + +my $diskoptions = { + 'boot' => 1, + 'bootdisk' => 1, +}; + my $check_vm_modify_config_perm = sub { my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_; @@ -182,37 +266,38 @@ my $check_vm_modify_config_perm = sub { foreach my $opt (@$key_list) { # disk checks need to be done somewhere else - next if PVE::QemuServer::valid_drivename($opt); + next if PVE::QemuServer::is_valid_drivename($opt); + next if $opt eq 'cdrom'; + next if $opt =~ m/^unused\d+$/; - if ($opt eq 'sockets' || $opt eq 'cores' || - $opt eq 'cpu' || $opt eq 'smp' || $opt eq 'vcpus' || - $opt eq 'cpulimit' || $opt eq 'cpuunits') { + if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) { $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']); - } elsif ($opt eq 'memory' || $opt eq 'balloon' || $opt eq 'shares') { + } elsif ($memoryoptions->{$opt}) { $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' || $opt eq 'machine' || - $opt eq 'vga' || $opt eq 'watchdog' || $opt eq 'tablet' || $opt eq 'smbios1') { + } elsif ($hwtypeoptions->{$opt}) { $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']); + } elsif ($generaloptions->{$opt}) { + $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']); + # special case for startup since it changes host behaviour + if ($opt eq 'startup') { + $rpcenv->check_full($authuser, "/", ['Sys.Modify']); + } + } elsif ($vmpoweroptions->{$opt}) { + $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']); + } elsif ($diskoptions->{$opt}) { + $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']); } elsif ($opt =~ m/^net\d+$/) { $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']); } else { - $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']); + # catches usb\d+, hostpci\d+, args, lock, etc. + # new options will be checked here + die "only root can set '$opt' config\n"; } } return 1; }; -my $check_protection = sub { - my ($vm_conf, $err_msg) = @_; - - if ($vm_conf->{protection}) { - die "$err_msg - protection mode enabled\n"; - } -}; - __PACKAGE__->register_method({ name => 'vmlist', path => '', @@ -228,6 +313,11 @@ __PACKAGE__->register_method({ additionalProperties => 0, properties => { node => get_standard_option('pve-node'), + full => { + type => 'boolean', + optional => 1, + description => "Determine the full status of active VMs.", + }, }, }, returns => { @@ -244,7 +334,7 @@ __PACKAGE__->register_method({ my $rpcenv = PVE::RPCEnvironment::get(); my $authuser = $rpcenv->get_user(); - my $vmstatus = PVE::QemuServer::vmstatus(); + my $vmstatus = PVE::QemuServer::vmstatus(undef, $param->{full}); my $res = []; foreach my $vmid (keys %$vmstatus) { @@ -334,7 +424,7 @@ __PACKAGE__->register_method({ my $pool = extract_param($param, 'pool'); - my $filename = PVE::QemuServer::config_file($vmid); + my $filename = PVE::QemuConfig->config_file($vmid); my $storecfg = PVE::Storage::config(); @@ -366,7 +456,7 @@ __PACKAGE__->register_method({ &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]); foreach my $opt (keys %$param) { - if (PVE::QemuServer::valid_drivename($opt)) { + if (PVE::QemuServer::is_valid_drivename($opt)) { my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}); raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive; @@ -394,15 +484,19 @@ __PACKAGE__->register_method({ if ($vmlist->{ids}->{$vmid}) { my $current_node = $vmlist->{ids}->{$vmid}->{node}; if ($current_node eq $node) { - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuConfig->load_config($vmid); - &$check_protection($conf, "unable to restore VM $vmid"); + PVE::QemuConfig->check_protection($conf, "unable to restore VM $vmid"); die "unable to restore vm $vmid - config file already exists\n" if !$force; die "unable to restore vm $vmid - vm is running\n" if PVE::QemuServer::check_running($vmid); + + die "unable to restore vm $vmid - vm is a template\n" + if PVE::QemuConfig->is_template($conf); + } else { die "unable to restore vm $vmid - already existing on cluster node '$current_node'\n"; } @@ -436,7 +530,7 @@ __PACKAGE__->register_method({ $vollist = &$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $param, $storage); # try to be smart about bootdisk - my @disks = PVE::QemuServer::disknames(); + my @disks = PVE::QemuServer::valid_drive_names(); my $firstdisk; foreach my $ds (reverse @disks) { next if !$conf->{$ds}; @@ -457,7 +551,7 @@ __PACKAGE__->register_method({ $conf->{smbios1} = "uuid=$uuid_str"; } - PVE::QemuServer::update_config_nolock($vmid, $conf); + PVE::QemuConfig->write_config($vmid, $conf); }; my $err = $@; @@ -476,7 +570,7 @@ __PACKAGE__->register_method({ return $rpcenv->fork_worker('qmcreate', $vmid, $authuser, $realcmd); }; - return PVE::QemuServer::lock_config_full($vmid, 1, $archive ? $restorefn : $createfn); + return PVE::QemuConfig->lock_config_full($vmid, 1, $archive ? $restorefn : $createfn); }}); __PACKAGE__->register_method({ @@ -520,6 +614,7 @@ __PACKAGE__->register_method({ { subdir => 'rrd' }, { subdir => 'rrddata' }, { subdir => 'monitor' }, + { subdir => 'agent' }, { subdir => 'snapshot' }, { subdir => 'spiceproxy' }, { subdir => 'sendkey' }, @@ -656,7 +751,7 @@ __PACKAGE__->register_method({ code => sub { my ($param) = @_; - my $conf = PVE::QemuServer::load_config($param->{vmid}); + my $conf = PVE::QemuConfig->load_config($param->{vmid}); delete $conf->{snapshots}; @@ -727,7 +822,7 @@ __PACKAGE__->register_method({ code => sub { my ($param) = @_; - my $conf = PVE::QemuServer::load_config($param->{vmid}); + my $conf = PVE::QemuConfig->load_config($param->{vmid}); my $pending_delete_hash = PVE::QemuServer::split_flagged_list($conf->{pending}->{delete}); @@ -846,9 +941,10 @@ my $update_vm_api = sub { } foreach my $opt (keys %$param) { - if (PVE::QemuServer::valid_drivename($opt)) { + if (PVE::QemuServer::is_valid_drivename($opt)) { # cleanup drive path my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}); + raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive; PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive); $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive); } elsif ($opt =~ m/^net(\d+)$/) { @@ -866,12 +962,12 @@ my $update_vm_api = sub { my $updatefn = sub { - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuConfig->load_config($vmid); die "checksum missmatch (file change by other user?)\n" if $digest && $digest ne $conf->{digest}; - PVE::QemuServer::check_lock($conf) if !$skiplock; + PVE::QemuConfig->check_lock($conf) if !$skiplock; foreach my $opt (keys %$revert) { if (defined($conf->{$opt})) { @@ -901,34 +997,34 @@ my $update_vm_api = sub { foreach my $opt (@delete) { $modified->{$opt} = 1; - $conf = PVE::QemuServer::load_config($vmid); # update/reload + $conf = PVE::QemuConfig->load_config($vmid); # update/reload if ($opt =~ m/^unused/) { my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt}); - &$check_protection($conf, "can't remove unused disk '$drive->{file}'"); + PVE::QemuConfig->check_protection($conf, "can't remove unused disk '$drive->{file}'"); $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']); if (PVE::QemuServer::try_deallocate_drive($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) { delete $conf->{$opt}; - PVE::QemuServer::update_config_nolock($vmid, $conf, 1); + PVE::QemuConfig->write_config($vmid, $conf); } - } elsif (PVE::QemuServer::valid_drivename($opt)) { - &$check_protection($conf, "can't remove drive '$opt'"); + } elsif (PVE::QemuServer::is_valid_drivename($opt)) { + PVE::QemuConfig->check_protection($conf, "can't remove drive '$opt'"); $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']); PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt})) if defined($conf->{pending}->{$opt}); PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force); - PVE::QemuServer::update_config_nolock($vmid, $conf, 1); + PVE::QemuConfig->write_config($vmid, $conf); } else { PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force); - PVE::QemuServer::update_config_nolock($vmid, $conf, 1); + PVE::QemuConfig->write_config($vmid, $conf); } } foreach my $opt (keys %$param) { # add/change $modified->{$opt} = 1; - $conf = PVE::QemuServer::load_config($vmid); # update/reload + $conf = PVE::QemuConfig->load_config($vmid); # update/reload next if defined($conf->{pending}->{$opt}) && ($param->{$opt} eq $conf->{pending}->{$opt}); # skip if nothing changed - if (PVE::QemuServer::valid_drivename($opt)) { + if (PVE::QemuServer::is_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']); @@ -943,13 +1039,13 @@ my $update_vm_api = sub { $conf->{pending}->{$opt} = $param->{$opt}; } PVE::QemuServer::vmconfig_undelete_pending_option($conf, $opt); - PVE::QemuServer::update_config_nolock($vmid, $conf, 1); + PVE::QemuConfig->write_config($vmid, $conf); } # remove pending changes when nothing changed - $conf = PVE::QemuServer::load_config($vmid); # update/reload + $conf = PVE::QemuConfig->load_config($vmid); # update/reload my $changes = PVE::QemuServer::vmconfig_cleanup_pending($conf); - PVE::QemuServer::update_config_nolock($vmid, $conf, 1) if $changes; + PVE::QemuConfig->write_config($vmid, $conf) if $changes; return if !scalar(keys %{$conf->{pending}}); @@ -957,7 +1053,7 @@ my $update_vm_api = sub { # apply pending changes - $conf = PVE::QemuServer::load_config($vmid); # update/reload + $conf = PVE::QemuConfig->load_config($vmid); # update/reload if ($running) { my $errors = {}; @@ -1003,7 +1099,7 @@ my $update_vm_api = sub { } }; - return PVE::QemuServer::lock_config($vmid, $updatefn); + return PVE::QemuConfig->lock_config($vmid, $updatefn); }; my $vm_config_perm_list = [ @@ -1156,11 +1252,11 @@ __PACKAGE__->register_method({ if $skiplock && $authuser ne 'root@pam'; # test if VM exists - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuConfig->load_config($vmid); my $storecfg = PVE::Storage::config(); - &$check_protection($conf, "can't remove VM $vmid"); + PVE::QemuConfig->check_protection($conf, "can't remove VM $vmid"); die "unable to remove VM $vmid - used in HA resources\n" if PVE::HA::Config::vm_is_ha_managed($vmid); @@ -1265,7 +1361,7 @@ __PACKAGE__->register_method({ my $node = $param->{node}; my $websocket = $param->{websocket}; - my $conf = PVE::QemuServer::load_config($vmid, $node); # check if VM exists + my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists my $authpath = "/vms/$vmid"; @@ -1382,7 +1478,7 @@ __PACKAGE__->register_method({ PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $authuser, $authpath); - my $conf = PVE::QemuServer::load_config($vmid, $node); # VM exists ? + my $conf = PVE::QemuConfig->load_config($vmid, $node); # VM exists ? # 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 @@ -1423,7 +1519,7 @@ __PACKAGE__->register_method({ my $node = $param->{node}; my $proxy = $param->{proxy}; - my $conf = PVE::QemuServer::load_config($vmid, $node); + my $conf = PVE::QemuConfig->load_config($vmid, $node); my $title = "VM $vmid"; $title .= " - ". $conf->{name} if $conf->{name}; @@ -1468,7 +1564,7 @@ __PACKAGE__->register_method({ my ($param) = @_; # test if VM exists - my $conf = PVE::QemuServer::load_config($param->{vmid}); + my $conf = PVE::QemuConfig->load_config($param->{vmid}); my $res = [ { subdir => 'current' }, @@ -1501,12 +1597,12 @@ __PACKAGE__->register_method({ my ($param) = @_; # test if VM exists - my $conf = PVE::QemuServer::load_config($param->{vmid}); + my $conf = PVE::QemuConfig->load_config($param->{vmid}); my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid}, 1); my $status = $vmstatus->{$param->{vmid}}; - $status->{ha} = PVE::HA::Config::vm_is_ha_managed($param->{vmid}); + $status->{ha} = PVE::HA::Config::get_service_status("vm:$param->{vmid}"); $status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga}); @@ -1532,6 +1628,19 @@ __PACKAGE__->register_method({ skiplock => get_standard_option('skiplock'), stateuri => get_standard_option('pve-qm-stateuri'), migratedfrom => get_standard_option('pve-node',{ optional => 1 }), + migration_type => { + type => 'string', + enum => ['secure', 'insecure'], + description => "Migration traffic is encrypted using an SSH " . + "tunnel by default. On secure, completely private networks " . + "this can be disabled to increase performance.", + optional => 1, + }, + migration_network => { + type => 'string', format => 'CIDR', + description => "CIDR of the (sub) network that is used for migration.", + optional => 1, + }, machine => get_standard_option('pve-qm-machine'), }, }, @@ -1563,6 +1672,14 @@ __PACKAGE__->register_method({ raise_param_exc({ migratedfrom => "Only root may use this option." }) if $migratedfrom && $authuser ne 'root@pam'; + my $migration_type = extract_param($param, 'migration_type'); + raise_param_exc({ migration_type => "Only root may use this option." }) + if $migration_type && $authuser ne 'root@pam'; + + my $migration_network = extract_param($param, 'migration_network'); + raise_param_exc({ migration_network => "Only root may use this option." }) + if $migration_network && $authuser ne 'root@pam'; + # read spice ticket from STDIN my $spice_ticket; if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type} eq 'cli')) { @@ -1584,7 +1701,7 @@ __PACKAGE__->register_method({ my $service = "vm:$vmid"; - my $cmd = ['ha-manager', 'enable', $service]; + my $cmd = ['ha-manager', 'set', $service, '--state', 'started']; print "Executing HA start for VM $vmid\n"; @@ -1603,7 +1720,7 @@ __PACKAGE__->register_method({ syslog('info', "start VM $vmid: $upid\n"); PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef, - $machine, $spice_ticket); + $machine, $spice_ticket, $migration_network, $migration_type); return; }; @@ -1618,7 +1735,8 @@ __PACKAGE__->register_method({ method => 'POST', protected => 1, proxyto => 'node', - description => "Stop virtual machine.", + description => "Stop virtual machine. The qemu process will exit immediately. This" . + "is akin to pulling the power plug of a running computer and may damage the VM data", permissions => { check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]], }, @@ -1637,7 +1755,7 @@ __PACKAGE__->register_method({ optional => 1, }, keepActive => { - description => "Do not decativate storage volumes.", + description => "Do not deactivate storage volumes.", type => 'boolean', optional => 1, default => 0, @@ -1680,7 +1798,7 @@ __PACKAGE__->register_method({ my $service = "vm:$vmid"; - my $cmd = ['ha-manager', 'disable', $service]; + my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped']; print "Executing HA stop for VM $vmid\n"; @@ -1763,7 +1881,8 @@ __PACKAGE__->register_method({ method => 'POST', protected => 1, proxyto => 'node', - description => "Shutdown virtual machine.", + description => "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." . + "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.", permissions => { check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]], }, @@ -1787,7 +1906,7 @@ __PACKAGE__->register_method({ default => 0, }, keepActive => { - description => "Do not decativate storage volumes.", + description => "Do not deactivate storage volumes.", type => 'boolean', optional => 1, default => 0, @@ -1818,18 +1937,63 @@ __PACKAGE__->register_method({ my $storecfg = PVE::Storage::config(); - my $realcmd = sub { - my $upid = shift; + my $shutdown = 1; - syslog('info', "shutdown VM $vmid: $upid\n"); + # if vm is paused, do not shutdown (but stop if forceStop = 1) + # otherwise, we will infer a shutdown command, but run into the timeout, + # then when the vm is resumed, it will instantly shutdown + # + # checking the qmp status here to get feedback to the gui/cli/api + # and the status query should not take too long + my $qmpstatus; + eval { + $qmpstatus = PVE::QemuServer::vm_qmp_command($vmid, { execute => "query-status" }, 0); + }; + my $err = $@ if $@; - PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout}, - 1, $param->{forceStop}, $keepActive); + if (!$err && $qmpstatus->{status} eq "paused") { + if ($param->{forceStop}) { + warn "VM is paused - stop instead of shutdown\n"; + $shutdown = 0; + } else { + die "VM is paused - cannot shutdown\n"; + } + } - return; - }; + if (PVE::HA::Config::vm_is_ha_managed($vmid) && + ($rpcenv->{type} ne 'ha')) { + + my $hacmd = sub { + my $upid = shift; + + my $service = "vm:$vmid"; + + my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped']; + + print "Executing HA stop for VM $vmid\n"; + + PVE::Tools::run_command($cmd); + + return; + }; + + return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd); + + } else { + + my $realcmd = sub { + my $upid = shift; + + syslog('info', "shutdown VM $vmid: $upid\n"); - return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd); + PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout}, + $shutdown, $param->{forceStop}, $keepActive); + + return; + }; + + return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd); + } }}); __PACKAGE__->register_method({ @@ -2032,7 +2196,7 @@ __PACKAGE__->register_method({ my $running = PVE::QemuServer::check_running($vmid); - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuConfig->load_config($vmid); if($snapname){ my $snap = $conf->{snapshots}->{$snapname}; @@ -2042,7 +2206,7 @@ __PACKAGE__->register_method({ my $storecfg = PVE::Storage::config(); my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg); - my $hasFeature = PVE::QemuServer::has_feature($feature, $conf, $storecfg, $snapname, $running); + my $hasFeature = PVE::QemuConfig->has_feature($feature, $conf, $storecfg, $snapname, $running); return { hasFeature => $hasFeature, @@ -2181,9 +2345,9 @@ __PACKAGE__->register_method({ # do all tests after lock # we also try to do all tests before we fork the worker - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuConfig->load_config($vmid); - PVE::QemuServer::check_lock($conf); + PVE::QemuConfig->check_lock($conf); my $verify_running = PVE::QemuServer::check_running($vmid) || 0; @@ -2198,7 +2362,7 @@ __PACKAGE__->register_method({ die "can't clone VM to node '$target' (VM uses local storage)\n" if $target && !$sharedvm; - my $conffile = PVE::QemuServer::config_file($newid); + my $conffile = PVE::QemuConfig->config_file($newid); die "unable to create VM $newid: config file already exists\n" if -f $conffile; @@ -2221,9 +2385,10 @@ __PACKAGE__->register_method({ # always change MAC! address if ($opt =~ m/^net(\d+)$/) { my $net = PVE::QemuServer::parse_net($value); - $net->{macaddr} = PVE::Tools::random_ether_addr(); + my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg'); + $net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix}); $newconf->{$opt} = PVE::QemuServer::print_net($net); - } elsif (PVE::QemuServer::valid_drivename($opt)) { + } elsif (PVE::QemuServer::is_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)) { @@ -2292,17 +2457,18 @@ __PACKAGE__->register_method({ $newconf->{$opt} = PVE::QemuServer::print_drive($vmid, $newdrive); - PVE::QemuServer::update_config_nolock($newid, $newconf, 1); + PVE::QemuConfig->write_config($newid, $newconf); } delete $newconf->{lock}; - PVE::QemuServer::update_config_nolock($newid, $newconf, 1); + PVE::QemuConfig->write_config($newid, $newconf); if ($target) { # always deactivate volumes - avoid lvm LVs to be active on several nodes - PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname); + PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running; + PVE::Storage::deactivate_volumes($storecfg, $newvollist); - my $newconffile = PVE::QemuServer::config_file($newid, $target); + my $newconffile = PVE::QemuConfig->config_file($newid, $target); die "Failed to move config to node '$target' - rename failed: $!\n" if !rename($conffile, $newconffile); } @@ -2329,9 +2495,9 @@ __PACKAGE__->register_method({ return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd); }; - return PVE::QemuServer::lock_config_mode($vmid, 1, $shared_lock, sub { + return PVE::QemuConfig->lock_config_mode($vmid, 1, $shared_lock, sub { # Aquire exclusive lock lock for $newid - return PVE::QemuServer::lock_config_full($newid, 1, $clonefn); + return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn); }); }}); @@ -2344,23 +2510,21 @@ __PACKAGE__->register_method({ 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' ]], - ], + 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 => { + properties => { node => get_standard_option('pve-node'), vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }), disk => { type => 'string', description => "The disk you want to move.", - enum => [ PVE::QemuServer::disknames() ], + enum => [ PVE::QemuServer::valid_drive_names() ], }, storage => get_standard_option('pve-storage-id', { description => "Target storage.", @@ -2413,7 +2577,9 @@ __PACKAGE__->register_method({ my $updatefn = sub { - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuConfig->load_config($vmid); + + PVE::QemuConfig->check_lock($conf); die "checksum missmatch (file change by other user?)\n" if $digest && $digest ne $conf->{digest}; @@ -2435,6 +2601,11 @@ __PACKAGE__->register_method({ die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format); + # this only checks snapshots because $disk is passed! + my $snapshotted = PVE::QemuServer::is_volume_in_use($storecfg, $conf, $disk, $old_volid); + die "you can't move a disk with snapshots and delete the source\n" + if $snapshotted && $param->{delete}; + PVE::Cluster::log_msg('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid"); my $running = PVE::QemuServer::check_running($vmid); @@ -2448,14 +2619,17 @@ __PACKAGE__->register_method({ eval { local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; }; + warn "moving disk with snapshots, snapshots will not be moved!\n" + if $snapshotted; + 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::QemuConfig->add_unused_volume($conf, $old_volid) if !$param->{delete}; - PVE::QemuServer::update_config_nolock($vmid, $conf, 1); + PVE::QemuConfig->write_config($vmid, $conf); eval { # try to deactivate volumes - avoid lvm LVs to be active on several nodes @@ -2474,21 +2648,18 @@ __PACKAGE__->register_method({ } if ($param->{delete}) { - if (PVE::QemuServer::is_volume_in_use($storecfg, $conf, undef, $old_volid)) { - warn "volume $old_volid still has 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 $@; - } + eval { + PVE::Storage::deactivate_volumes($storecfg, [$old_volid]); + PVE::Storage::vdisk_free($storecfg, $old_volid); + }; + warn $@ if $@; } }; return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd); }; - return PVE::QemuServer::lock_config($vmid, $updatefn); + return PVE::QemuConfig->lock_config($vmid, $updatefn); }}); __PACKAGE__->register_method({ @@ -2520,6 +2691,17 @@ __PACKAGE__->register_method({ description => "Allow to migrate VMs which use local devices. Only root may use this option.", optional => 1, }, + migration_type => { + type => 'string', + enum => ['secure', 'insecure'], + description => "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.", + optional => 1, + }, + migration_network => { + type => 'string', format => 'CIDR', + description => "CIDR of the (sub) network that is used for migration.", + optional => 1, + }, }, }, returns => { @@ -2549,12 +2731,19 @@ __PACKAGE__->register_method({ raise_param_exc({ force => "Only root may use this option." }) if $param->{force} && $authuser ne 'root@pam'; + raise_param_exc({ migration_type => "Only root may use this option." }) + if $param->{migration_type} && $authuser ne 'root@pam'; + + # allow root only until better network permissions are available + raise_param_exc({ migration_network => "Only root may use this option." }) + if $param->{migration_network} && $authuser ne 'root@pam'; + # test if VM exists - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuConfig->load_config($vmid); # try to detect errors early - PVE::QemuServer::check_lock($conf); + PVE::QemuConfig->check_lock($conf); if (PVE::QemuServer::check_running($vmid)) { die "cant migrate running VM without --online\n" @@ -2603,7 +2792,8 @@ __PACKAGE__->register_method({ proxyto => 'node', description => "Execute Qemu monitor commands.", permissions => { - check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]], + description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')", + check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]], }, parameters => { additionalProperties => 0, @@ -2620,9 +2810,21 @@ __PACKAGE__->register_method({ code => sub { my ($param) = @_; + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + my $is_ro = sub { + my $command = shift; + return $command =~ m/^\s*info(\s+|$)/ + || $command =~ m/^\s*help\s*$/; + }; + + $rpcenv->check_full($authuser, "/", ['Sys.Modify']) + if !&$is_ro($param->{command}); + my $vmid = $param->{vmid}; - my $conf = PVE::QemuServer::load_config ($vmid); # check if VM exists + my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists my $res = ''; eval { @@ -2633,6 +2835,72 @@ __PACKAGE__->register_method({ return $res; }}); +my $guest_agent_commands = [ + 'guest-ping', + 'guest-get-time', + 'guest-info', + 'guest-fsfreeze-status', + 'guest-fsfreeze-freeze', + 'guest-fsfreeze-thaw', + 'guest-fstrim', + 'guest-network-get-interfaces', + 'guest-get-vcpus', + 'guest-get-fsinfo', + 'guest-get-memory-blocks', + 'guest-get-memory-block-info', + 'guest-suspend-hybrid', + 'guest-suspend-ram', + 'guest-suspend-disk', + 'guest-shutdown', + ]; + +__PACKAGE__->register_method({ + name => 'agent', + path => '{vmid}/agent', + method => 'POST', + protected => 1, + proxyto => 'node', + description => "Execute Qemu Guest Agent commands.", + permissions => { + check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]], + }, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid', { + completion => \&PVE::QemuServer::complete_vmid_running }), + command => { + type => 'string', + description => "The QGA command.", + enum => $guest_agent_commands, + }, + }, + }, + returns => { type => 'object' }, + code => sub { + my ($param) = @_; + + my $vmid = $param->{vmid}; + + my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists + + die "Only qga commands are allowed\n" if $param->{command} !~ m/^guest-.*$/; + die "No Qemu Guest Agent\n" if !defined($conf->{agent}); + die "VM $vmid is not running\n" if !PVE::QemuServer::check_running($vmid); + + my $res = ''; + eval { + $res = PVE::QemuServer::vm_mon_cmd($vmid, $param->{command}); + }; + + if (my $err = $@) { + return {'ERROR:', $err}; + } else { + return {'OK:', $res}; + } + }}); + __PACKAGE__->register_method({ name => 'resize_vm', path => '{vmid}/resize', @@ -2652,7 +2920,7 @@ __PACKAGE__->register_method({ disk => { type => 'string', description => "The disk you want to resize.", - enum => [PVE::QemuServer::disknames()], + enum => [PVE::QemuServer::valid_drive_names()], }, size => { type => 'string', @@ -2693,11 +2961,11 @@ __PACKAGE__->register_method({ my $updatefn = sub { - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuConfig->load_config($vmid); die "checksum missmatch (file change by other user?)\n" if $digest && $digest ne $conf->{digest}; - PVE::QemuServer::check_lock($conf) if !$skiplock; + PVE::QemuConfig->check_lock($conf) if !$skiplock; die "disk '$disk' does not exist\n" if !$conf->{$disk}; @@ -2719,6 +2987,7 @@ __PACKAGE__->register_method({ $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']); + PVE::Storage::activate_volumes($storecfg, [$volid]); my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5); die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/; @@ -2748,10 +3017,10 @@ __PACKAGE__->register_method({ $drive->{size} = $newsize; $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $drive); - PVE::QemuServer::update_config_nolock($vmid, $conf, 1); + PVE::QemuConfig->write_config($vmid, $conf); }; - PVE::QemuServer::lock_config($vmid, $updatefn); + PVE::QemuConfig->lock_config($vmid, $updatefn); return undef; }}); @@ -2768,7 +3037,7 @@ __PACKAGE__->register_method({ parameters => { additionalProperties => 0, properties => { - vmid => get_standard_option('pve-vmid'), + vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }), node => get_standard_option('pve-node'), }, }, @@ -2785,7 +3054,7 @@ __PACKAGE__->register_method({ my $vmid = $param->{vmid}; - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuConfig->load_config($vmid); my $snaphash = $conf->{snapshots} || {}; my $res = []; @@ -2862,7 +3131,7 @@ __PACKAGE__->register_method({ my $realcmd = sub { PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname"); - PVE::QemuServer::snapshot_create($vmid, $snapname, $param->{vmstate}, + PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate}, $param->{description}); }; @@ -2943,9 +3212,9 @@ __PACKAGE__->register_method({ my $updatefn = sub { - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuConfig->load_config($vmid); - PVE::QemuServer::check_lock($conf); + PVE::QemuConfig->check_lock($conf); my $snap = $conf->{snapshots}->{$snapname}; @@ -2953,10 +3222,10 @@ __PACKAGE__->register_method({ $snap->{description} = $param->{description} if defined($param->{description}); - PVE::QemuServer::update_config_nolock($vmid, $conf, 1); + PVE::QemuConfig->write_config($vmid, $conf); }; - PVE::QemuServer::lock_config($vmid, $updatefn); + PVE::QemuConfig->lock_config($vmid, $updatefn); return undef; }}); @@ -2990,7 +3259,7 @@ __PACKAGE__->register_method({ my $snapname = extract_param($param, 'snapname'); - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuConfig->load_config($vmid); my $snap = $conf->{snapshots}->{$snapname}; @@ -3036,7 +3305,7 @@ __PACKAGE__->register_method({ my $realcmd = sub { PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname"); - PVE::QemuServer::snapshot_rollback($vmid, $snapname); + PVE::QemuConfig->snapshot_rollback($vmid, $snapname); }; return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $realcmd); @@ -3084,7 +3353,7 @@ __PACKAGE__->register_method({ my $realcmd = sub { PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname"); - PVE::QemuServer::snapshot_delete($vmid, $snapname, $param->{force}); + PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force}); }; return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd); @@ -3110,7 +3379,7 @@ __PACKAGE__->register_method({ optional => 1, type => 'string', description => "If you want to convert only 1 disk to base image.", - enum => [PVE::QemuServer::disknames()], + enum => [PVE::QemuServer::valid_drive_names()], }, }, @@ -3131,15 +3400,15 @@ __PACKAGE__->register_method({ my $updatefn = sub { - my $conf = PVE::QemuServer::load_config($vmid); + my $conf = PVE::QemuConfig->load_config($vmid); - PVE::QemuServer::check_lock($conf); + PVE::QemuConfig->check_lock($conf); die "unable to create template, because VM contains snapshots\n" if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}}); die "you can't convert a template to a template\n" - if PVE::QemuServer::is_template($conf) && !$disk; + if PVE::QemuConfig->is_template($conf) && !$disk; die "you can't convert a VM to template if VM is running\n" if PVE::QemuServer::check_running($vmid); @@ -3149,12 +3418,12 @@ __PACKAGE__->register_method({ }; $conf->{template} = 1; - PVE::QemuServer::update_config_nolock($vmid, $conf, 1); + PVE::QemuConfig->write_config($vmid, $conf); return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd); }; - PVE::QemuServer::lock_config($vmid, $updatefn); + PVE::QemuConfig->lock_config($vmid, $updatefn); return undef; }});