X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FAPI2%2FQemu.pm;h=1aed9875c7722c4942a9970f525989660872ade7;hb=a4262553deb9a0105280daebd978daf16b0e98c0;hp=fb8a6e3cd30824bf8ce6e8b0a9b531c1a03b5b3f;hpb=68e46b84527e9cce8993ebff53ccde3988b88b28;p=qemu-server.git diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm index fb8a6e3..1aed987 100644 --- a/PVE/API2/Qemu.pm +++ b/PVE/API2/Qemu.pm @@ -130,7 +130,7 @@ my $check_storage_access_clone = sub { # Note: $pool is only needed when creating a VM, because pool permissions # are automatically inherited if VM already exists inside a pool. my $create_disks = sub { - my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $settings, $default_storage) = @_; + my ($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $settings, $default_storage) = @_; my $vollist = []; @@ -149,17 +149,18 @@ my $create_disks = sub { die "no storage ID specified (and no default storage)\n" if !$storeid; my $scfg = PVE::Storage::storage_config($storecfg, $storeid); my $name = "vm-$vmid-cloudinit"; + my $fmt = undef; if ($scfg->{path}) { - $name .= ".qcow2"; - $fmt = 'qcow2'; - }else{ - $fmt = 'raw'; + $fmt = $disk->{format} // "qcow2"; + $name .= ".$fmt"; + } else { + $fmt = $disk->{format} // "raw"; } - # FIXME: Reasonable size? qcow2 shouldn't grow if the space isn't used anyway? - my $cloudinit_iso_size = 5; # in MB - my $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, - $fmt, $name, $cloudinit_iso_size*1024); + + # Initial disk created with 4 MB and aligned to 4MB on regeneration + my $ci_size = PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE; + my $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, $name, $ci_size/1024); $disk->{file} = $volid; $disk->{media} = 'cdrom'; push @$vollist, $volid; @@ -175,7 +176,7 @@ my $create_disks = sub { my $volid; if ($ds eq 'efidisk0') { - ($volid, $size) = PVE::QemuServer::create_efidisk($storecfg, $storeid, $vmid, $fmt); + ($volid, $size) = PVE::QemuServer::create_efidisk($storecfg, $storeid, $vmid, $fmt, $arch); } else { $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, undef, $size); } @@ -291,16 +292,28 @@ my $diskoptions = { 'vmstatestorage' => 1, }; +my $cloudinitoptions = { + cicustom => 1, + cipassword => 1, + citype => 1, + ciuser => 1, + nameserver => 1, + searchdomain => 1, + sshkeys => 1, +}; + my $check_vm_modify_config_perm = sub { my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_; return 1 if $authuser eq 'root@pam'; foreach my $opt (@$key_list) { - # disk checks need to be done somewhere else + # some checks (e.g., disk, serial port, usb) need to be done somewhere + # else, as there the permission can be value dependend next if PVE::QemuServer::is_valid_drivename($opt); next if $opt eq 'cdrom'; - next if $opt =~ m/^unused\d+$/; + next if $opt =~ m/^(?:unused|serial|usb)\d+$/; + if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) { $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']); @@ -318,10 +331,10 @@ my $check_vm_modify_config_perm = sub { $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|ipconfig)\d+$/) { + } elsif ($cloudinitoptions->{$opt} || ($opt =~ m/^(?:net|ipconfig)\d+$/)) { $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']); } else { - # catches usb\d+, hostpci\d+, args, lock, etc. + # catches hostpci\d+, args, lock, etc. # new options will be checked here die "only root can set '$opt' config\n"; } @@ -356,7 +369,7 @@ __PACKAGE__->register_method({ type => 'array', items => { type => "object", - properties => {}, + properties => $PVE::QemuServer::vmstatus_return_properties, }, links => [ { rel => 'child', href => "{vmid}" } ], }, @@ -373,7 +386,6 @@ __PACKAGE__->register_method({ next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1); my $data = $vmstatus->{$vmid}; - $data->{vmid} = int($vmid); push @$res, $data; } @@ -431,11 +443,18 @@ __PACKAGE__->register_method({ description => "Add the VM to the specified pool.", }, bwlimit => { - description => "Override i/o bandwidth limit (in KiB/s).", + description => "Override I/O bandwidth limit (in KiB/s).", optional => 1, type => 'integer', minimum => '0', - } + default => 'restore limit from datacenter or storage config', + }, + start => { + optional => 1, + type => 'boolean', + default => 0, + description => "Start VM after it was created successfully.", + }, }), }, returns => { @@ -453,6 +472,7 @@ __PACKAGE__->register_method({ my $vmid = extract_param($param, 'vmid'); my $archive = extract_param($param, 'archive'); + my $is_restore = !!$archive; my $storage = extract_param($param, 'storage'); @@ -464,6 +484,8 @@ __PACKAGE__->register_method({ my $bwlimit = extract_param($param, 'bwlimit'); + my $start_after_create = extract_param($param, 'start'); + my $filename = PVE::QemuConfig->config_file($vmid); my $storecfg = PVE::Storage::config(); @@ -524,50 +546,48 @@ __PACKAGE__->register_method({ } } - my $restorefn = sub { - my $vmlist = PVE::Cluster::get_vmlist(); - if ($vmlist->{ids}->{$vmid}) { - my $current_node = $vmlist->{ids}->{$vmid}->{node}; - if ($current_node eq $node) { - my $conf = PVE::QemuConfig->load_config($vmid); + my $emsg = $is_restore ? "unable to restore VM $vmid -" : "unable to create VM $vmid -"; - PVE::QemuConfig->check_protection($conf, "unable to restore VM $vmid"); + eval { PVE::QemuConfig->create_and_lock_config($vmid, $force) }; + die "$emsg $@" if $@; - 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); + my $restorefn = sub { + my $conf = PVE::QemuConfig->load_config($vmid); - die "unable to restore vm $vmid - vm is a template\n" - if PVE::QemuConfig->is_template($conf); + PVE::QemuConfig->check_protection($conf, $emsg); - } else { - die "unable to restore vm $vmid - already existing on cluster node '$current_node'\n"; - } - } + die "$emsg vm is running\n" if PVE::QemuServer::check_running($vmid); my $realcmd = sub { PVE::QemuServer::restore_archive($archive, $vmid, $authuser, { storage => $storage, pool => $pool, unique => $unique, - bwlimit => $bwlimit, }); + bwlimit => $bwlimit, + }); + my $restored_conf = PVE::QemuConfig->load_config($vmid); + # Convert restored VM to template if backup was VM template + if (PVE::QemuConfig->is_template($restored_conf)) { + warn "Convert to template.\n"; + eval { PVE::QemuServer::template_create($vmid, $restored_conf) }; + warn $@ if $@; + } PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool; + + if ($start_after_create) { + eval { PVE::API2::Qemu->vm_start({ vmid => $vmid, node => $node }) }; + warn $@ if $@; + } }; # ensure no old replication state are exists PVE::ReplicationState::delete_guest_states($vmid); - return $rpcenv->fork_worker('qmrestore', $vmid, $authuser, $realcmd); + return PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd); }; my $createfn = sub { - - # test after locking - PVE::Cluster::check_vmid_unused($vmid); - # ensure no old replication state are exists PVE::ReplicationState::delete_guest_states($vmid); @@ -577,9 +597,11 @@ __PACKAGE__->register_method({ my $conf = $param; + my ($arch, undef) = PVE::QemuServer::get_basic_machine_info($conf); + eval { - $vollist = &$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $param, $storage); + $vollist = &$create_disks($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $param, $storage); if (!$conf->{bootdisk}) { my $firstdisk = PVE::QemuServer::resolve_first_disk($conf); @@ -591,6 +613,10 @@ __PACKAGE__->register_method({ $conf->{smbios1} = PVE::QemuServer::generate_smbios1_uuid(); } + if ((!defined($conf->{vmgenid}) || $conf->{vmgenid} eq '1') && $arch ne 'aarch64') { + $conf->{vmgenid} = PVE::QemuServer::generate_uuid(); + } + PVE::QemuConfig->write_config($vmid, $conf); }; @@ -601,16 +627,48 @@ __PACKAGE__->register_method({ eval { PVE::Storage::vdisk_free($storecfg, $volid); }; warn $@ if $@; } - die "create failed - $err"; + die "$emsg $err"; } PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool; }; - return $rpcenv->fork_worker('qmcreate', $vmid, $authuser, $realcmd); + PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd); + + if ($start_after_create) { + print "Execute autostart\n"; + eval { PVE::API2::Qemu->vm_start({vmid => $vmid, node => $node}) }; + warn $@ if $@; + } }; - return PVE::QemuConfig->lock_config_full($vmid, 1, $archive ? $restorefn : $createfn); + my ($code, $worker_name); + if ($is_restore) { + $worker_name = 'qmrestore'; + $code = sub { + eval { $restorefn->() }; + if (my $err = $@) { + eval { PVE::QemuConfig->remove_lock($vmid, 'create') }; + warn $@ if $@; + die $err; + } + }; + } else { + $worker_name = 'qmcreate'; + $code = sub { + eval { $createfn->() }; + if (my $err = $@) { + eval { + my $conffile = PVE::QemuConfig->config_file($vmid); + unlink($conffile) or die "failed to remove config file: $!\n"; + }; + warn $@ if $@; + die $err; + } + }; + } + + return $rpcenv->fork_worker($worker_name, $vmid, $authuser, $code); }}); __PACKAGE__->register_method({ @@ -777,28 +835,46 @@ __PACKAGE__->register_method({ properties => { node => get_standard_option('pve-node'), vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }), - current => { - description => "Get current values (instead of pending values).", - optional => 1, + current => { + description => "Get current values (instead of pending values).", + optional => 1, default => 0, type => 'boolean', - }, + }, + snapshot => get_standard_option('pve-snapshot-name', { + description => "Fetch config values from given snapshot.", + optional => 1, + completion => sub { + my ($cmd, $pname, $cur, $args) = @_; + PVE::QemuConfig->snapshot_list($args->[0]); + }, + }), }, }, returns => { + description => "The current VM configuration.", type => "object", - properties => { + properties => PVE::QemuServer::json_config_properties({ digest => { type => 'string', description => 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.', } - }, + }), }, code => sub { my ($param) = @_; my $conf = PVE::QemuConfig->load_config($param->{vmid}); + if (my $snapname = $param->{snapshot}) { + my $snapshot = $conf->{snapshots}->{$snapname}; + die "snapshot '$snapname' does not exist\n" if !defined($snapshot); + + $snapshot->{digest} = $conf->{digest}; # keep file digest for API + + $conf = $snapshot; + } + delete $conf->{snapshots}; if (!$param->{current}) { @@ -1049,6 +1125,13 @@ my $update_vm_api = sub { # add macaddr my $net = PVE::QemuServer::parse_net($param->{$opt}); $param->{$opt} = PVE::QemuServer::print_net($net); + } elsif ($opt eq 'vmgenid') { + if ($param->{$opt} eq '1') { + $param->{$opt} = PVE::QemuServer::generate_uuid(); + } + } elsif ($opt eq 'hookscript') { + eval { PVE::GuestHelpers::check_hookscript($param->{$opt}, $storecfg); }; + raise_param_exc({ $opt => $@ }) if $@; } } @@ -1117,6 +1200,22 @@ my $update_vm_api = sub { if defined($conf->{pending}->{$opt}); PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force); PVE::QemuConfig->write_config($vmid, $conf); + } elsif ($opt =~ m/^serial\d+$/) { + if ($conf->{$opt} eq 'socket') { + $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']); + } elsif ($authuser ne 'root@pam') { + die "only root can delete '$opt' config for real devices\n"; + } + PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force); + PVE::QemuConfig->write_config($vmid, $conf); + } elsif ($opt =~ m/^usb\d+$/) { + if ($conf->{$opt} =~ m/spice/) { + $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']); + } elsif ($authuser ne 'root@pam') { + die "only root can delete '$opt' config for real devices\n"; + } + PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force); + PVE::QemuConfig->write_config($vmid, $conf); } else { PVE::QemuServer::vmconfig_delete_pending_option($conf, $opt, $force); PVE::QemuConfig->write_config($vmid, $conf); @@ -1128,6 +1227,8 @@ my $update_vm_api = sub { $conf = PVE::QemuConfig->load_config($vmid); # update/reload next if defined($conf->{pending}->{$opt}) && ($param->{$opt} eq $conf->{pending}->{$opt}); # skip if nothing changed + my ($arch, undef) = PVE::QemuServer::get_basic_machine_info($conf); + if (PVE::QemuServer::is_valid_drivename($opt)) { my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}); # FIXME: cloudinit: CDROM or Disk? @@ -1139,7 +1240,21 @@ my $update_vm_api = sub { PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt})) if defined($conf->{pending}->{$opt}); - &$create_disks($rpcenv, $authuser, $conf->{pending}, $storecfg, $vmid, undef, {$opt => $param->{$opt}}); + &$create_disks($rpcenv, $authuser, $conf->{pending}, $arch, $storecfg, $vmid, undef, {$opt => $param->{$opt}}); + } elsif ($opt =~ m/^serial\d+/) { + if ((!defined($conf->{$opt}) || $conf->{$opt} eq 'socket') && $param->{$opt} eq 'socket') { + $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']); + } elsif ($authuser ne 'root@pam') { + die "only root can modify '$opt' config for real devices\n"; + } + $conf->{pending}->{$opt} = $param->{$opt}; + } elsif ($opt =~ m/^usb\d+/) { + if ((!defined($conf->{$opt}) || $conf->{$opt} =~ m/spice/) && $param->{$opt} =~ m/spice/) { + $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']); + } elsif ($authuser ne 'root@pam') { + die "only root can modify '$opt' config for real devices\n"; + } + $conf->{pending}->{$opt} = $param->{$opt}; } else { $conf->{pending}->{$opt} = $param->{$opt}; } @@ -1471,6 +1586,7 @@ __PACKAGE__->register_method({ my $websocket = $param->{websocket}; my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists + my $use_serial = ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/)); my $authpath = "/vms/$vmid"; @@ -1479,13 +1595,14 @@ __PACKAGE__->register_method({ $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192) if !$sslcert; - my ($remip, $family); + my $family; my $remcmd = []; if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) { - ($remip, $family) = PVE::Cluster::remote_node_ip($node); + (undef, $family) = PVE::Cluster::remote_node_ip($node); + my $sshinfo = PVE::Cluster::get_ssh_info($node); # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure - $remcmd = ['/usr/bin/ssh', '-e', 'none', '-T', '-o', 'BatchMode=yes', $remip]; + $remcmd = PVE::Cluster::ssh_info_to_command($sshinfo, $use_serial ? '-t' : '-T'); } else { $family = PVE::Tools::get_host_address_family($node); } @@ -1501,8 +1618,7 @@ __PACKAGE__->register_method({ my $cmd; - if ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/)) { - + if ($use_serial) { my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga}, '-escape', '0' ]; @@ -1619,19 +1735,20 @@ __PACKAGE__->register_method({ my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath); - my ($remip, $family); + my $family; + my $remcmd = []; if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) { - ($remip, $family) = PVE::Cluster::remote_node_ip($node); + (undef, $family) = PVE::Cluster::remote_node_ip($node); + my $sshinfo = PVE::Cluster::get_ssh_info($node); + $remcmd = PVE::Cluster::ssh_info_to_command($sshinfo, '-t'); + push @$remcmd, '--'; } else { $family = PVE::Tools::get_host_address_family($node); } my $port = PVE::Tools::next_vnc_port($family); - my $remcmd = $remip ? - ['/usr/bin/ssh', '-e', 'none', '-t', $remip, '--'] : []; - my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0']; push @$termcmd, '-iface', $serial if $serial; @@ -1820,7 +1937,26 @@ __PACKAGE__->register_method({ vmid => get_standard_option('pve-vmid'), }, }, - returns => { type => 'object' }, + returns => { + type => 'object', + properties => { + %$PVE::QemuServer::vmstatus_return_properties, + ha => { + description => "HA manager service status.", + type => 'object', + }, + spice => { + description => "Qemu VGA configuration supports spice.", + type => 'boolean', + optional => 1, + }, + agent => { + description => "Qemu GuestAgent enabled in config.", + type => 'boolean', + optional => 1, + }, + }, + }, code => sub { my ($param) = @_; @@ -1833,8 +1969,7 @@ __PACKAGE__->register_method({ $status->{ha} = PVE::HA::Config::get_service_status("vm:$param->{vmid}"); $status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga}); - - $status->{agent} = 1 if $conf->{agent}; + $status->{agent} = 1 if (PVE::QemuServer::parse_guest_agent($conf)->{enabled}); return $status; }}); @@ -1886,11 +2021,9 @@ __PACKAGE__->register_method({ 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 $machine = extract_param($param, 'machine'); @@ -1935,20 +2068,14 @@ __PACKAGE__->register_method({ my $storecfg = PVE::Storage::config(); - if (PVE::HA::Config::vm_is_ha_managed($vmid) && !$stateuri && - $rpcenv->{type} ne 'ha') { - + if (PVE::HA::Config::vm_is_ha_managed($vmid) && !$stateuri && $rpcenv->{type} ne 'ha') { my $hacmd = sub { my $upid = shift; - my $service = "vm:$vmid"; - - my $cmd = ['ha-manager', 'set', $service, '--state', 'started']; - print "Requesting HA start for VM $vmid\n"; + my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'started']; PVE::Tools::run_command($cmd); - return; }; @@ -1963,7 +2090,6 @@ __PACKAGE__->register_method({ PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef, $machine, $spice_ticket, $migration_network, $migration_type, $targetstorage); - return; }; @@ -2011,11 +2137,9 @@ __PACKAGE__->register_method({ 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 $skiplock = extract_param($param, 'skiplock'); @@ -2038,14 +2162,10 @@ __PACKAGE__->register_method({ my $hacmd = sub { my $upid = shift; - my $service = "vm:$vmid"; - - my $cmd = ['ha-manager', 'set', $service, '--state', 'stopped']; - print "Requesting HA stop for VM $vmid\n"; + my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'stopped']; PVE::Tools::run_command($cmd); - return; }; @@ -2059,7 +2179,6 @@ __PACKAGE__->register_method({ PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout}, 0, 1, $keepActive, $migratedfrom); - return; }; @@ -2162,11 +2281,9 @@ __PACKAGE__->register_method({ 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 $skiplock = extract_param($param, 'skiplock'); @@ -2187,9 +2304,8 @@ __PACKAGE__->register_method({ # # 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 $qmpstatus = eval { + PVE::QemuServer::vm_qmp_command($vmid, { execute => "query-status" }, 0); }; my $err = $@ if $@; @@ -2202,20 +2318,15 @@ __PACKAGE__->register_method({ } } - if (PVE::HA::Config::vm_is_ha_managed($vmid) && - ($rpcenv->{type} ne 'ha')) { + 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 "Requesting HA stop for VM $vmid\n"; + my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'stopped']; PVE::Tools::run_command($cmd); - return; }; @@ -2230,7 +2341,6 @@ __PACKAGE__->register_method({ PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout}, $shutdown, $param->{forceStop}, $keepActive); - return; }; @@ -2255,6 +2365,18 @@ __PACKAGE__->register_method({ vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_running }), skiplock => get_standard_option('skiplock'), + todisk => { + type => 'boolean', + default => 0, + optional => 1, + description => 'If set, suspends the VM to disk. Will be resumed on next VM start.', + }, + statestorage => get_standard_option('pve-storage-id', { + description => "The storage for the VM state", + requires => 'todisk', + optional => 1, + completion => \&PVE::Storage::complete_storage_enabled, + }), }, }, returns => { @@ -2264,30 +2386,36 @@ __PACKAGE__->register_method({ 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 $todisk = extract_param($param, 'todisk') // 0; + + my $statestorage = extract_param($param, 'statestorage'); + my $skiplock = extract_param($param, 'skiplock'); raise_param_exc({ skiplock => "Only root may use this option." }) if $skiplock && $authuser ne 'root@pam'; die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid); + die "Cannot suspend HA managed VM to disk\n" + if $todisk && PVE::HA::Config::vm_is_ha_managed($vmid); + my $realcmd = sub { my $upid = shift; syslog('info', "suspend VM $vmid: $upid\n"); - PVE::QemuServer::vm_suspend($vmid, $skiplock); + PVE::QemuServer::vm_suspend($vmid, $skiplock, $todisk, $statestorage); return; }; - return $rpcenv->fork_worker('qmsuspend', $vmid, $authuser, $realcmd); + my $taskname = $todisk ? 'qmsuspend' : 'qmpause'; + return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd); }}); __PACKAGE__->register_method({ @@ -2331,14 +2459,28 @@ __PACKAGE__->register_method({ my $nocheck = extract_param($param, 'nocheck'); - die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid, $nocheck); + my $to_disk_suspended; + eval { + PVE::QemuConfig->lock_config($vmid, sub { + my $conf = PVE::QemuConfig->load_config($vmid); + $to_disk_suspended = PVE::QemuConfig->has_lock($conf, 'suspended'); + }); + }; + + die "VM $vmid not running\n" + if !$to_disk_suspended && !PVE::QemuServer::check_running($vmid, $nocheck); my $realcmd = sub { my $upid = shift; syslog('info', "resume VM $vmid: $upid\n"); - PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck); + if (!$to_disk_suspended) { + PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck); + } else { + my $storecfg = PVE::Storage::config(); + PVE::QemuServer::vm_start($storecfg, $vmid, undef, $skiplock); + } return; }; @@ -2522,6 +2664,13 @@ __PACKAGE__->register_method({ description => "Target node. Only allowed if the original VM is on shared storage.", optional => 1, }), + bwlimit => { + description => "Override I/O bandwidth limit (in KiB/s).", + optional => 1, + type => 'integer', + minimum => '0', + default => 'clone limit from datacenter or storage config', + }, }, }, returns => { @@ -2665,13 +2814,15 @@ __PACKAGE__->register_method({ } # auto generate a new uuid - my ($uuid, $uuid_str); - UUID::generate($uuid); - UUID::unparse($uuid, $uuid_str); my $smbios1 = PVE::QemuServer::parse_smbios1($newconf->{smbios1} || ''); - $smbios1->{uuid} = $uuid_str; + $smbios1->{uuid} = PVE::QemuServer::generate_uuid(); $newconf->{smbios1} = PVE::QemuServer::print_smbios1($smbios1); + # auto generate a new vmgenid if the option was set + if ($newconf->{vmgenid}) { + $newconf->{vmgenid} = PVE::QemuServer::generate_uuid(); + } + delete $newconf->{template}; if ($param->{name}) { @@ -2705,6 +2856,8 @@ __PACKAGE__->register_method({ PVE::Storage::activate_volumes($storecfg, $vollist, $snapname); + my $bwlimit = extract_param($param, 'bwlimit'); + my $total_jobs = scalar(keys %{$drives}); my $i = 1; @@ -2712,9 +2865,14 @@ __PACKAGE__->register_method({ my $drive = $drives->{$opt}; my $skipcomplete = ($total_jobs != $i); # finish after last drive + my $src_sid = PVE::Storage::parse_volume_id($drive->{file}); + my $storage_list = [ $src_sid ]; + push @$storage_list, $storage if defined($storage); + my $clonelimit = PVE::Storage::get_bandwidth_limit('clone', $storage_list, $bwlimit); + my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $opt, $drive, $snapname, $newid, $storage, $format, $fullclone->{$opt}, $newvollist, - $jobs, $skipcomplete, $oldconf->{agent}); + $jobs, $skipcomplete, $oldconf->{agent}, $clonelimit); $newconf->{$opt} = PVE::QemuServer::print_drive($vmid, $newdrive); @@ -2725,8 +2883,9 @@ __PACKAGE__->register_method({ delete $newconf->{lock}; # do not write pending changes - if ($newconf->{pending}) { - warn "found pending changes, discarding for clone\n"; + if (my @changes = keys %{$newconf->{pending}}) { + my $pending = join(',', @changes); + warn "found pending changes for '$pending', discarding for clone\n"; delete $newconf->{pending}; } @@ -2819,6 +2978,13 @@ __PACKAGE__->register_method({ maxLength => 40, optional => 1, }, + bwlimit => { + description => "Override I/O bandwidth limit (in KiB/s).", + optional => 1, + type => 'integer', + minimum => '0', + default => 'move limit from datacenter or storage config', + }, }, }, returns => { @@ -2896,8 +3062,11 @@ __PACKAGE__->register_method({ warn "moving disk with snapshots, snapshots will not be moved!\n" if $snapshotted; + my $bwlimit = extract_param($param, 'bwlimit'); + my $movelimit = PVE::Storage::get_bandwidth_limit('move', [$oldstoreid, $storeid], $bwlimit); + my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $disk, $drive, undef, - $vmid, $storeid, $format, 1, $newvollist); + $vmid, $storeid, $format, 1, $newvollist, undef, undef, undef, $movelimit); $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $newdrive); @@ -2909,6 +3078,10 @@ __PACKAGE__->register_method({ PVE::QemuConfig->write_config($vmid, $conf); + if ($running && PVE::QemuServer::parse_guest_agent($conf)->{fstrim_cloned_disks} && PVE::QemuServer::qga_check_running($vmid)) { + eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fstrim"); }; + } + eval { # try to deactivate volumes - avoid lvm LVs to be active on several nodes PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ]) @@ -2955,7 +3128,7 @@ __PACKAGE__->register_method({ properties => { node => get_standard_option('pve-node'), vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }), - target => get_standard_option('pve-node', { + target => get_standard_option('pve-node', { description => "Target node.", completion => \&PVE::Cluster::complete_migration_target, }), @@ -2990,6 +3163,13 @@ __PACKAGE__->register_method({ optional => 1, completion => \&PVE::QemuServer::complete_storage, }), + bwlimit => { + description => "Override I/O bandwidth limit (in KiB/s).", + optional => 1, + type => 'integer', + minimum => '0', + default => 'migrate limit from datacenter or storage config', + }, }, }, returns => { @@ -3000,7 +3180,6 @@ __PACKAGE__->register_method({ my ($param) = @_; my $rpcenv = PVE::RPCEnvironment::get(); - my $authuser = $rpcenv->get_user(); my $target = extract_param($param, 'target'); @@ -3054,14 +3233,10 @@ __PACKAGE__->register_method({ my $hacmd = sub { my $upid = shift; - my $service = "vm:$vmid"; - - my $cmd = ['ha-manager', 'migrate', $service, $target]; - print "Requesting HA migration for VM $vmid to node $target\n"; + my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target]; PVE::Tools::run_command($cmd); - return; }; @@ -3206,7 +3381,7 @@ __PACKAGE__->register_method({ my (undef, undef, undef, undef, undef, undef, $format) = PVE::Storage::parse_volname($storecfg, $drive->{file}); - die "can't resize volume: $disk if snapshot exists\n" + die "can't resize volume: $disk if snapshot exists\n" if %{$conf->{snapshots}} && $format eq 'qcow2'; my $volid = $drive->{file}; @@ -3277,7 +3452,32 @@ __PACKAGE__->register_method({ type => 'array', items => { type => "object", - properties => {}, + properties => { + name => { + description => "Snapshot identifier. Value 'current' identifies the current VM.", + type => 'string', + }, + vmstate => { + description => "Snapshot includes RAM.", + type => 'boolean', + optional => 1, + }, + description => { + description => "Snapshot description.", + type => 'string', + }, + snaptime => { + description => "Snapshot creation time", + type => 'integer', + renderer => 'timestamp', + optional => 1, + }, + parent => { + description => "Parent snapshot identifier.", + type => 'string', + optional => 1, + }, + }, }, links => [ { rel => 'child', href => "{name}" } ], }, @@ -3305,7 +3505,12 @@ __PACKAGE__->register_method({ } my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0; - my $current = { name => 'current', digest => $conf->{digest}, running => $running }; + my $current = { + name => 'current', + digest => $conf->{digest}, + running => $running, + description => "You are here!", + }; $current->{parent} = $conf->{parent} if $conf->{parent}; push @$res, $current; @@ -3363,7 +3568,7 @@ __PACKAGE__->register_method({ my $realcmd = sub { PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname"); - PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate}, + PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate}, $param->{description}); };