X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FAPI2%2FQemu.pm;h=5814f941dc4967710c762529f5268e14ab99e1c0;hb=4c1f3929eabf4a118ef7109938eb2c9088332c60;hp=54db5ab0caaf43255dcf8d73fb9262eddd31c32c;hpb=af6d2db4b3dd54dee517a3ae6a49eac43c068af9;p=qemu-server.git diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm index 54db5ab..5814f94 100644 --- a/PVE/API2/Qemu.pm +++ b/PVE/API2/Qemu.pm @@ -7,6 +7,7 @@ use Net::SSLeay; use UUID; use POSIX; use IO::Socket::IP; +use URI::Escape; use PVE::Cluster qw (cfs_read_file cfs_write_file);; use PVE::SafeSyslog; @@ -26,6 +27,7 @@ use PVE::INotify; use PVE::Network; use PVE::Firewall; use PVE::API2::Firewall::VM; +use PVE::API2::Qemu::Agent; BEGIN { if (!$ENV{PVE_GENERATING_DOCS}) { @@ -52,6 +54,7 @@ my $resolve_cdrom_alias = sub { } }; +my $NEW_DISK_RE = qr!^(([^/:\s]+):)?(\d+(\.\d+)?)$!; my $check_storage_access = sub { my ($rpcenv, $authuser, $storecfg, $vmid, $settings, $default_storage) = @_; @@ -62,18 +65,26 @@ my $check_storage_access = sub { my $volid = $drive->{file}; - if (!$volid || $volid eq 'none') { + if (!$volid || ($volid eq 'none' || $volid eq 'cloudinit')) { + # nothing to check + } elsif ($volid =~ m/^(([^:\s]+):)?(cloudinit)$/) { # nothing to check } elsif ($isCDROM && ($volid eq 'cdrom')) { $rpcenv->check($authuser, "/", ['Sys.Console']); - } elsif (!$isCDROM && ($volid =~ m/^(([^:\s]+):)?(\d+(\.\d+)?)$/)) { + } elsif (!$isCDROM && ($volid =~ $NEW_DISK_RE)) { my ($storeid, $size) = ($2 || $default_storage, $3); die "no storage ID specified (and no default storage)\n" if !$storeid; $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']); + my $scfg = PVE::Storage::storage_config($storecfg, $storeid); + raise_param_exc({ storage => "storage '$storeid' does not support vm images"}) + if !$scfg->{content}->{images}; } else { PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $volid); } }); + + $rpcenv->check($authuser, "/storage/$settings->{vmstatestorage}", ['Datastore.AllocateSpace']) + if defined($settings->{vmstatestorage}); }; my $check_storage_access_clone = sub { @@ -110,13 +121,16 @@ my $check_storage_access_clone = sub { } }); + $rpcenv->check($authuser, "/storage/$conf->{vmstatestorage}", ['Datastore.AllocateSpace']) + if defined($conf->{vmstatestorage}); + return $sharedvm; }; # 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 = []; @@ -130,41 +144,45 @@ my $create_disks = sub { if (!$volid || $volid eq 'none' || $volid eq 'cdrom') { delete $disk->{size}; $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk); - } elsif ($volid =~ m!^(([^/:\s]+):)?(\d+(\.\d+)?)$!) { + } elsif ($volid =~ m!^(?:([^/:\s]+):)?cloudinit$!) { + my $storeid = $1 || $default_storage; + 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}) { + $fmt = $disk->{format} // "qcow2"; + $name .= ".$fmt"; + } else { + $fmt = $disk->{format} // "raw"; + } + + # 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; + delete $disk->{format}; # no longer needed + $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk); + } elsif ($volid =~ $NEW_DISK_RE) { my ($storeid, $size) = ($2 || $default_storage, $3); 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; + $size = PVE::Tools::convert_size($size, 'gb' => 'kb'); # vdisk_alloc uses kb + 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; + ($volid, $size) = PVE::QemuServer::create_efidisk($storecfg, $storeid, $vmid, $fmt, $arch); } else { - $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, - $fmt, undef, $size*1024*1024); - $disk->{file} = $volid; - $disk->{size} = $size*1024*1024*1024; + $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, undef, $size); } push @$vollist, $volid; + $disk->{file} = $volid; + $disk->{size} = PVE::Tools::convert_size($size, 'kb' => 'b'); delete $disk->{format}; # no longer needed $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk); } else { @@ -242,6 +260,7 @@ my $hwtypeoptions = { 'tablet' => 1, 'vga' => 1, 'watchdog' => 1, + 'audio0' => 1, }; my $generaloptions = { @@ -271,6 +290,17 @@ my $vmpoweroptions = { my $diskoptions = { 'boot' => 1, 'bootdisk' => 1, + '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 { @@ -279,10 +309,12 @@ my $check_vm_modify_config_perm = sub { 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']); @@ -300,10 +332,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\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"; } @@ -338,7 +370,7 @@ __PACKAGE__->register_method({ type => 'array', items => { type => "object", - properties => {}, + properties => $PVE::QemuServer::vmstatus_return_properties, }, links => [ { rel => 'child', href => "{vmid}" } ], }, @@ -355,7 +387,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; } @@ -412,6 +443,19 @@ __PACKAGE__->register_method({ type => 'string', format => 'pve-poolid', description => "Add the VM to the specified pool.", }, + bwlimit => { + 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 => { @@ -429,6 +473,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'); @@ -438,10 +483,19 @@ __PACKAGE__->register_method({ my $pool = extract_param($param, 'pool'); + 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(); + if (defined(my $ssh_keys = $param->{sshkeys})) { + $ssh_keys = URI::Escape::uri_unescape($ssh_keys); + PVE::Tools::validate_ssh_public_keys($ssh_keys); + } + PVE::Cluster::check_cfs_quorum(); if (defined($pool)) { @@ -493,45 +547,50 @@ __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; + my $restorefn = sub { + my $conf = PVE::QemuConfig->load_config($vmid); - die "unable to restore vm $vmid - vm is running\n" - if PVE::QemuServer::check_running($vmid); + PVE::QemuConfig->check_protection($conf, $emsg); - 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"; - } - } + 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 }); + unique => $unique, + 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 $@; + } }; - return $rpcenv->fork_worker('qmrestore', $vmid, $authuser, $realcmd); + # ensure no old replication state are exists + PVE::ReplicationState::delete_guest_states($vmid); + + 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); my $realcmd = sub { @@ -539,30 +598,24 @@ __PACKAGE__->register_method({ my $conf = $param; - eval { + my ($arch, undef) = PVE::QemuServer::get_basic_machine_info($conf); - $vollist = &$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $param, $storage); + eval { - # try to be smart about bootdisk - my @disks = PVE::QemuServer::valid_drive_names(); - my $firstdisk; - foreach my $ds (reverse @disks) { - next if !$conf->{$ds}; - my $disk = PVE::QemuServer::parse_drive($ds, $conf->{$ds}); - next if PVE::QemuServer::drive_is_cdrom($disk); - $firstdisk = $ds; - } + $vollist = &$create_disks($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $param, $storage); - if (!$conf->{bootdisk} && $firstdisk) { - $conf->{bootdisk} = $firstdisk; + if (!$conf->{bootdisk}) { + my $firstdisk = PVE::QemuServer::resolve_first_disk($conf); + $conf->{bootdisk} = $firstdisk if $firstdisk; } # auto generate uuid if user did not specify smbios1 option if (!$conf->{smbios1}) { - my ($uuid, $uuid_str); - UUID::generate($uuid); - UUID::unparse($uuid, $uuid_str); - $conf->{smbios1} = "uuid=$uuid_str"; + $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); @@ -575,16 +628,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({ @@ -622,6 +707,7 @@ __PACKAGE__->register_method({ { subdir => 'status' }, { subdir => 'unlink' }, { subdir => 'vncproxy' }, + { subdir => 'termproxy' }, { subdir => 'migrate' }, { subdir => 'resize' }, { subdir => 'move' }, @@ -643,6 +729,11 @@ __PACKAGE__->register_method ({ path => '{vmid}/firewall', }); +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Qemu::Agent", + path => '{vmid}/agent', +}); + __PACKAGE__->register_method({ name => 'rrd', path => '{vmid}/rrd', @@ -745,28 +836,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}) { @@ -784,6 +893,11 @@ __PACKAGE__->register_method({ delete $conf->{pending}; + # hide cloudinit password + if ($conf->{cipassword}) { + $conf->{cipassword} = '**********'; + } + return $conf; }}); @@ -848,6 +962,13 @@ __PACKAGE__->register_method({ $item->{value} = $conf->{$opt} if defined($conf->{$opt}); $item->{pending} = $conf->{pending}->{$opt} if defined($conf->{pending}->{$opt}); $item->{delete} = ($pending_delete_hash->{$opt} ? 2 : 1) if exists $pending_delete_hash->{$opt}; + + # hide cloudinit password + if ($opt eq 'cipassword') { + $item->{value} = '**********' if defined($item->{value}); + # the trailing space so that the pending string is different + $item->{pending} = '********** ' if defined($item->{pending}); + } push @$res, $item; } @@ -857,6 +978,11 @@ __PACKAGE__->register_method({ next if defined($conf->{$opt}); my $item = { key => $opt }; $item->{pending} = $conf->{pending}->{$opt}; + + # hide cloudinit password + if ($opt eq 'cipassword') { + $item->{pending} = '**********' if defined($item->{pending}); + } push @$res, $item; } @@ -897,9 +1023,16 @@ my $update_vm_api = sub { my $background_delay = extract_param($param, 'background_delay'); + if (defined(my $cipassword = $param->{cipassword})) { + # Same logic as in cloud-init (but with the regex fixed...) + $param->{cipassword} = PVE::Tools::encrypt_pw($cipassword) + if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/; + } + my @paramarr = (); # used for log message - foreach my $key (keys %$param) { - push @paramarr, "-$key", $param->{$key}; + foreach my $key (sort keys %$param) { + my $value = $key eq 'cipassword' ? '' : $param->{$key}; + push @paramarr, "-$key", $value; } my $skiplock = extract_param($param, 'skiplock'); @@ -912,6 +1045,11 @@ my $update_vm_api = sub { my $force = extract_param($param, 'force'); + if (defined(my $ssh_keys = $param->{sshkeys})) { + $ssh_keys = URI::Escape::uri_unescape($ssh_keys); + PVE::Tools::validate_ssh_public_keys($ssh_keys); + } + die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param); my $storecfg = PVE::Storage::config(); @@ -954,17 +1092,47 @@ my $update_vm_api = sub { push @delete, $opt; } + my $repl_conf = PVE::ReplicationConfig->new(); + my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1); + my $check_replication = sub { + my ($drive) = @_; + return if !$is_replicated; + my $volid = $drive->{file}; + return if !$volid || !($drive->{replicate}//1); + return if PVE::QemuServer::drive_is_cdrom($drive); + my ($storeid, $format); + if ($volid =~ $NEW_DISK_RE) { + $storeid = $2; + $format = $drive->{format} || PVE::Storage::storage_default_format($storecfg, $storeid); + } else { + ($storeid, undef) = PVE::Storage::parse_volume_id($volid, 1); + $format = (PVE::Storage::parse_volname($storecfg, $volid))[6]; + } + return if PVE::Storage::storage_can_replicate($storecfg, $storeid, $format); + my $scfg = PVE::Storage::storage_config($storecfg, $storeid); + return if $scfg->{shared}; + die "cannot add non-replicatable volume to a replicated VM\n"; + }; + foreach my $opt (keys %$param) { 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); + $check_replication->($drive); $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive); } elsif ($opt =~ m/^net(\d+)$/) { # add macaddr my $net = PVE::QemuServer::parse_net($param->{$opt}); $param->{$opt} = PVE::QemuServer::print_net($net); + } 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 $@; } } @@ -1033,6 +1201,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' || (!$conf->{$opt} && $conf->{pending}->{$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/ || (!$conf->{$opt} && $conf->{pending}->{$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); @@ -1044,8 +1228,11 @@ 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? if (PVE::QemuServer::drive_is_cdrom($drive)) { # CDROM $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']); } else { @@ -1054,7 +1241,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}; } @@ -1236,7 +1437,6 @@ __PACKAGE__->register_method({ } }); - __PACKAGE__->register_method({ name => 'destroy_vm', path => '{vmid}', @@ -1262,9 +1462,7 @@ __PACKAGE__->register_method({ my ($param) = @_; my $rpcenv = PVE::RPCEnvironment::get(); - my $authuser = $rpcenv->get_user(); - my $vmid = $param->{vmid}; my $skiplock = $param->{skiplock}; @@ -1273,11 +1471,8 @@ __PACKAGE__->register_method({ # test if VM exists my $conf = PVE::QemuConfig->load_config($vmid); - my $storecfg = PVE::Storage::config(); - 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); @@ -1293,11 +1488,8 @@ __PACKAGE__->register_method({ my $upid = shift; syslog('info', "destroy VM $vmid: $upid\n"); - PVE::QemuServer::vm_destroy($storecfg, $vmid, $skiplock); - PVE::AccessControl::remove_vm_access($vmid); - PVE::Firewall::remove_vmfw_conf($vmid); }; @@ -1386,6 +1578,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"; @@ -1394,13 +1587,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', '-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); } @@ -1416,16 +1610,23 @@ __PACKAGE__->register_method({ my $cmd; - if ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/)) { + if ($use_serial) { - die "Websocket mode is not supported in vga serial mode!" if $websocket; + my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga}, '-escape', '0' ]; - 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]; + '-perm', 'Sys.Console']; + + if ($param->{websocket}) { + $ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm + push @$cmd, '-notls', '-listen', 'localhost'; + } + + push @$cmd, '-c', @$remcmd, @$termcmd; + PVE::Tools::run_command($cmd); + } else { $ENV{LC_PVE_TICKET} = $ticket if $websocket; # set ticket with "qm vncproxy" @@ -1472,6 +1673,101 @@ __PACKAGE__->register_method({ }; }}); +__PACKAGE__->register_method({ + name => 'termproxy', + path => '{vmid}/termproxy', + method => 'POST', + protected => 1, + permissions => { + check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]], + }, + description => "Creates a TCP proxy connections.", + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid'), + serial=> { + optional => 1, + type => 'string', + enum => [qw(serial0 serial1 serial2 serial3)], + description => "opens a serial terminal (defaults to display)", + }, + }, + }, + returns => { + additionalProperties => 0, + properties => { + user => { type => 'string' }, + ticket => { type => 'string' }, + port => { type => 'integer' }, + upid => { type => 'string' }, + }, + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + my $vmid = $param->{vmid}; + my $node = $param->{node}; + my $serial = $param->{serial}; + + my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists + + if (!defined($serial)) { + if ($conf->{vga} && $conf->{vga} =~ m/^serial\d+$/) { + $serial = $conf->{vga}; + } + } + + my $authpath = "/vms/$vmid"; + + my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath); + + my $family; + my $remcmd = []; + + if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) { + (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 $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0']; + push @$termcmd, '-iface', $serial if $serial; + + my $realcmd = sub { + my $upid = shift; + + syslog('info', "starting qemu termproxy $upid\n"); + + my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath, + '--perm', 'VM.Console', '--']; + push @$cmd, @$remcmd, @$termcmd; + + PVE::Tools::run_command($cmd); + }; + + my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1); + + PVE::Tools::wait_for_vnc_port($port); + + return { + user => $authuser, + ticket => $ticket, + port => $port, + upid => $upid, + }; + }}); + __PACKAGE__->register_method({ name => 'vncwebsocket', path => '{vmid}/vncwebsocket', @@ -1611,6 +1907,10 @@ __PACKAGE__->register_method({ { subdir => 'current' }, { subdir => 'start' }, { subdir => 'stop' }, + { subdir => 'reset' }, + { subdir => 'shutdown' }, + { subdir => 'suspend' }, + { subdir => 'reboot' }, ]; return $res; @@ -1633,7 +1933,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) = @_; @@ -1646,6 +1965,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 (PVE::QemuServer::parse_guest_agent($conf)->{enabled}); return $status; }}); @@ -1697,46 +2017,34 @@ __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'); - my $stateuri = extract_param($param, 'stateuri'); - raise_param_exc({ stateuri => "Only root may use this option." }) - if $stateuri && $authuser ne 'root@pam'; - - my $skiplock = extract_param($param, 'skiplock'); - raise_param_exc({ skiplock => "Only root may use this option." }) - if $skiplock && $authuser ne 'root@pam'; - - my $migratedfrom = extract_param($param, 'migratedfrom'); - 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'; + my $get_root_param = sub { + my $value = extract_param($param, $_[0]); + raise_param_exc({ "$_[0]" => "Only root may use this option." }) + if $value && $authuser ne 'root@pam'; + return $value; + }; - my $targetstorage = extract_param($param, 'targetstorage'); - raise_param_exc({ targetstorage => "Only root may use this option." }) - if $targetstorage && $authuser ne 'root@pam'; + my $stateuri = $get_root_param->('stateuri'); + my $skiplock = $get_root_param->('skiplock'); + my $migratedfrom = $get_root_param->('migratedfrom'); + my $migration_type = $get_root_param->('migration_type'); + my $migration_network = $get_root_param->('migration_network'); + my $targetstorage = $get_root_param->('targetstorage'); raise_param_exc({ targetstorage => "targetstorage can only by used with migratedfrom." }) if $targetstorage && !$migratedfrom; # read spice ticket from STDIN my $spice_ticket; - if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type} eq 'cli')) { - if (defined(my $line = <>)) { + if ($stateuri && ($stateuri eq 'tcp' || $stateuri eq 'unix') && $migratedfrom && ($rpcenv->{type} eq 'cli')) { + if (defined(my $line = )) { chomp $line; $spice_ticket = $line; } @@ -1746,20 +2054,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 "Executing HA start for VM $vmid\n"; + print "Requesting HA start for VM $vmid\n"; + my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'started']; PVE::Tools::run_command($cmd); - return; }; @@ -1774,7 +2076,6 @@ __PACKAGE__->register_method({ PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef, $machine, $spice_ticket, $migration_network, $migration_type, $targetstorage); - return; }; @@ -1822,11 +2123,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'); @@ -1849,14 +2148,10 @@ __PACKAGE__->register_method({ 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"; + print "Requesting HA stop for VM $vmid\n"; + my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'stopped']; PVE::Tools::run_command($cmd); - return; }; @@ -1870,7 +2165,6 @@ __PACKAGE__->register_method({ PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout}, 0, 1, $keepActive, $migratedfrom); - return; }; @@ -1973,11 +2267,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'); @@ -1998,9 +2290,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 $@; @@ -2013,20 +2304,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 "Executing HA stop for VM $vmid\n"; + print "Requesting HA stop for VM $vmid\n"; + my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'stopped']; PVE::Tools::run_command($cmd); - return; }; @@ -2041,7 +2327,6 @@ __PACKAGE__->register_method({ PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout}, $shutdown, $param->{forceStop}, $keepActive); - return; }; @@ -2049,6 +2334,64 @@ __PACKAGE__->register_method({ } }}); +__PACKAGE__->register_method({ + name => 'vm_reboot', + path => '{vmid}/status/reboot', + method => 'POST', + protected => 1, + proxyto => 'node', + description => "Reboot the VM by shutting it down, and starting it again. Applies pending changes.", + permissions => { + check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]], + }, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid', + { completion => \&PVE::QemuServer::complete_vmid_running }), + timeout => { + description => "Wait maximal timeout seconds for the shutdown.", + type => 'integer', + minimum => 0, + optional => 1, + }, + }, + }, + returns => { + type => 'string', + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + my $node = extract_param($param, 'node'); + my $vmid = extract_param($param, 'vmid'); + + my $qmpstatus = eval { + PVE::QemuServer::vm_qmp_command($vmid, { execute => "query-status" }, 0); + }; + my $err = $@ if $@; + + if (!$err && $qmpstatus->{status} eq "paused") { + die "VM is paused - cannot shutdown\n"; + } + + die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid); + + my $realcmd = sub { + my $upid = shift; + + syslog('info', "requesting reboot of VM $vmid: $upid\n"); + PVE::QemuServer::vm_reboot($vmid, $param->{timeout}); + return; + }; + + return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd); + }}); + __PACKAGE__->register_method({ name => 'vm_suspend', path => '{vmid}/status/suspend', @@ -2066,6 +2409,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 => { @@ -2075,30 +2430,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({ @@ -2142,14 +2503,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; }; @@ -2292,7 +2667,9 @@ __PACKAGE__->register_method({ properties => { node => get_standard_option('pve-node'), vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }), - newid => get_standard_option('pve-vmid', { description => 'VMID for the clone.' }), + newid => get_standard_option('pve-vmid', { + completion => \&PVE::Cluster::complete_next_vmid, + description => 'VMID for the clone.' }), name => { optional => 1, type => 'string', format => 'dns-name', @@ -2313,12 +2690,10 @@ __PACKAGE__->register_method({ }), storage => get_standard_option('pve-storage-id', { description => "Target storage for full clone.", - requires => 'full', optional => 1, }), 'format' => { - description => "Target format for file storage.", - requires => 'full', + description => "Target format for file storage. Only valid for full clone.", type => 'string', optional => 1, enum => [ 'raw', 'qcow2', 'vmdk'], @@ -2326,14 +2701,20 @@ __PACKAGE__->register_method({ full => { optional => 1, type => 'boolean', - description => "Create a full copy of all disk. This is always done when " . + description => "Create a full copy of all disks. This is always done when " . "you clone a normal VM. For VM templates, we try to create a linked clone by default.", - default => 0, }, target => get_standard_option('pve-node', { 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 => { @@ -2409,6 +2790,17 @@ __PACKAGE__->register_method({ die "snapshot '$snapname' does not exist\n" if $snapname && !defined( $conf->{snapshots}->{$snapname}); + my $full = extract_param($param, 'full'); + if (!defined($full)) { + $full = !PVE::QemuConfig->is_template($conf); + } + + die "parameter 'storage' not allowed for linked clones\n" + if defined($storage) && !$full; + + die "parameter 'format' not allowed for linked clones\n" + if defined($format) && !$full; + my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf; my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage); @@ -2444,10 +2836,10 @@ __PACKAGE__->register_method({ } 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)) { + if (PVE::QemuServer::drive_is_cdrom($drive, 1)) { $newconf->{$opt} = $value; # simply copy configuration } else { - if ($param->{full}) { + if ($full || PVE::QemuServer::drive_is_cloudinit($drive)) { die "Full clone feature is not supported for drive '$opt'\n" if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running); $fullclone->{$opt} = 1; @@ -2466,13 +2858,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}) { @@ -2499,10 +2893,15 @@ __PACKAGE__->register_method({ my $jobs = {}; eval { - local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; }; + local $SIG{INT} = + local $SIG{TERM} = + local $SIG{QUIT} = + local $SIG{HUP} = sub { die "interrupted by signal\n"; }; PVE::Storage::activate_volumes($storecfg, $vollist, $snapname); + my $bwlimit = extract_param($param, 'bwlimit'); + my $total_jobs = scalar(keys %{$drives}); my $i = 1; @@ -2510,9 +2909,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); @@ -2521,6 +2925,14 @@ __PACKAGE__->register_method({ } delete $newconf->{lock}; + + # do not write pending changes + if (my @changes = keys %{$newconf->{pending}}) { + my $pending = join(',', @changes); + warn "found pending changes for '$pending', discarding for clone\n"; + delete $newconf->{pending}; + } + PVE::QemuConfig->write_config($newid, $newconf); if ($target) { @@ -2610,6 +3022,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 => { @@ -2652,7 +3071,7 @@ __PACKAGE__->register_method({ my $old_volid = $drive->{file} || die "disk '$disk' has no associated volume\n"; - die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive); + die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive, 1); my $oldfmt; my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid); @@ -2679,13 +3098,19 @@ __PACKAGE__->register_method({ my $newvollist = []; eval { - local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; }; + local $SIG{INT} = + local $SIG{TERM} = + local $SIG{QUIT} = + local $SIG{HUP} = sub { die "interrupted by signal\n"; }; 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); @@ -2697,6 +3122,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} ]) @@ -2728,6 +3157,138 @@ __PACKAGE__->register_method({ return PVE::QemuConfig->lock_config($vmid, $updatefn); }}); +my $check_vm_disks_local = sub { + my ($storecfg, $vmconf, $vmid) = @_; + + my $local_disks = {}; + + # add some more information to the disks e.g. cdrom + PVE::QemuServer::foreach_volid($vmconf, sub { + my ($volid, $attr) = @_; + + my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1); + if ($storeid) { + my $scfg = PVE::Storage::storage_config($storecfg, $storeid); + return if $scfg->{shared}; + } + # The shared attr here is just a special case where the vdisk + # is marked as shared manually + return if $attr->{shared}; + return if $attr->{cdrom} and $volid eq "none"; + + if (exists $local_disks->{$volid}) { + @{$local_disks->{$volid}}{keys %$attr} = values %$attr + } else { + $local_disks->{$volid} = $attr; + # ensure volid is present in case it's needed + $local_disks->{$volid}->{volid} = $volid; + } + }); + + return $local_disks; +}; + +__PACKAGE__->register_method({ + name => 'migrate_vm_precondition', + path => '{vmid}/migrate', + method => 'GET', + protected => 1, + proxyto => 'node', + description => "Get preconditions for migration.", + permissions => { + check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]], + }, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }), + target => get_standard_option('pve-node', { + description => "Target node.", + completion => \&PVE::Cluster::complete_migration_target, + optional => 1, + }), + }, + }, + returns => { + type => "object", + properties => { + running => { type => 'boolean' }, + allowed_nodes => { + type => 'array', + optional => 1, + description => "List nodes allowed for offline migration, only passed if VM is offline" + }, + not_allowed_nodes => { + type => 'object', + optional => 1, + description => "List not allowed nodes with additional informations, only passed if VM is offline" + }, + local_disks => { + type => 'array', + description => "List local disks including CD-Rom, unsused and not referenced disks" + }, + local_resources => { + type => 'array', + description => "List local resources e.g. pci, usb" + } + }, + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + PVE::Cluster::check_cfs_quorum(); + + my $res = {}; + + my $vmid = extract_param($param, 'vmid'); + my $target = extract_param($param, 'target'); + my $localnode = PVE::INotify::nodename(); + + + # test if VM exists + my $vmconf = PVE::QemuConfig->load_config($vmid); + my $storecfg = PVE::Storage::config(); + + + # try to detect errors early + PVE::QemuConfig->check_lock($vmconf); + + $res->{running} = PVE::QemuServer::check_running($vmid) ? 1:0; + + # if vm is not running, return target nodes where local storage is available + # for offline migration + if (!$res->{running}) { + $res->{allowed_nodes} = []; + my $checked_nodes = PVE::QemuServer::check_local_storage_availability($vmconf, $storecfg); + delete $checked_nodes->{$localnode}; + + foreach my $node (keys %$checked_nodes) { + if (!defined $checked_nodes->{$node}->{unavailable_storages}) { + push @{$res->{allowed_nodes}}, $node; + } + + } + $res->{not_allowed_nodes} = $checked_nodes; + } + + + my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid); + $res->{local_disks} = [ values %$local_disks ];; + + my $local_resources = PVE::QemuServer::check_local_resources($vmconf, 1); + + $res->{local_resources} = $local_resources; + + return $res; + + + }}); + __PACKAGE__->register_method({ name => 'migrate_vm', path => '{vmid}/migrate', @@ -2743,7 +3304,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, }), @@ -2778,6 +3339,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 => { @@ -2788,7 +3356,6 @@ __PACKAGE__->register_method({ my ($param) = @_; my $rpcenv = PVE::RPCEnvironment::get(); - my $authuser = $rpcenv->get_user(); my $target = extract_param($param, 'target'); @@ -2842,14 +3409,10 @@ __PACKAGE__->register_method({ my $hacmd = sub { my $upid = shift; - my $service = "vm:$vmid"; - - my $cmd = ['ha-manager', 'migrate', $service, $target]; - - print "Executing HA migrate for VM $vmid to node $target\n"; + 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; }; @@ -2857,17 +3420,15 @@ __PACKAGE__->register_method({ } else { - my $code = sub { - my $realcmd = sub { - my $upid = shift; - - PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param); - }; + my $realcmd = sub { + PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param); + }; - return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $realcmd); + my $worker = sub { + return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd); }; - return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $code); + return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker); } }}); @@ -2923,70 +3484,6 @@ __PACKAGE__->register_method({ return $res; }}); -my $guest_agent_commands = [ - 'ping', - 'get-time', - 'info', - 'fsfreeze-status', - 'fsfreeze-freeze', - 'fsfreeze-thaw', - 'fstrim', - 'network-get-interfaces', - 'get-vcpus', - 'get-fsinfo', - 'get-memory-blocks', - 'get-memory-block-info', - 'suspend-hybrid', - 'suspend-ram', - 'suspend-disk', - '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', - description => "Returns an object with a single `result` property. The type of that -property depends on the executed command.", - }, - code => sub { - my ($param) = @_; - - my $vmid = $param->{vmid}; - - my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists - - die "No Qemu Guest Agent\n" if !defined($conf->{agent}); - die "VM $vmid is not running\n" if !PVE::QemuServer::check_running($vmid); - - my $cmd = $param->{command}; - - my $res = PVE::QemuServer::vm_mon_cmd($vmid, "guest-$cmd"); - - return { result => $res }; - }}); - __PACKAGE__->register_method({ name => 'resize_vm', path => '{vmid}/resize', @@ -3060,7 +3557,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}; @@ -3131,7 +3628,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}" } ], }, @@ -3159,7 +3681,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; @@ -3217,7 +3744,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}); }; @@ -3323,7 +3850,7 @@ __PACKAGE__->register_method({ proxyto => 'node', description => "Get snapshot configuration", permissions => { - check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]], + check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1], }, parameters => { additionalProperties => 0, @@ -3362,7 +3889,7 @@ __PACKAGE__->register_method({ proxyto => 'node', description => "Rollback VM state to specified snapshot.", permissions => { - check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]], + check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1], }, parameters => { additionalProperties => 0, @@ -3394,7 +3921,12 @@ __PACKAGE__->register_method({ PVE::QemuConfig->snapshot_rollback($vmid, $snapname); }; - return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $realcmd); + my $worker = sub { + # hold migration lock, this makes sure that nobody create replication snapshots + return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd); + }; + + return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker); }}); __PACKAGE__->register_method({ @@ -3513,4 +4045,36 @@ __PACKAGE__->register_method({ return undef; }}); +__PACKAGE__->register_method({ + name => 'cloudinit_generated_config_dump', + path => '{vmid}/cloudinit/dump', + method => 'GET', + proxyto => 'node', + description => "Get automatically generated cloudinit config.", + permissions => { + check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]], + }, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }), + type => { + description => 'Config type.', + type => 'string', + enum => ['user', 'network', 'meta'], + }, + }, + }, + returns => { + type => 'string', + }, + code => sub { + my ($param) = @_; + + my $conf = PVE::QemuConfig->load_config($param->{vmid}); + + return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, $param->{vmid}, $param->{type}); + }}); + 1;