]> git.proxmox.com Git - qemu-server.git/blame - PVE/API2/Qemu.pm
api: fix using import-from with SCSI disks
[qemu-server.git] / PVE / API2 / Qemu.pm
CommitLineData
1e3baf05
DM
1package PVE::API2::Qemu;
2
3use strict;
4use warnings;
5b9d692a 5use Cwd 'abs_path';
451b2b81 6use Net::SSLeay;
655d7462 7use IO::Socket::IP;
347dc136
FG
8use IO::Socket::UNIX;
9use IPC::Open3;
10use JSON;
0c9a7596 11use URI::Escape;
3c5bdde8 12use Crypt::OpenSSL::Random;
347dc136 13use Socket qw(SOCK_STREAM);
1e3baf05 14
06fedff6 15use PVE::APIClient::LWP;
8481721f 16use PVE::CGroup;
502d18a2 17use PVE::Cluster qw (cfs_read_file cfs_write_file);;
95896f80 18use PVE::RRD;
1e3baf05
DM
19use PVE::SafeSyslog;
20use PVE::Tools qw(extract_param);
f9bfceef 21use PVE::Exception qw(raise raise_param_exc raise_perm_exc);
1e3baf05
DM
22use PVE::Storage;
23use PVE::JSONSchema qw(get_standard_option);
24use PVE::RESTHandler;
628bb7f2 25use PVE::ReplicationConfig;
d6deb7f6 26use PVE::GuestHelpers qw(assert_tag_permissions);
ffda963f 27use PVE::QemuConfig;
1e3baf05 28use PVE::QemuServer;
2be1fb0a 29use PVE::QemuServer::Cloudinit;
6e72f90b 30use PVE::QemuServer::CPUConfig;
e6ac9fed
DJ
31use PVE::QemuServer::Drive;
32use PVE::QemuServer::ImportDisk;
0a13e08e 33use PVE::QemuServer::Monitor qw(mon_cmd);
90b20b15 34use PVE::QemuServer::Machine;
7f8c8087 35use PVE::QemuServer::Memory qw(get_current_memory);
9b71c34d
DC
36use PVE::QemuServer::PCI;
37use PVE::QemuServer::USB;
3ea94c60 38use PVE::QemuMigrate;
1e3baf05
DM
39use PVE::RPCEnvironment;
40use PVE::AccessControl;
41use PVE::INotify;
de8f60b2 42use PVE::Network;
e9abcde6 43use PVE::Firewall;
228a998b 44use PVE::API2::Firewall::VM;
b8158701 45use PVE::API2::Qemu::Agent;
d9123ef5 46use PVE::VZDump::Plugin;
48cf040f 47use PVE::DataCenterConfig;
f42ea29b 48use PVE::SSHInfo;
a9453218 49use PVE::Replication;
347dc136 50use PVE::StorageTunnel;
9f11fc5f
WB
51
52BEGIN {
53 if (!$ENV{PVE_GENERATING_DOCS}) {
54 require PVE::HA::Env::PVE2;
55 import PVE::HA::Env::PVE2;
56 require PVE::HA::Config;
57 import PVE::HA::Config;
58 }
59}
1e3baf05 60
1e3baf05
DM
61use base qw(PVE::RESTHandler);
62
63my $opt_force_description = "Force physical removal. Without this, we simple remove the disk from the config file and create an additional configuration entry called 'unused[n]', which contains the volume ID. Unlink of unused[n] always cause physical removal.";
64
65my $resolve_cdrom_alias = sub {
66 my $param = shift;
67
68 if (my $value = $param->{cdrom}) {
69 $value .= ",media=cdrom" if $value !~ m/media=/;
70 $param->{ide2} = $value;
71 delete $param->{cdrom};
72 }
73};
74
c1accf9d
FE
75# Used in import-enabled API endpoints. Parses drives using the extended '_with_alloc' schema.
76my $foreach_volume_with_alloc = sub {
77 my ($param, $func) = @_;
78
79 for my $opt (sort keys $param->%*) {
80 next if !PVE::QemuServer::is_valid_drivename($opt);
81
82 my $drive = PVE::QemuServer::Drive::parse_drive($opt, $param->{$opt}, 1);
83 next if !$drive;
84
85 $func->($opt, $drive);
86 }
87};
88
7979bbcd
FE
89my $check_drive_param = sub {
90 my ($param, $storecfg, $extra_checks) = @_;
91
92 for my $opt (sort keys $param->%*) {
93 next if !PVE::QemuServer::is_valid_drivename($opt);
94
c1accf9d 95 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}, 1);
7979bbcd
FE
96 raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
97
e6ac9fed 98 if ($drive->{'import-from'}) {
028aee45 99 if ($drive->{file} !~ $PVE::QemuServer::Drive::NEW_DISK_RE || $3 != 0) {
e6ac9fed
DJ
100 raise_param_exc({
101 $opt => "'import-from' requires special syntax - ".
102 "use <storage ID>:0,import-from=<source>",
103 });
104 }
105
106 if ($opt eq 'efidisk0') {
107 for my $required (qw(efitype pre-enrolled-keys)) {
108 if (!defined($drive->{$required})) {
109 raise_param_exc({
110 $opt => "need to specify '$required' when using 'import-from'",
111 });
112 }
113 }
114 } elsif ($opt eq 'tpmstate0') {
115 raise_param_exc({ $opt => "need to specify 'version' when using 'import-from'" })
116 if !defined($drive->{version});
117 }
118 }
119
7979bbcd
FE
120 PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
121
122 $extra_checks->($drive) if $extra_checks;
123
c1accf9d 124 $param->{$opt} = PVE::QemuServer::print_drive($drive, 1);
7979bbcd
FE
125 }
126};
127
ae57f6b3 128my $check_storage_access = sub {
fcbb753e 129 my ($rpcenv, $authuser, $storecfg, $vmid, $settings, $default_storage) = @_;
a0d1b1a2 130
c1accf9d 131 $foreach_volume_with_alloc->($settings, sub {
ae57f6b3 132 my ($ds, $drive) = @_;
a0d1b1a2 133
ae57f6b3 134 my $isCDROM = PVE::QemuServer::drive_is_cdrom($drive);
a0d1b1a2 135
ae57f6b3 136 my $volid = $drive->{file};
21e1ee7b 137 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
a0d1b1a2 138
8f2c9019 139 if (!$volid || ($volid eq 'none' || $volid eq 'cloudinit' || (defined($volname) && $volname eq 'cloudinit'))) {
09d0ee64 140 # nothing to check
f5782fd0
DM
141 } elsif ($isCDROM && ($volid eq 'cdrom')) {
142 $rpcenv->check($authuser, "/", ['Sys.Console']);
028aee45 143 } elsif (!$isCDROM && ($volid =~ $PVE::QemuServer::Drive::NEW_DISK_RE)) {
a0d1b1a2
DM
144 my ($storeid, $size) = ($2 || $default_storage, $3);
145 die "no storage ID specified (and no default storage)\n" if !$storeid;
fcbb753e 146 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
c46366fd
DC
147 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
148 raise_param_exc({ storage => "storage '$storeid' does not support vm images"})
149 if !$scfg->{content}->{images};
a0d1b1a2 150 } else {
db81c007
FE
151 PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $volid);
152 if ($storeid) {
153 my ($vtype) = PVE::Storage::parse_volname($storecfg, $volid);
154 raise_param_exc({ $ds => "content type needs to be 'images' or 'iso'" })
155 if $vtype ne 'images' && $vtype ne 'iso';
156 }
a0d1b1a2 157 }
e6ac9fed
DJ
158
159 if (my $src_image = $drive->{'import-from'}) {
160 my $src_vmid;
161 if (PVE::Storage::parse_volume_id($src_image, 1)) { # PVE-managed volume
162 (my $vtype, undef, $src_vmid) = PVE::Storage::parse_volname($storecfg, $src_image);
163 raise_param_exc({ $ds => "$src_image has wrong type '$vtype' - not an image" })
164 if $vtype ne 'images';
165 }
166
167 if ($src_vmid) { # might be actively used by VM and will be copied via clone_disk()
168 $rpcenv->check($authuser, "/vms/${src_vmid}", ['VM.Clone']);
169 } else {
170 PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $src_image);
171 }
172 }
a0d1b1a2 173 });
253624c7
FG
174
175 $rpcenv->check($authuser, "/storage/$settings->{vmstatestorage}", ['Datastore.AllocateSpace'])
176 if defined($settings->{vmstatestorage});
ae57f6b3 177};
a0d1b1a2 178
9418baad 179my $check_storage_access_clone = sub {
81f043eb 180 my ($rpcenv, $authuser, $storecfg, $conf, $storage) = @_;
6116f729 181
55173c6b
DM
182 my $sharedvm = 1;
183
912792e2 184 PVE::QemuConfig->foreach_volume($conf, sub {
6116f729
DM
185 my ($ds, $drive) = @_;
186
187 my $isCDROM = PVE::QemuServer::drive_is_cdrom($drive);
188
189 my $volid = $drive->{file};
190
191 return if !$volid || $volid eq 'none';
192
193 if ($isCDROM) {
194 if ($volid eq 'cdrom') {
195 $rpcenv->check($authuser, "/", ['Sys.Console']);
196 } else {
75466c4f 197 # we simply allow access
55173c6b
DM
198 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
199 my $scfg = PVE::Storage::storage_config($storecfg, $sid);
200 $sharedvm = 0 if !$scfg->{shared};
201
6116f729
DM
202 }
203 } else {
55173c6b
DM
204 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
205 my $scfg = PVE::Storage::storage_config($storecfg, $sid);
206 $sharedvm = 0 if !$scfg->{shared};
207
81f043eb 208 $sid = $storage if $storage;
6116f729
DM
209 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
210 }
211 });
55173c6b 212
253624c7
FG
213 $rpcenv->check($authuser, "/storage/$conf->{vmstatestorage}", ['Datastore.AllocateSpace'])
214 if defined($conf->{vmstatestorage});
215
55173c6b 216 return $sharedvm;
6116f729
DM
217};
218
9fb295d0
FG
219my $check_storage_access_migrate = sub {
220 my ($rpcenv, $authuser, $storecfg, $storage, $node) = @_;
221
222 PVE::Storage::storage_check_enabled($storecfg, $storage, $node);
223
224 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
225
226 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
227 die "storage '$storage' does not support vm images\n"
228 if !$scfg->{content}->{images};
229};
230
e6ac9fed
DJ
231my $import_from_volid = sub {
232 my ($storecfg, $src_volid, $dest_info, $vollist) = @_;
233
234 die "could not get size of $src_volid\n"
235 if !PVE::Storage::volume_size_info($storecfg, $src_volid, 10);
236
237 die "cannot import from cloudinit disk\n"
238 if PVE::QemuServer::Drive::drive_is_cloudinit({ file => $src_volid });
239
240 my $src_vmid = (PVE::Storage::parse_volname($storecfg, $src_volid))[2];
241
242 my $src_vm_state = sub {
243 my $exists = $src_vmid && PVE::Cluster::get_vmlist()->{ids}->{$src_vmid} ? 1 : 0;
244
245 my $runs = 0;
246 if ($exists) {
247 eval { PVE::QemuConfig::assert_config_exists_on_node($src_vmid); };
248 die "owner VM $src_vmid not on local node\n" if $@;
249 $runs = PVE::QemuServer::Helpers::vm_running_locally($src_vmid) || 0;
250 }
251
252 return ($exists, $runs);
253 };
254
255 my ($src_vm_exists, $running) = $src_vm_state->();
256
257 die "cannot import from '$src_volid' - full clone feature is not supported\n"
258 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $src_volid, undef, $running);
259
260 my $clonefn = sub {
261 my ($src_vm_exists_now, $running_now) = $src_vm_state->();
262
263 die "owner VM $src_vmid changed state unexpectedly\n"
264 if $src_vm_exists_now != $src_vm_exists || $running_now != $running;
265
266 my $src_conf = $src_vm_exists_now ? PVE::QemuConfig->load_config($src_vmid) : {};
267
268 my $src_drive = { file => $src_volid };
269 my $src_drivename;
270 PVE::QemuConfig->foreach_volume($src_conf, sub {
271 my ($ds, $drive) = @_;
272
273 return if $src_drivename;
274
275 if ($drive->{file} eq $src_volid) {
276 $src_drive = $drive;
277 $src_drivename = $ds;
278 }
279 });
280
281 my $source_info = {
282 vmid => $src_vmid,
283 running => $running_now,
284 drivename => $src_drivename,
285 drive => $src_drive,
286 snapname => undef,
287 };
288
289 my ($src_storeid) = PVE::Storage::parse_volume_id($src_volid);
290
291 return PVE::QemuServer::clone_disk(
292 $storecfg,
293 $source_info,
294 $dest_info,
295 1,
296 $vollist,
297 undef,
298 undef,
299 $src_conf->{agent},
300 PVE::Storage::get_bandwidth_limit('clone', [$src_storeid, $dest_info->{storage}]),
301 );
302 };
303
304 my $cloned;
305 if ($running) {
306 $cloned = PVE::QemuConfig->lock_config_full($src_vmid, 30, $clonefn);
307 } elsif ($src_vmid) {
308 $cloned = PVE::QemuConfig->lock_config_shared($src_vmid, 30, $clonefn);
309 } else {
310 $cloned = $clonefn->();
311 }
312
313 return $cloned->@{qw(file size)};
314};
315
ae57f6b3
DM
316# Note: $pool is only needed when creating a VM, because pool permissions
317# are automatically inherited if VM already exists inside a pool.
318my $create_disks = sub {
96ed3574 319 my ($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
a0d1b1a2
DM
320
321 my $vollist = [];
a0d1b1a2 322
ae57f6b3 323 my $res = {};
64932aeb
DM
324
325 my $code = sub {
ae57f6b3
DM
326 my ($ds, $disk) = @_;
327
328 my $volid = $disk->{file};
21e1ee7b 329 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
ae57f6b3 330
f5782fd0 331 if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
628e9a2b 332 delete $disk->{size};
71c58bb7 333 $res->{$ds} = PVE::QemuServer::print_drive($disk);
8f2c9019 334 } elsif (defined($volname) && $volname eq 'cloudinit') {
21e1ee7b 335 $storeid = $storeid // $default_storage;
0c9a7596 336 die "no storage ID specified (and no default storage)\n" if !$storeid;
39b56b16
FE
337
338 if (
339 my $ci_key = PVE::QemuConfig->has_cloudinit($conf, $ds)
340 || PVE::QemuConfig->has_cloudinit($conf->{pending} || {}, $ds)
341 || PVE::QemuConfig->has_cloudinit($res, $ds)
342 ) {
343 die "$ds - cloud-init drive is already attached at '$ci_key'\n";
344 }
345
0c9a7596
AD
346 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
347 my $name = "vm-$vmid-cloudinit";
64d1a6ae 348
0c9a7596
AD
349 my $fmt = undef;
350 if ($scfg->{path}) {
c152600b 351 $fmt = $disk->{format} // "qcow2";
64d1a6ae
WL
352 $name .= ".$fmt";
353 } else {
c152600b 354 $fmt = $disk->{format} // "raw";
0c9a7596 355 }
64d1a6ae 356
4fdc1d3d 357 # Initial disk created with 4 MB and aligned to 4MB on regeneration
7d761a01
ML
358 my $ci_size = PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE;
359 my $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, $name, $ci_size/1024);
0c9a7596
AD
360 $disk->{file} = $volid;
361 $disk->{media} = 'cdrom';
362 push @$vollist, $volid;
363 delete $disk->{format}; # no longer needed
71c58bb7 364 $res->{$ds} = PVE::QemuServer::print_drive($disk);
3e7d9fac 365 print "$ds: successfully created disk '$res->{$ds}'\n";
028aee45 366 } elsif ($volid =~ $PVE::QemuServer::Drive::NEW_DISK_RE) {
ae57f6b3
DM
367 my ($storeid, $size) = ($2 || $default_storage, $3);
368 die "no storage ID specified (and no default storage)\n" if !$storeid;
e6ac9fed
DJ
369
370 if (my $source = delete $disk->{'import-from'}) {
371 my $dst_volid;
372
373 if (PVE::Storage::parse_volume_id($source, 1)) { # PVE-managed volume
374 my $dest_info = {
375 vmid => $vmid,
376 drivename => $ds,
377 storage => $storeid,
378 format => $disk->{format},
379 };
380
381 $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($conf, $disk)
382 if $ds eq 'efidisk0';
383
384 ($dst_volid, $size) = eval {
385 $import_from_volid->($storecfg, $source, $dest_info, $vollist);
386 };
387 die "cannot import from '$source' - $@" if $@;
388 } else {
389 $source = PVE::Storage::abs_filesystem_path($storecfg, $source, 1);
390 $size = PVE::Storage::file_size_info($source);
391 die "could not get file size of $source\n" if !$size;
392
393 (undef, $dst_volid) = PVE::QemuServer::ImportDisk::do_import(
394 $source,
395 $vmid,
396 $storeid,
397 {
398 drive_name => $ds,
399 format => $disk->{format},
400 'skip-config-update' => 1,
401 },
402 );
403 push @$vollist, $dst_volid;
404 }
405
406 $disk->{file} = $dst_volid;
407 $disk->{size} = $size;
408 delete $disk->{format}; # no longer needed
409 $res->{$ds} = PVE::QemuServer::print_drive($disk);
1a35631a 410 } else {
e6ac9fed
DJ
411 my $defformat = PVE::Storage::storage_default_format($storecfg, $storeid);
412 my $fmt = $disk->{format} || $defformat;
413
414 $size = PVE::Tools::convert_size($size, 'gb' => 'kb'); # vdisk_alloc uses kb
415
416 my $volid;
417 if ($ds eq 'efidisk0') {
418 my $smm = PVE::QemuServer::Machine::machine_type_is_q35($conf);
419 ($volid, $size) = PVE::QemuServer::create_efidisk(
420 $storecfg, $storeid, $vmid, $fmt, $arch, $disk, $smm);
421 } elsif ($ds eq 'tpmstate0') {
422 # swtpm can only use raw volumes, and uses a fixed size
423 $size = PVE::Tools::convert_size(PVE::QemuServer::Drive::TPMSTATE_DISK_SIZE, 'b' => 'kb');
424 $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, "raw", undef, $size);
425 } else {
426 $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, undef, $size);
427 }
428 push @$vollist, $volid;
429 $disk->{file} = $volid;
430 $disk->{size} = PVE::Tools::convert_size($size, 'kb' => 'b');
431 delete $disk->{format}; # no longer needed
432 $res->{$ds} = PVE::QemuServer::print_drive($disk);
1a35631a 433 }
3e7d9fac
FE
434
435 print "$ds: successfully created disk '$res->{$ds}'\n";
ae57f6b3 436 } else {
db81c007
FE
437 PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $volid);
438 if ($storeid) {
439 my ($vtype) = PVE::Storage::parse_volname($storecfg, $volid);
440 die "cannot use volume $volid - content type needs to be 'images' or 'iso'"
441 if $vtype ne 'images' && $vtype ne 'iso';
39b56b16
FE
442
443 if (PVE::QemuServer::Drive::drive_is_cloudinit($disk)) {
444 if (
445 my $ci_key = PVE::QemuConfig->has_cloudinit($conf, $ds)
446 || PVE::QemuConfig->has_cloudinit($conf->{pending} || {}, $ds)
447 || PVE::QemuConfig->has_cloudinit($res, $ds)
448 ) {
449 die "$ds - cloud-init drive is already attached at '$ci_key'\n";
450 }
451 }
db81c007 452 }
75466c4f 453
fe19840a 454 PVE::Storage::activate_volumes($storecfg, [ $volid ]) if $storeid;
09a89895 455
fe19840a
FE
456 my $size = PVE::Storage::volume_size_info($storecfg, $volid);
457 die "volume $volid does not exist\n" if !$size;
458 $disk->{size} = $size;
24afaca0 459
71c58bb7 460 $res->{$ds} = PVE::QemuServer::print_drive($disk);
a0d1b1a2 461 }
64932aeb
DM
462 };
463
c1accf9d 464 eval { $foreach_volume_with_alloc->($settings, $code); };
a0d1b1a2
DM
465
466 # free allocated images on error
467 if (my $err = $@) {
468 syslog('err', "VM $vmid creating disks failed");
469 foreach my $volid (@$vollist) {
470 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
471 warn $@ if $@;
472 }
473 die $err;
474 }
475
367e6bf4 476 return ($vollist, $res);
a0d1b1a2
DM
477};
478
6e72f90b
SR
479my $check_cpu_model_access = sub {
480 my ($rpcenv, $authuser, $new, $existing) = @_;
481
482 return if !defined($new->{cpu});
483
484 my $cpu = PVE::JSONSchema::check_format('pve-vm-cpu-conf', $new->{cpu});
485 return if !$cpu || !$cpu->{cputype}; # always allow default
486 my $cputype = $cpu->{cputype};
487
488 if ($existing && $existing->{cpu}) {
489 # changing only other settings doesn't require permissions for CPU model
490 my $existingCpu = PVE::JSONSchema::check_format('pve-vm-cpu-conf', $existing->{cpu});
491 return if $existingCpu->{cputype} eq $cputype;
492 }
493
494 if (PVE::QemuServer::CPUConfig::is_custom_model($cputype)) {
495 $rpcenv->check($authuser, "/nodes", ['Sys.Audit']);
496 }
497};
498
58cb690b
DC
499my $cpuoptions = {
500 'cores' => 1,
501 'cpu' => 1,
502 'cpulimit' => 1,
503 'cpuunits' => 1,
504 'numa' => 1,
505 'smp' => 1,
506 'sockets' => 1,
84b31f48 507 'vcpus' => 1,
58cb690b
DC
508};
509
510my $memoryoptions = {
511 'memory' => 1,
512 'balloon' => 1,
513 'shares' => 1,
514};
515
516my $hwtypeoptions = {
517 'acpi' => 1,
518 'hotplug' => 1,
519 'kvm' => 1,
520 'machine' => 1,
521 'scsihw' => 1,
522 'smbios1' => 1,
523 'tablet' => 1,
524 'vga' => 1,
525 'watchdog' => 1,
b2dd61a0 526 'audio0' => 1,
58cb690b
DC
527};
528
6bcacc21 529my $generaloptions = {
58cb690b
DC
530 'agent' => 1,
531 'autostart' => 1,
532 'bios' => 1,
533 'description' => 1,
534 'keyboard' => 1,
535 'localtime' => 1,
536 'migrate_downtime' => 1,
537 'migrate_speed' => 1,
538 'name' => 1,
539 'onboot' => 1,
540 'ostype' => 1,
541 'protection' => 1,
542 'reboot' => 1,
543 'startdate' => 1,
544 'startup' => 1,
545 'tdf' => 1,
546 'template' => 1,
547};
548
549my $vmpoweroptions = {
550 'freeze' => 1,
551};
552
553my $diskoptions = {
554 'boot' => 1,
555 'bootdisk' => 1,
253624c7 556 'vmstatestorage' => 1,
58cb690b
DC
557};
558
7ee990cd 559my $cloudinitoptions = {
cb702ebe 560 cicustom => 1,
7ee990cd
DM
561 cipassword => 1,
562 citype => 1,
563 ciuser => 1,
92c02f6c 564 ciupgrade => 1,
7ee990cd
DM
565 nameserver => 1,
566 searchdomain => 1,
567 sshkeys => 1,
568};
569
0761ee01
FE
570my $check_vm_create_serial_perm = sub {
571 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
572
573 return 1 if $authuser eq 'root@pam';
574
575 foreach my $opt (keys %{$param}) {
576 next if $opt !~ m/^serial\d+$/;
577
578 if ($param->{$opt} eq 'socket') {
579 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
580 } else {
581 die "only root can set '$opt' config for real devices\n";
582 }
583 }
584
585 return 1;
586};
587
0cf8d56c
DC
588my sub check_usb_perm {
589 my ($rpcenv, $authuser, $vmid, $pool, $opt, $value) = @_;
590
591 return 1 if $authuser eq 'root@pam';
592
e3971865
DC
593 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
594
0cf8d56c 595 my $device = PVE::JSONSchema::parse_property_string('pve-qm-usb', $value);
e3971865 596 if ($device->{host} && $device->{host} !~ m/^spice$/i) {
0cf8d56c 597 die "only root can set '$opt' config for real devices\n";
e3971865
DC
598 } elsif ($device->{mapping}) {
599 $rpcenv->check_full($authuser, "/mapping/usb/$device->{mapping}", ['Mapping.Use']);
600 } else {
601 die "either 'host' or 'mapping' must be set.\n";
0cf8d56c
DC
602 }
603
604 return 1;
605}
606
607my sub check_vm_create_usb_perm {
0761ee01
FE
608 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
609
610 return 1 if $authuser eq 'root@pam';
611
612 foreach my $opt (keys %{$param}) {
613 next if $opt !~ m/^usb\d+$/;
0cf8d56c 614 check_usb_perm($rpcenv, $authuser, $vmid, $pool, $opt, $param->{$opt});
0761ee01
FE
615 }
616
617 return 1;
618};
619
9b71c34d
DC
620my sub check_hostpci_perm {
621 my ($rpcenv, $authuser, $vmid, $pool, $opt, $value) = @_;
622
623 return 1 if $authuser eq 'root@pam';
624
625 my $device = PVE::JSONSchema::parse_property_string('pve-qm-hostpci', $value);
626 if ($device->{host}) {
627 die "only root can set '$opt' config for non-mapped devices\n";
628 } elsif ($device->{mapping}) {
629 $rpcenv->check_full($authuser, "/mapping/pci/$device->{mapping}", ['Mapping.Use']);
630 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
631 } else {
632 die "either 'host' or 'mapping' must be set.\n";
633 }
634
635 return 1;
636}
637
638my sub check_vm_create_hostpci_perm {
639 my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
640
641 return 1 if $authuser eq 'root@pam';
642
643 foreach my $opt (keys %{$param}) {
644 next if $opt !~ m/^hostpci\d+$/;
645 check_hostpci_perm($rpcenv, $authuser, $vmid, $pool, $opt, $param->{$opt});
646 }
647
648 return 1;
649};
650
a0d1b1a2 651my $check_vm_modify_config_perm = sub {
e30f75c5 652 my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
a0d1b1a2 653
6e5c4da7 654 return 1 if $authuser eq 'root@pam';
a0d1b1a2 655
ae57f6b3 656 foreach my $opt (@$key_list) {
97415261
TL
657 # some checks (e.g., disk, serial port, usb) need to be done somewhere
658 # else, as there the permission can be value dependend
74479ee9 659 next if PVE::QemuServer::is_valid_drivename($opt);
58cb690b 660 next if $opt eq 'cdrom';
9b71c34d 661 next if $opt =~ m/^(?:unused|serial|usb|hostpci)\d+$/;
97c76039 662 next if $opt eq 'tags';
165be267 663
a0d1b1a2 664
6bcacc21 665 if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
a0d1b1a2 666 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
58cb690b 667 } elsif ($memoryoptions->{$opt}) {
a0d1b1a2 668 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
58cb690b 669 } elsif ($hwtypeoptions->{$opt}) {
a0d1b1a2 670 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
6bcacc21 671 } elsif ($generaloptions->{$opt}) {
58cb690b
DC
672 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
673 # special case for startup since it changes host behaviour
674 if ($opt eq 'startup') {
675 $rpcenv->check_full($authuser, "/", ['Sys.Modify']);
676 }
677 } elsif ($vmpoweroptions->{$opt}) {
678 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
679 } elsif ($diskoptions->{$opt}) {
680 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
46f3fc25 681 } elsif ($opt =~ m/^net\d+$/) {
a0d1b1a2 682 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
46f3fc25 683 } elsif ($cloudinitoptions->{$opt} || $opt =~ m/^ipconfig\d+$/) {
fc701af7 684 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Cloudinit', 'VM.Config.Network'], 1);
5661a681
DC
685 } elsif ($opt eq 'vmstate') {
686 # the user needs Disk and PowerMgmt privileges to change the vmstate
687 # also needs privileges on the storage, that will be checked later
688 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk', 'VM.PowerMgmt' ]);
a0d1b1a2 689 } else {
9b71c34d 690 # catches args, lock, etc.
58cb690b
DC
691 # new options will be checked here
692 die "only root can set '$opt' config\n";
a0d1b1a2
DM
693 }
694 }
695
696 return 1;
697};
698
d1feab4a
HD
699sub assert_scsi_feature_compatibility {
700 my ($opt, $conf, $storecfg, $drive_attributes) = @_;
701
40786ff9 702 my $drive = PVE::QemuServer::Drive::parse_drive($opt, $drive_attributes, 1);
d1feab4a
HD
703
704 my $machine_type = PVE::QemuServer::get_vm_machine($conf, undef, $conf->{arch});
705 my $machine_version = PVE::QemuServer::Machine::extract_version(
706 $machine_type, PVE::QemuServer::kvm_user_version());
707 my $drivetype = PVE::QemuServer::Drive::get_scsi_devicetype(
708 $drive, $storecfg, $machine_version);
709
710 if ($drivetype ne 'hd' && $drivetype ne 'cd') {
711 if ($drive->{product}) {
712 raise_param_exc({
713 $opt => "Passing of product information is only supported for 'scsi-hd' and "
714 ."'scsi-cd' devices (e.g. not pass-through).",
715 });
716 }
717 if ($drive->{vendor}) {
718 raise_param_exc({
719 $opt => "Passing of vendor information is only supported for 'scsi-hd' and "
720 ."'scsi-cd' devices (e.g. not pass-through).",
721 });
722 }
723 }
724}
725
1e3baf05 726__PACKAGE__->register_method({
afdb31d5
DM
727 name => 'vmlist',
728 path => '',
1e3baf05
DM
729 method => 'GET',
730 description => "Virtual machine index (per node).",
a0d1b1a2
DM
731 permissions => {
732 description => "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.",
733 user => 'all',
734 },
1e3baf05
DM
735 proxyto => 'node',
736 protected => 1, # qemu pid files are only readable by root
737 parameters => {
3326ae19 738 additionalProperties => 0,
1e3baf05
DM
739 properties => {
740 node => get_standard_option('pve-node'),
12612b09
WB
741 full => {
742 type => 'boolean',
743 optional => 1,
744 description => "Determine the full status of active VMs.",
745 },
1e3baf05
DM
746 },
747 },
748 returns => {
749 type => 'array',
750 items => {
751 type => "object",
b1a70cab 752 properties => $PVE::QemuServer::vmstatus_return_properties,
1e3baf05
DM
753 },
754 links => [ { rel => 'child', href => "{vmid}" } ],
755 },
756 code => sub {
757 my ($param) = @_;
758
a0d1b1a2
DM
759 my $rpcenv = PVE::RPCEnvironment::get();
760 my $authuser = $rpcenv->get_user();
761
12612b09 762 my $vmstatus = PVE::QemuServer::vmstatus(undef, $param->{full});
1e3baf05 763
a0d1b1a2
DM
764 my $res = [];
765 foreach my $vmid (keys %$vmstatus) {
766 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
767
768 my $data = $vmstatus->{$vmid};
a0d1b1a2
DM
769 push @$res, $data;
770 }
1e3baf05 771
a0d1b1a2 772 return $res;
1e3baf05
DM
773 }});
774
d1e92cf6
DM
775my $parse_restore_archive = sub {
776 my ($storecfg, $archive) = @_;
777
e5fd1c65 778 my ($archive_storeid, $archive_volname) = PVE::Storage::parse_volume_id($archive, 1);
d1e92cf6 779
c36214d2
FG
780 my $res = {};
781
d1e92cf6
DM
782 if (defined($archive_storeid)) {
783 my $scfg = PVE::Storage::storage_config($storecfg, $archive_storeid);
c36214d2 784 $res->{volid} = $archive;
d1e92cf6 785 if ($scfg->{type} eq 'pbs') {
c36214d2
FG
786 $res->{type} = 'pbs';
787 return $res;
d1e92cf6
DM
788 }
789 }
790 my $path = PVE::Storage::abs_filesystem_path($storecfg, $archive);
c36214d2
FG
791 $res->{type} = 'file';
792 $res->{path} = $path;
793 return $res;
d1e92cf6 794};
d703d4c0 795
d703d4c0 796
1e3baf05 797__PACKAGE__->register_method({
afdb31d5
DM
798 name => 'create_vm',
799 path => '',
1e3baf05 800 method => 'POST',
3e16d5fc 801 description => "Create or restore a virtual machine.",
a0d1b1a2 802 permissions => {
f9bfceef
DM
803 description => "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
804 "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
9cfd06d9
AD
805 "If you create disks you need 'Datastore.AllocateSpace' on any used storage." .
806 "If you use a bridge/vlan, you need 'SDN.Use' on any used bridge/vlan.",
f9bfceef 807 user => 'all', # check inside
a0d1b1a2 808 },
1e3baf05
DM
809 protected => 1,
810 proxyto => 'node',
811 parameters => {
3326ae19 812 additionalProperties => 0,
1e3baf05
DM
813 properties => PVE::QemuServer::json_config_properties(
814 {
815 node => get_standard_option('pve-node'),
65e866e5 816 vmid => get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_next_vmid }),
3e16d5fc 817 archive => {
d1e92cf6 818 description => "The backup archive. Either the file system path to a .tar or .vma file (use '-' to pipe data from stdin) or a proxmox storage backup volume identifier.",
3e16d5fc
DM
819 type => 'string',
820 optional => 1,
821 maxLength => 255,
65e866e5 822 completion => \&PVE::QemuServer::complete_backup_archives,
3e16d5fc
DM
823 },
824 storage => get_standard_option('pve-storage-id', {
825 description => "Default storage.",
826 optional => 1,
335af808 827 completion => \&PVE::QemuServer::complete_storage,
3e16d5fc
DM
828 }),
829 force => {
afdb31d5 830 optional => 1,
3e16d5fc
DM
831 type => 'boolean',
832 description => "Allow to overwrite existing VM.",
51586c3a
DM
833 requires => 'archive',
834 },
835 unique => {
afdb31d5 836 optional => 1,
51586c3a
DM
837 type => 'boolean',
838 description => "Assign a unique random ethernet address.",
839 requires => 'archive',
3e16d5fc 840 },
26731a3c
SR
841 'live-restore' => {
842 optional => 1,
843 type => 'boolean',
844 description => "Start the VM immediately from the backup and restore in background. PBS only.",
845 requires => 'archive',
846 },
75466c4f 847 pool => {
a0d1b1a2
DM
848 optional => 1,
849 type => 'string', format => 'pve-poolid',
850 description => "Add the VM to the specified pool.",
851 },
7c536e11 852 bwlimit => {
0aab5a16 853 description => "Override I/O bandwidth limit (in KiB/s).",
7c536e11
WB
854 optional => 1,
855 type => 'integer',
856 minimum => '0',
41756a3b 857 default => 'restore limit from datacenter or storage config',
e33f774d
TL
858 },
859 start => {
860 optional => 1,
861 type => 'boolean',
862 default => 0,
863 description => "Start VM after it was created successfully.",
864 },
c1accf9d
FE
865 },
866 1, # with_disk_alloc
867 ),
1e3baf05 868 },
afdb31d5 869 returns => {
5fdbe4f0
DM
870 type => 'string',
871 },
1e3baf05
DM
872 code => sub {
873 my ($param) = @_;
874
5fdbe4f0 875 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 876 my $authuser = $rpcenv->get_user();
5fdbe4f0 877
1e3baf05 878 my $node = extract_param($param, 'node');
1e3baf05
DM
879 my $vmid = extract_param($param, 'vmid');
880
3e16d5fc 881 my $archive = extract_param($param, 'archive');
8ba8418c 882 my $is_restore = !!$archive;
3e16d5fc 883
b924c435 884 my $bwlimit = extract_param($param, 'bwlimit');
51586c3a 885 my $force = extract_param($param, 'force');
a0d1b1a2 886 my $pool = extract_param($param, 'pool');
e33f774d 887 my $start_after_create = extract_param($param, 'start');
b924c435
TL
888 my $storage = extract_param($param, 'storage');
889 my $unique = extract_param($param, 'unique');
26731a3c
SR
890 my $live_restore = extract_param($param, 'live-restore');
891
0c9a7596
AD
892 if (defined(my $ssh_keys = $param->{sshkeys})) {
893 $ssh_keys = URI::Escape::uri_unescape($ssh_keys);
894 PVE::Tools::validate_ssh_public_keys($ssh_keys);
895 }
896
8481721f 897 $param->{cpuunits} = PVE::CGroup::clamp_cpu_shares($param->{cpuunits})
dbc45fdf
FE
898 if defined($param->{cpuunits}); # clamp value depending on cgroup version
899
3e16d5fc 900 PVE::Cluster::check_cfs_quorum();
1e3baf05 901
b924c435
TL
902 my $filename = PVE::QemuConfig->config_file($vmid);
903 my $storecfg = PVE::Storage::config();
904
a0d1b1a2
DM
905 if (defined($pool)) {
906 $rpcenv->check_pool_exist($pool);
75466c4f 907 }
a0d1b1a2 908
fcbb753e 909 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace'])
a0d1b1a2
DM
910 if defined($storage);
911
f9bfceef
DM
912 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
913 # OK
914 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
915 # OK
916 } elsif ($archive && $force && (-f $filename) &&
917 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
4748661a 918 # OK: user has VM.Backup permissions and wants to restore an existing VM
f9bfceef
DM
919 } else {
920 raise_perm_exc();
921 }
922
325b32cc 923 if ($archive) {
202a2a0b
FE
924 for my $opt (sort keys $param->%*) {
925 if (PVE::QemuServer::Drive::is_valid_drivename($opt)) {
926 raise_param_exc({ $opt => "option conflicts with option 'archive'" });
927 }
928 }
bc4dcb99 929
5b9d692a 930 if ($archive eq '-') {
3326ae19 931 die "pipe requires cli environment\n" if $rpcenv->{type} ne 'cli';
d1e92cf6 932 $archive = { type => 'pipe' };
5b9d692a 933 } else {
f9be9137
FE
934 PVE::Storage::check_volume_access(
935 $rpcenv,
936 $authuser,
937 $storecfg,
938 $vmid,
939 $archive,
940 'backup',
941 );
d1e92cf6
DM
942
943 $archive = $parse_restore_archive->($storecfg, $archive);
971f27c4 944 }
1e3baf05
DM
945 }
946
325b32cc
FE
947 if (scalar(keys $param->%*) > 0) {
948 &$resolve_cdrom_alias($param);
949
950 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param, $storage);
951
952 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
953
954 &$check_vm_create_serial_perm($rpcenv, $authuser, $vmid, $pool, $param);
0cf8d56c 955 check_vm_create_usb_perm($rpcenv, $authuser, $vmid, $pool, $param);
9b71c34d 956 check_vm_create_hostpci_perm($rpcenv, $authuser, $vmid, $pool, $param);
0cf8d56c 957
525c6c62 958 PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $param);
325b32cc
FE
959 &$check_cpu_model_access($rpcenv, $authuser, $param);
960
961 $check_drive_param->($param, $storecfg);
962
963 PVE::QemuServer::add_random_macs($param);
964 }
965
4fedc13b 966 my $emsg = $is_restore ? "unable to restore VM $vmid -" : "unable to create VM $vmid -";
3e16d5fc 967
4fedc13b
TL
968 eval { PVE::QemuConfig->create_and_lock_config($vmid, $force) };
969 die "$emsg $@" if $@;
3e16d5fc 970
b973806e 971 my $restored_data = 0;
4fedc13b
TL
972 my $restorefn = sub {
973 my $conf = PVE::QemuConfig->load_config($vmid);
4d8d55f1 974
4fedc13b 975 PVE::QemuConfig->check_protection($conf, $emsg);
3a07a8a9 976
4fedc13b 977 die "$emsg vm is running\n" if PVE::QemuServer::check_running($vmid);
3e16d5fc
DM
978
979 my $realcmd = sub {
d1e92cf6 980 my $restore_options = {
51586c3a 981 storage => $storage,
a0d1b1a2 982 pool => $pool,
7c536e11 983 unique => $unique,
5294c110 984 bwlimit => $bwlimit,
26731a3c 985 live => $live_restore,
202a2a0b 986 override_conf => $param,
d1e92cf6 987 };
621edb2b
FG
988 if (my $volid = $archive->{volid}) {
989 # best effort, real check is after restoring!
990 my $merged = eval {
991 my $old_conf = PVE::Storage::extract_vzdump_config($storecfg, $volid);
992 PVE::QemuServer::restore_merge_config("backup/qemu-server/$vmid.conf", $old_conf, $param);
993 };
994 if ($@) {
995 warn "Could not extract backed up config: $@\n";
996 warn "Skipping early checks!\n";
997 } else {
998 PVE::QemuServer::check_restore_permissions($rpcenv, $authuser, $merged);
999 }
1000 }
d1e92cf6 1001 if ($archive->{type} eq 'file' || $archive->{type} eq 'pipe') {
b973806e
TL
1002 die "live-restore is only compatible with backup images from a Proxmox Backup Server\n"
1003 if $live_restore;
d1e92cf6
DM
1004 PVE::QemuServer::restore_file_archive($archive->{path} // '-', $vmid, $authuser, $restore_options);
1005 } elsif ($archive->{type} eq 'pbs') {
1006 PVE::QemuServer::restore_proxmox_backup_archive($archive->{volid}, $vmid, $authuser, $restore_options);
1007 } else {
1008 die "unknown backup archive type\n";
1009 }
b973806e
TL
1010 $restored_data = 1;
1011
5294c110
CE
1012 my $restored_conf = PVE::QemuConfig->load_config($vmid);
1013 # Convert restored VM to template if backup was VM template
1014 if (PVE::QemuConfig->is_template($restored_conf)) {
1015 warn "Convert to template.\n";
1016 eval { PVE::QemuServer::template_create($vmid, $restored_conf) };
1017 warn $@ if $@;
1018 }
ee8d2dea
AD
1019
1020 PVE::QemuServer::create_ifaces_ipams_ips($restored_conf, $vmid) if $unique;
3e16d5fc
DM
1021 };
1022
223e032b
WL
1023 # ensure no old replication state are exists
1024 PVE::ReplicationState::delete_guest_states($vmid);
1025
0c97024d
TL
1026 PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
1027
a0e27afb 1028 if ($start_after_create && !$live_restore) {
0c97024d
TL
1029 print "Execute autostart\n";
1030 eval { PVE::API2::Qemu->vm_start({ vmid => $vmid, node => $node }) };
1031 warn $@ if $@;
1032 }
3e16d5fc 1033 };
1e3baf05 1034
1e3baf05 1035 my $createfn = sub {
223e032b
WL
1036 # ensure no old replication state are exists
1037 PVE::ReplicationState::delete_guest_states($vmid);
1038
5fdbe4f0 1039 my $realcmd = sub {
1858638f 1040 my $conf = $param;
de64f101 1041 my $arch = PVE::QemuServer::get_vm_arch($conf);
40c3bcf8 1042
d1feab4a
HD
1043
1044 for my $opt (sort keys $param->%*) {
1045 next if $opt !~ m/^scsi\d+$/;
1046 assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt});
1047 }
1048
26b443c8
TL
1049 $conf->{meta} = PVE::QemuServer::new_meta_info_string();
1050
a85ff91b 1051 my $vollist = [];
5fdbe4f0 1052 eval {
367e6bf4
FE
1053 ($vollist, my $created_opts) = $create_disks->(
1054 $rpcenv,
1055 $authuser,
1056 $conf,
1057 $arch,
1058 $storecfg,
1059 $vmid,
1060 $pool,
1061 $param,
1062 $storage,
1063 );
1064 $conf->{$_} = $created_opts->{$_} for keys $created_opts->%*;
1e3baf05 1065
2141a802
SR
1066 if (!$conf->{boot}) {
1067 my $devs = PVE::QemuServer::get_default_bootdevices($conf);
1068 $conf->{boot} = PVE::QemuServer::print_bootorder($devs);
5fdbe4f0 1069 }
1e3baf05 1070
b62997a1
MF
1071 my $vga = PVE::QemuServer::parse_vga($conf->{vga});
1072 PVE::QemuServer::assert_clipboard_config($vga);
1073
47314bf5
DM
1074 # auto generate uuid if user did not specify smbios1 option
1075 if (!$conf->{smbios1}) {
ae2fcb3b 1076 $conf->{smbios1} = PVE::QemuServer::generate_smbios1_uuid();
47314bf5
DM
1077 }
1078
40c3bcf8 1079 if ((!defined($conf->{vmgenid}) || $conf->{vmgenid} eq '1') && $arch ne 'aarch64') {
6ee499ff
DC
1080 $conf->{vmgenid} = PVE::QemuServer::generate_uuid();
1081 }
1082
4dd1e83c
TL
1083 my $machine = $conf->{machine};
1084 if (!$machine || $machine =~ m/^(?:pc|q35|virt)$/) {
1085 # always pin Windows' machine version on create, they get to easily confused
238af88e 1086 if (PVE::QemuServer::Helpers::windows_version($conf->{ostype})) {
0761e619 1087 $conf->{machine} = PVE::QemuServer::windows_get_pinned_machine_version($machine);
4dd1e83c
TL
1088 }
1089 }
1090
ffda963f 1091 PVE::QemuConfig->write_config($vmid, $conf);
ae9ca91d 1092
5fdbe4f0
DM
1093 };
1094 my $err = $@;
1e3baf05 1095
5fdbe4f0
DM
1096 if ($err) {
1097 foreach my $volid (@$vollist) {
1098 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
1099 warn $@ if $@;
1100 }
4fedc13b 1101 die "$emsg $err";
5fdbe4f0 1102 }
502d18a2 1103
be517049 1104 PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
ee8d2dea
AD
1105
1106 PVE::QemuServer::create_ifaces_ipams_ips($conf, $vmid);
5fdbe4f0
DM
1107 };
1108
e33f774d
TL
1109 PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
1110
1111 if ($start_after_create) {
1112 print "Execute autostart\n";
5bf96183
WB
1113 eval { PVE::API2::Qemu->vm_start({vmid => $vmid, node => $node}) };
1114 warn $@ if $@;
e33f774d 1115 }
5fdbe4f0
DM
1116 };
1117
5bf96183
WB
1118 my ($code, $worker_name);
1119 if ($is_restore) {
1120 $worker_name = 'qmrestore';
1121 $code = sub {
1122 eval { $restorefn->() };
1123 if (my $err = $@) {
1124 eval { PVE::QemuConfig->remove_lock($vmid, 'create') };
1125 warn $@ if $@;
b973806e
TL
1126 if ($restored_data) {
1127 warn "error after data was restored, VM disks should be OK but config may "
1128 ."require adaptions. VM $vmid state is NOT cleaned up.\n";
1129 } else {
1130 warn "error before or during data restore, some or all disks were not "
1131 ."completely restored. VM $vmid state is NOT cleaned up.\n";
1132 }
5bf96183
WB
1133 die $err;
1134 }
1135 };
1136 } else {
1137 $worker_name = 'qmcreate';
1138 $code = sub {
1139 eval { $createfn->() };
1140 if (my $err = $@) {
1141 eval {
1142 my $conffile = PVE::QemuConfig->config_file($vmid);
f1e277cd 1143 unlink($conffile) or die "failed to remove config file: $!\n";
5bf96183
WB
1144 };
1145 warn $@ if $@;
1146 die $err;
1147 }
1148 };
1149 }
8ba8418c
TL
1150
1151 return $rpcenv->fork_worker($worker_name, $vmid, $authuser, $code);
1e3baf05
DM
1152 }});
1153
1154__PACKAGE__->register_method({
1155 name => 'vmdiridx',
afdb31d5 1156 path => '{vmid}',
1e3baf05
DM
1157 method => 'GET',
1158 proxyto => 'node',
1159 description => "Directory index",
a0d1b1a2
DM
1160 permissions => {
1161 user => 'all',
1162 },
1e3baf05 1163 parameters => {
3326ae19 1164 additionalProperties => 0,
1e3baf05
DM
1165 properties => {
1166 node => get_standard_option('pve-node'),
1167 vmid => get_standard_option('pve-vmid'),
1168 },
1169 },
1170 returns => {
1171 type => 'array',
1172 items => {
1173 type => "object",
1174 properties => {
1175 subdir => { type => 'string' },
1176 },
1177 },
1178 links => [ { rel => 'child', href => "{subdir}" } ],
1179 },
1180 code => sub {
1181 my ($param) = @_;
1182
1183 my $res = [
1184 { subdir => 'config' },
92f4be45 1185 { subdir => 'cloudinit' },
df2a2dbb 1186 { subdir => 'pending' },
1e3baf05
DM
1187 { subdir => 'status' },
1188 { subdir => 'unlink' },
1189 { subdir => 'vncproxy' },
87302002 1190 { subdir => 'termproxy' },
3ea94c60 1191 { subdir => 'migrate' },
2f48a4f5 1192 { subdir => 'resize' },
586bfa78 1193 { subdir => 'move' },
1e3baf05
DM
1194 { subdir => 'rrd' },
1195 { subdir => 'rrddata' },
91c94f0a 1196 { subdir => 'monitor' },
d1a47427 1197 { subdir => 'agent' },
7e7d7b61 1198 { subdir => 'snapshot' },
288eeea8 1199 { subdir => 'spiceproxy' },
7aa608d6 1200 { subdir => 'sendkey' },
228a998b 1201 { subdir => 'firewall' },
347dc136 1202 { subdir => 'mtunnel' },
06fedff6
FG
1203 { subdir => 'remote_migrate' },
1204 ];
afdb31d5 1205
1e3baf05
DM
1206 return $res;
1207 }});
1208
228a998b 1209__PACKAGE__->register_method ({
f34ebd52 1210 subclass => "PVE::API2::Firewall::VM",
228a998b
DM
1211 path => '{vmid}/firewall',
1212});
1213
b8158701
DC
1214__PACKAGE__->register_method ({
1215 subclass => "PVE::API2::Qemu::Agent",
1216 path => '{vmid}/agent',
1217});
1218
1e3baf05 1219__PACKAGE__->register_method({
afdb31d5
DM
1220 name => 'rrd',
1221 path => '{vmid}/rrd',
1e3baf05
DM
1222 method => 'GET',
1223 protected => 1, # fixme: can we avoid that?
1224 permissions => {
378b359e 1225 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1e3baf05
DM
1226 },
1227 description => "Read VM RRD statistics (returns PNG)",
1228 parameters => {
3326ae19 1229 additionalProperties => 0,
1e3baf05
DM
1230 properties => {
1231 node => get_standard_option('pve-node'),
1232 vmid => get_standard_option('pve-vmid'),
1233 timeframe => {
1234 description => "Specify the time frame you are interested in.",
1235 type => 'string',
1236 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
1237 },
1238 ds => {
1239 description => "The list of datasources you want to display.",
1240 type => 'string', format => 'pve-configid-list',
1241 },
1242 cf => {
1243 description => "The RRD consolidation function",
1244 type => 'string',
1245 enum => [ 'AVERAGE', 'MAX' ],
1246 optional => 1,
1247 },
1248 },
1249 },
1250 returns => {
1251 type => "object",
1252 properties => {
1253 filename => { type => 'string' },
1254 },
1255 },
1256 code => sub {
1257 my ($param) = @_;
1258
95896f80 1259 return PVE::RRD::create_rrd_graph(
afdb31d5 1260 "pve2-vm/$param->{vmid}", $param->{timeframe},
1e3baf05 1261 $param->{ds}, $param->{cf});
afdb31d5 1262
1e3baf05
DM
1263 }});
1264
1265__PACKAGE__->register_method({
afdb31d5
DM
1266 name => 'rrddata',
1267 path => '{vmid}/rrddata',
1e3baf05
DM
1268 method => 'GET',
1269 protected => 1, # fixme: can we avoid that?
1270 permissions => {
378b359e 1271 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1e3baf05
DM
1272 },
1273 description => "Read VM RRD statistics",
1274 parameters => {
3326ae19 1275 additionalProperties => 0,
1e3baf05
DM
1276 properties => {
1277 node => get_standard_option('pve-node'),
1278 vmid => get_standard_option('pve-vmid'),
1279 timeframe => {
1280 description => "Specify the time frame you are interested in.",
1281 type => 'string',
1282 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
1283 },
1284 cf => {
1285 description => "The RRD consolidation function",
1286 type => 'string',
1287 enum => [ 'AVERAGE', 'MAX' ],
1288 optional => 1,
1289 },
1290 },
1291 },
1292 returns => {
1293 type => "array",
1294 items => {
1295 type => "object",
1296 properties => {},
1297 },
1298 },
1299 code => sub {
1300 my ($param) = @_;
1301
95896f80 1302 return PVE::RRD::create_rrd_data(
1e3baf05
DM
1303 "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{cf});
1304 }});
1305
1306
1307__PACKAGE__->register_method({
afdb31d5
DM
1308 name => 'vm_config',
1309 path => '{vmid}/config',
1e3baf05
DM
1310 method => 'GET',
1311 proxyto => 'node',
86ea0ed0
FE
1312 description => "Get the virtual machine configuration with pending configuration " .
1313 "changes applied. Set the 'current' parameter to get the current configuration instead.",
a0d1b1a2
DM
1314 permissions => {
1315 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1316 },
1e3baf05 1317 parameters => {
3326ae19 1318 additionalProperties => 0,
1e3baf05
DM
1319 properties => {
1320 node => get_standard_option('pve-node'),
335af808 1321 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2a68ec78
TL
1322 current => {
1323 description => "Get current values (instead of pending values).",
1324 optional => 1,
6d89b548
DM
1325 default => 0,
1326 type => 'boolean',
2a68ec78 1327 },
b14477e7
RV
1328 snapshot => get_standard_option('pve-snapshot-name', {
1329 description => "Fetch config values from given snapshot.",
1330 optional => 1,
1331 completion => sub {
1332 my ($cmd, $pname, $cur, $args) = @_;
1333 PVE::QemuConfig->snapshot_list($args->[0]);
1334 },
1335 }),
1e3baf05
DM
1336 },
1337 },
afdb31d5 1338 returns => {
86ea0ed0 1339 description => "The VM configuration.",
1e3baf05 1340 type => "object",
ce9b0a38 1341 properties => PVE::QemuServer::json_config_properties({
554ac7e7
DM
1342 digest => {
1343 type => 'string',
1344 description => 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
1345 }
ce9b0a38 1346 }),
1e3baf05
DM
1347 },
1348 code => sub {
1349 my ($param) = @_;
1350
d3179e1c
OB
1351 raise_param_exc({ snapshot => "cannot use 'snapshot' parameter with 'current'",
1352 current => "cannot use 'snapshot' parameter with 'current'"})
1353 if ($param->{snapshot} && $param->{current});
1e3baf05 1354
d3179e1c
OB
1355 my $conf;
1356 if ($param->{snapshot}) {
1357 $conf = PVE::QemuConfig->load_snapshot_config($param->{vmid}, $param->{snapshot});
1358 } else {
1359 $conf = PVE::QemuConfig->load_current_config($param->{vmid}, $param->{current});
2254ffcf 1360 }
d3179e1c 1361 $conf->{cipassword} = '**********' if $conf->{cipassword};
1e3baf05 1362 return $conf;
d3179e1c 1363
1e3baf05
DM
1364 }});
1365
1e7f2726
DM
1366__PACKAGE__->register_method({
1367 name => 'vm_pending',
1368 path => '{vmid}/pending',
1369 method => 'GET',
1370 proxyto => 'node',
86ea0ed0 1371 description => "Get the virtual machine configuration with both current and pending values.",
1e7f2726
DM
1372 permissions => {
1373 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1374 },
1375 parameters => {
1376 additionalProperties => 0,
1377 properties => {
1378 node => get_standard_option('pve-node'),
335af808 1379 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1e7f2726
DM
1380 },
1381 },
1382 returns => {
1383 type => "array",
1384 items => {
1385 type => "object",
1386 properties => {
1387 key => {
1388 description => "Configuration option name.",
1389 type => 'string',
1390 },
1391 value => {
1392 description => "Current value.",
1393 type => 'string',
1394 optional => 1,
1395 },
1396 pending => {
1397 description => "Pending value.",
1398 type => 'string',
1399 optional => 1,
1400 },
1401 delete => {
1bc483f6
WB
1402 description => "Indicates a pending delete request if present and not 0. " .
1403 "The value 2 indicates a force-delete request.",
1404 type => 'integer',
1405 minimum => 0,
1406 maximum => 2,
1e7f2726
DM
1407 optional => 1,
1408 },
1409 },
1410 },
1411 },
1412 code => sub {
1413 my ($param) = @_;
1414
ffda963f 1415 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1e7f2726 1416
98bc3aeb 1417 my $pending_delete_hash = PVE::QemuConfig->parse_pending_delete($conf->{pending}->{delete});
1e7f2726 1418
59ef7003
OB
1419 $conf->{cipassword} = '**********' if defined($conf->{cipassword});
1420 $conf->{pending}->{cipassword} = '********** ' if defined($conf->{pending}->{cipassword});
1e7f2726 1421
69f2907c 1422 return PVE::GuestHelpers::config_with_pending_array($conf, $pending_delete_hash);
59ef7003 1423 }});
1e7f2726 1424
2be1fb0a
AD
1425__PACKAGE__->register_method({
1426 name => 'cloudinit_pending',
1427 path => '{vmid}/cloudinit',
1428 method => 'GET',
1429 proxyto => 'node',
1430 description => "Get the cloudinit configuration with both current and pending values.",
1431 permissions => {
1432 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1433 },
1434 parameters => {
1435 additionalProperties => 0,
1436 properties => {
1437 node => get_standard_option('pve-node'),
1438 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1439 },
1440 },
1441 returns => {
1442 type => "array",
1443 items => {
1444 type => "object",
1445 properties => {
1446 key => {
1447 description => "Configuration option name.",
1448 type => 'string',
1449 },
3a704639 1450 value => {
1b5706cd 1451 description => "Value as it was used to generate the current cloudinit image.",
2be1fb0a
AD
1452 type => 'string',
1453 optional => 1,
1454 },
3a704639 1455 pending => {
1b5706cd 1456 description => "The new pending value.",
2be1fb0a
AD
1457 type => 'string',
1458 optional => 1,
1459 },
3a704639
LN
1460 delete => {
1461 description => "Indicates a pending delete request if present and not 0. ",
1462 type => 'integer',
1463 minimum => 0,
1464 maximum => 1,
1465 optional => 1,
1466 },
2be1fb0a
AD
1467 },
1468 },
1469 },
1470 code => sub {
1471 my ($param) = @_;
1472
1473 my $vmid = $param->{vmid};
1474 my $conf = PVE::QemuConfig->load_config($vmid);
1475
1b5706cd
WB
1476 my $ci = $conf->{cloudinit};
1477
3a704639
LN
1478 $conf->{cipassword} = '**********' if exists $conf->{cipassword};
1479 $ci->{cipassword} = '**********' if exists $ci->{cipassword};
1480
1481 my $res = [];
1482
1483 # All the values that got added
1b5706cd
WB
1484 my $added = delete($ci->{added}) // '';
1485 for my $key (PVE::Tools::split_list($added)) {
3a704639 1486 push @$res, { key => $key, pending => $conf->{$key} };
2be1fb0a
AD
1487 }
1488
3a704639
LN
1489 # All already existing values (+ their new value, if it exists)
1490 for my $opt (keys %$cloudinitoptions) {
1491 next if !$conf->{$opt};
1492 next if $added =~ m/$opt/;
1493 my $item = {
1494 key => $opt,
1495 };
1496
1497 if (my $pending = $ci->{$opt}) {
1498 $item->{value} = $pending;
1499 $item->{pending} = $conf->{$opt};
1b5706cd 1500 } else {
3a704639 1501 $item->{value} = $conf->{$opt},
1b5706cd 1502 }
3a704639
LN
1503
1504 push @$res, $item;
1b5706cd 1505 }
2be1fb0a 1506
3a704639
LN
1507 # Now, we'll find the deleted ones
1508 for my $opt (keys %$ci) {
1509 next if $conf->{$opt};
1510 push @$res, { key => $opt, delete => 1 };
1b5706cd 1511 }
2be1fb0a
AD
1512
1513 return $res;
1514 }});
1515
9687287b
AD
1516__PACKAGE__->register_method({
1517 name => 'cloudinit_update',
1518 path => '{vmid}/cloudinit',
1519 method => 'PUT',
1520 protected => 1,
1521 proxyto => 'node',
1522 description => "Regenerate and change cloudinit config drive.",
1523 permissions => {
dafabbd0 1524 check => ['perm', '/vms/{vmid}', ['VM.Config.Cloudinit']],
9687287b
AD
1525 },
1526 parameters => {
1527 additionalProperties => 0,
1528 properties => {
1529 node => get_standard_option('pve-node'),
1530 vmid => get_standard_option('pve-vmid'),
1531 },
1532 },
1533 returns => { type => 'null' },
1534 code => sub {
1535 my ($param) = @_;
1536
1537 my $rpcenv = PVE::RPCEnvironment::get();
9687287b
AD
1538 my $authuser = $rpcenv->get_user();
1539
1540 my $vmid = extract_param($param, 'vmid');
1541
058fe7bc 1542 PVE::QemuConfig->lock_config($vmid, sub {
9687287b 1543 my $conf = PVE::QemuConfig->load_config($vmid);
9687287b
AD
1544 PVE::QemuConfig->check_lock($conf);
1545
1546 my $storecfg = PVE::Storage::config();
9687287b 1547 PVE::QemuServer::vmconfig_update_cloudinit_drive($storecfg, $conf, $vmid);
058fe7bc 1548 });
9687287b
AD
1549 return;
1550 }});
1551
5555edea
DM
1552# POST/PUT {vmid}/config implementation
1553#
1554# The original API used PUT (idempotent) an we assumed that all operations
1555# are fast. But it turned out that almost any configuration change can
1556# involve hot-plug actions, or disk alloc/free. Such actions can take long
1557# time to complete and have side effects (not idempotent).
1558#
7043d946 1559# The new implementation uses POST and forks a worker process. We added
5555edea 1560# a new option 'background_delay'. If specified we wait up to
7043d946 1561# 'background_delay' second for the worker task to complete. It returns null
5555edea 1562# if the task is finished within that time, else we return the UPID.
7043d946 1563
5555edea
DM
1564my $update_vm_api = sub {
1565 my ($param, $sync) = @_;
a0d1b1a2 1566
5555edea 1567 my $rpcenv = PVE::RPCEnvironment::get();
1e3baf05 1568
5555edea 1569 my $authuser = $rpcenv->get_user();
1e3baf05 1570
5555edea 1571 my $node = extract_param($param, 'node');
1e3baf05 1572
5555edea 1573 my $vmid = extract_param($param, 'vmid');
1e3baf05 1574
5555edea 1575 my $digest = extract_param($param, 'digest');
1e3baf05 1576
5555edea 1577 my $background_delay = extract_param($param, 'background_delay');
1e3baf05 1578
347dc136
FG
1579 my $skip_cloud_init = extract_param($param, 'skip_cloud_init');
1580
cefb41fa
WB
1581 if (defined(my $cipassword = $param->{cipassword})) {
1582 # Same logic as in cloud-init (but with the regex fixed...)
1583 $param->{cipassword} = PVE::Tools::encrypt_pw($cipassword)
1584 if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
1585 }
0c9a7596 1586
5555edea 1587 my @paramarr = (); # used for log message
edd48c32 1588 foreach my $key (sort keys %$param) {
cefb41fa
WB
1589 my $value = $key eq 'cipassword' ? '<hidden>' : $param->{$key};
1590 push @paramarr, "-$key", $value;
5555edea 1591 }
0532bc63 1592
5555edea
DM
1593 my $skiplock = extract_param($param, 'skiplock');
1594 raise_param_exc({ skiplock => "Only root may use this option." })
1595 if $skiplock && $authuser ne 'root@pam';
1e3baf05 1596
5555edea 1597 my $delete_str = extract_param($param, 'delete');
0532bc63 1598
d3df8cf3
DM
1599 my $revert_str = extract_param($param, 'revert');
1600
5555edea 1601 my $force = extract_param($param, 'force');
1e68cb19 1602
0c9a7596 1603 if (defined(my $ssh_keys = $param->{sshkeys})) {
231f824b
WB
1604 $ssh_keys = URI::Escape::uri_unescape($ssh_keys);
1605 PVE::Tools::validate_ssh_public_keys($ssh_keys);
0c9a7596
AD
1606 }
1607
8481721f 1608 $param->{cpuunits} = PVE::CGroup::clamp_cpu_shares($param->{cpuunits})
dbc45fdf
FE
1609 if defined($param->{cpuunits}); # clamp value depending on cgroup version
1610
d3df8cf3 1611 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
7bfdeb5f 1612
5555edea 1613 my $storecfg = PVE::Storage::config();
1e68cb19 1614
5555edea 1615 &$resolve_cdrom_alias($param);
0532bc63 1616
5555edea 1617 # now try to verify all parameters
ae57f6b3 1618
d3df8cf3
DM
1619 my $revert = {};
1620 foreach my $opt (PVE::Tools::split_list($revert_str)) {
1621 if (!PVE::QemuServer::option_exists($opt)) {
1622 raise_param_exc({ revert => "unknown option '$opt'" });
1623 }
1624
1625 raise_param_exc({ delete => "you can't use '-$opt' and " .
1626 "-revert $opt' at the same time" })
1627 if defined($param->{$opt});
1628
1629 $revert->{$opt} = 1;
1630 }
1631
5555edea
DM
1632 my @delete = ();
1633 foreach my $opt (PVE::Tools::split_list($delete_str)) {
1634 $opt = 'ide2' if $opt eq 'cdrom';
d3df8cf3 1635
5555edea
DM
1636 raise_param_exc({ delete => "you can't use '-$opt' and " .
1637 "-delete $opt' at the same time" })
1638 if defined($param->{$opt});
7043d946 1639
d3df8cf3
DM
1640 raise_param_exc({ revert => "you can't use '-delete $opt' and " .
1641 "-revert $opt' at the same time" })
1642 if $revert->{$opt};
1643
5555edea
DM
1644 if (!PVE::QemuServer::option_exists($opt)) {
1645 raise_param_exc({ delete => "unknown option '$opt'" });
0532bc63 1646 }
1e3baf05 1647
5555edea
DM
1648 push @delete, $opt;
1649 }
1650
17677004
WB
1651 my $repl_conf = PVE::ReplicationConfig->new();
1652 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
1653 my $check_replication = sub {
1654 my ($drive) = @_;
1655 return if !$is_replicated;
1656 my $volid = $drive->{file};
1657 return if !$volid || !($drive->{replicate}//1);
1658 return if PVE::QemuServer::drive_is_cdrom($drive);
21e1ee7b
ML
1659
1660 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
35171ddb
FG
1661 die "cannot add non-managed/pass-through volume to a replicated VM\n"
1662 if !defined($storeid);
1663
e3d31944 1664 return if defined($volname) && $volname eq 'cloudinit';
21e1ee7b
ML
1665
1666 my $format;
028aee45 1667 if ($volid =~ $PVE::QemuServer::Drive::NEW_DISK_RE) {
17677004
WB
1668 $storeid = $2;
1669 $format = $drive->{format} || PVE::Storage::storage_default_format($storecfg, $storeid);
1670 } else {
17677004
WB
1671 $format = (PVE::Storage::parse_volname($storecfg, $volid))[6];
1672 }
1673 return if PVE::Storage::storage_can_replicate($storecfg, $storeid, $format);
9b1396ed
WB
1674 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
1675 return if $scfg->{shared};
17677004
WB
1676 die "cannot add non-replicatable volume to a replicated VM\n";
1677 };
1678
7979bbcd
FE
1679 $check_drive_param->($param, $storecfg, $check_replication);
1680
5555edea 1681 foreach my $opt (keys %$param) {
7979bbcd 1682 if ($opt =~ m/^net(\d+)$/) {
5555edea
DM
1683 # add macaddr
1684 my $net = PVE::QemuServer::parse_net($param->{$opt});
1685 $param->{$opt} = PVE::QemuServer::print_net($net);
6ee499ff
DC
1686 } elsif ($opt eq 'vmgenid') {
1687 if ($param->{$opt} eq '1') {
1688 $param->{$opt} = PVE::QemuServer::generate_uuid();
1689 }
9e784b11
DC
1690 } elsif ($opt eq 'hookscript') {
1691 eval { PVE::GuestHelpers::check_hookscript($param->{$opt}, $storecfg); };
1692 raise_param_exc({ $opt => $@ }) if $@;
1e68cb19 1693 }
5555edea 1694 }
1e3baf05 1695
5555edea 1696 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
ae57f6b3 1697
e30f75c5 1698 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
ae57f6b3 1699
5555edea 1700 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
1e3baf05 1701
d6deb7f6 1702 PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $param);
9cfd06d9 1703
5555edea 1704 my $updatefn = sub {
1e3baf05 1705
ffda963f 1706 my $conf = PVE::QemuConfig->load_config($vmid);
1e3baf05 1707
5555edea
DM
1708 die "checksum missmatch (file change by other user?)\n"
1709 if $digest && $digest ne $conf->{digest};
1710
6e72f90b
SR
1711 &$check_cpu_model_access($rpcenv, $authuser, $param, $conf);
1712
546644e2
TL
1713 # FIXME: 'suspended' lock should probabyl be a state or "weak" lock?!
1714 if (scalar(@delete) && grep { $_ eq 'vmstate'} @delete) {
1715 if (defined($conf->{lock}) && $conf->{lock} eq 'suspended') {
1716 delete $conf->{lock}; # for check lock check, not written out
1717 push @delete, 'lock'; # this is the real deal to write it out
1718 }
1719 push @delete, 'runningmachine' if $conf->{runningmachine};
ea1c2110 1720 push @delete, 'runningcpu' if $conf->{runningcpu};
546644e2
TL
1721 }
1722
ffda963f 1723 PVE::QemuConfig->check_lock($conf) if !$skiplock;
7043d946 1724
d3df8cf3
DM
1725 foreach my $opt (keys %$revert) {
1726 if (defined($conf->{$opt})) {
1727 $param->{$opt} = $conf->{$opt};
1728 } elsif (defined($conf->{pending}->{$opt})) {
1729 push @delete, $opt;
1730 }
1731 }
1732
5555edea 1733 if ($param->{memory} || defined($param->{balloon})) {
7f8c8087
AD
1734
1735 my $memory = $param->{memory} || $conf->{pending}->{memory} || $conf->{memory};
1736 my $maxmem = get_current_memory($memory);
6ca8b698 1737 my $balloon = defined($param->{balloon}) ? $param->{balloon} : $conf->{pending}->{balloon} || $conf->{balloon};
7043d946 1738
5555edea
DM
1739 die "balloon value too large (must be smaller than assigned memory)\n"
1740 if $balloon && $balloon > $maxmem;
1741 }
1e3baf05 1742
5555edea 1743 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1e3baf05 1744
5555edea 1745 my $worker = sub {
7bfdeb5f 1746
5555edea 1747 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
c2a64aa7 1748
202d1f45
DM
1749 # write updates to pending section
1750
3a11fadb
DM
1751 my $modified = {}; # record what $option we modify
1752
11c601e9
TL
1753 my @bootorder;
1754 if (my $boot = $conf->{boot}) {
1755 my $bootcfg = PVE::JSONSchema::parse_property_string('pve-qm-boot', $boot);
1756 @bootorder = PVE::Tools::split_list($bootcfg->{order}) if $bootcfg && $bootcfg->{order};
1757 }
078c109f
SR
1758 my $bootorder_deleted = grep {$_ eq 'bootorder'} @delete;
1759
bb660bc3
DC
1760 my $check_drive_perms = sub {
1761 my ($opt, $val) = @_;
c1accf9d 1762 my $drive = PVE::QemuServer::parse_drive($opt, $val, 1);
f5a88e98
LN
1763 if (PVE::QemuServer::drive_is_cloudinit($drive)) {
1764 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Cloudinit', 'VM.Config.CDROM']);
1765 } elsif (PVE::QemuServer::drive_is_cdrom($drive, 1)) { # CDROM
bb660bc3
DC
1766 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1767 } else {
1768 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
f5a88e98 1769
bb660bc3
DC
1770 }
1771 };
1772
202d1f45 1773 foreach my $opt (@delete) {
3a11fadb 1774 $modified->{$opt} = 1;
ffda963f 1775 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
f70a6ea9
TL
1776
1777 # value of what we want to delete, independent if pending or not
1778 my $val = $conf->{$opt} // $conf->{pending}->{$opt};
1779 if (!defined($val)) {
d2c6bf93
FG
1780 warn "cannot delete '$opt' - not set in current configuration!\n";
1781 $modified->{$opt} = 0;
1782 next;
1783 }
f70a6ea9 1784 my $is_pending_val = defined($conf->{pending}->{$opt});
6aa43f92 1785 delete $conf->{pending}->{$opt};
d2c6bf93 1786
078c109f
SR
1787 # remove from bootorder if necessary
1788 if (!$bootorder_deleted && @bootorder && grep {$_ eq $opt} @bootorder) {
1789 @bootorder = grep {$_ ne $opt} @bootorder;
1790 $conf->{pending}->{boot} = PVE::QemuServer::print_bootorder(\@bootorder);
1791 $modified->{boot} = 1;
1792 }
1793
202d1f45 1794 if ($opt =~ m/^unused/) {
f70a6ea9 1795 my $drive = PVE::QemuServer::parse_drive($opt, $val);
ffda963f 1796 PVE::QemuConfig->check_protection($conf, "can't remove unused disk '$drive->{file}'");
4d8d55f1 1797 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
3dc38fbb
WB
1798 if (PVE::QemuServer::try_deallocate_drive($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1799 delete $conf->{$opt};
ffda963f 1800 PVE::QemuConfig->write_config($vmid, $conf);
202d1f45 1801 }
6afb6794
DC
1802 } elsif ($opt eq 'vmstate') {
1803 PVE::QemuConfig->check_protection($conf, "can't remove vmstate '$val'");
6afb6794
DC
1804 if (PVE::QemuServer::try_deallocate_drive($storecfg, $vmid, $conf, $opt, { file => $val }, $rpcenv, $authuser, 1)) {
1805 delete $conf->{$opt};
1806 PVE::QemuConfig->write_config($vmid, $conf);
1807 }
74479ee9 1808 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
ffda963f 1809 PVE::QemuConfig->check_protection($conf, "can't remove drive '$opt'");
bb660bc3 1810 $check_drive_perms->($opt, $val);
f70a6ea9
TL
1811 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $val))
1812 if $is_pending_val;
98bc3aeb 1813 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
ffda963f 1814 PVE::QemuConfig->write_config($vmid, $conf);
e30f75c5 1815 } elsif ($opt =~ m/^serial\d+$/) {
f70a6ea9 1816 if ($val eq 'socket') {
e5453043
DC
1817 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1818 } elsif ($authuser ne 'root@pam') {
1819 die "only root can delete '$opt' config for real devices\n";
1820 }
98bc3aeb 1821 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
e5453043 1822 PVE::QemuConfig->write_config($vmid, $conf);
165be267 1823 } elsif ($opt =~ m/^usb\d+$/) {
0cf8d56c 1824 check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
98bc3aeb 1825 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
165be267 1826 PVE::QemuConfig->write_config($vmid, $conf);
9b71c34d
DC
1827 } elsif ($opt =~ m/^hostpci\d+$/) {
1828 check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
1829 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
1830 PVE::QemuConfig->write_config($vmid, $conf);
97c76039
DC
1831 } elsif ($opt eq 'tags') {
1832 assert_tag_permissions($vmid, $val, '', $rpcenv, $authuser);
1833 delete $conf->{$opt};
1834 PVE::QemuConfig->write_config($vmid, $conf);
de5af2d3
FE
1835 } elsif ($opt =~ m/^net\d+$/) {
1836 if ($conf->{$opt}) {
1837 PVE::QemuServer::check_bridge_access(
b704d825
FG
1838 $rpcenv,
1839 $authuser,
1840 { $opt => $conf->{$opt} },
1841 );
de5af2d3
FE
1842 }
1843 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
1844 PVE::QemuConfig->write_config($vmid, $conf);
202d1f45 1845 } else {
98bc3aeb 1846 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
ffda963f 1847 PVE::QemuConfig->write_config($vmid, $conf);
202d1f45 1848 }
5d39a182 1849 }
1e3baf05 1850
202d1f45 1851 foreach my $opt (keys %$param) { # add/change
3a11fadb 1852 $modified->{$opt} = 1;
ffda963f 1853 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
202d1f45
DM
1854 next if defined($conf->{pending}->{$opt}) && ($param->{$opt} eq $conf->{pending}->{$opt}); # skip if nothing changed
1855
de64f101 1856 my $arch = PVE::QemuServer::get_vm_arch($conf);
96ed3574 1857
74479ee9 1858 if (PVE::QemuServer::is_valid_drivename($opt)) {
2c44ec49
DC
1859 # old drive
1860 if ($conf->{$opt}) {
1861 $check_drive_perms->($opt, $conf->{$opt});
1862 }
1863
1864 # new drive
bb660bc3 1865 $check_drive_perms->($opt, $param->{$opt});
055d554d 1866 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
202d1f45
DM
1867 if defined($conf->{pending}->{$opt});
1868
d1feab4a
HD
1869 assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt})
1870 if $opt =~ m/^scsi\d+$/;
1871
367e6bf4
FE
1872 my (undef, $created_opts) = $create_disks->(
1873 $rpcenv,
1874 $authuser,
1875 $conf,
1876 $arch,
1877 $storecfg,
1878 $vmid,
1879 undef,
1880 {$opt => $param->{$opt}},
1881 );
1882 $conf->{pending}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
deb734e3 1883
a2e22f9f
DC
1884 # default legacy boot order implies all cdroms anyway
1885 if (@bootorder) {
1886 # append new CD drives to bootorder to mark them bootable
c1accf9d 1887 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}, 1);
a2e22f9f
DC
1888 if (PVE::QemuServer::drive_is_cdrom($drive, 1) && !grep(/^$opt$/, @bootorder)) {
1889 push @bootorder, $opt;
1890 $conf->{pending}->{boot} = PVE::QemuServer::print_bootorder(\@bootorder);
1891 $modified->{boot} = 1;
1892 }
deb734e3 1893 }
e30f75c5
DC
1894 } elsif ($opt =~ m/^serial\d+/) {
1895 if ((!defined($conf->{$opt}) || $conf->{$opt} eq 'socket') && $param->{$opt} eq 'socket') {
1896 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1897 } elsif ($authuser ne 'root@pam') {
1898 die "only root can modify '$opt' config for real devices\n";
1899 }
1900 $conf->{pending}->{$opt} = $param->{$opt};
b62997a1
MF
1901 } elsif ($opt eq 'vga') {
1902 my $vga = PVE::QemuServer::parse_vga($param->{$opt});
1903 PVE::QemuServer::assert_clipboard_config($vga);
1904 $conf->{pending}->{$opt} = $param->{$opt};
165be267 1905 } elsif ($opt =~ m/^usb\d+/) {
0cf8d56c
DC
1906 if (my $olddevice = $conf->{$opt}) {
1907 check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $conf->{$opt});
165be267 1908 }
0cf8d56c 1909 check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
165be267 1910 $conf->{pending}->{$opt} = $param->{$opt};
9b71c34d
DC
1911 } elsif ($opt =~ m/^hostpci\d+$/) {
1912 if (my $oldvalue = $conf->{$opt}) {
1913 check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
1914 }
1915 check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
1916 $conf->{pending}->{$opt} = $param->{$opt};
97c76039
DC
1917 } elsif ($opt eq 'tags') {
1918 assert_tag_permissions($vmid, $conf->{$opt}, $param->{$opt}, $rpcenv, $authuser);
db22080c 1919 $conf->{pending}->{$opt} = PVE::GuestHelpers::get_unique_tags($param->{$opt});
de5af2d3
FE
1920 } elsif ($opt =~ m/^net\d+$/) {
1921 if ($conf->{$opt}) {
1922 PVE::QemuServer::check_bridge_access(
b704d825
FG
1923 $rpcenv,
1924 $authuser,
1925 { $opt => $conf->{$opt} },
1926 );
de5af2d3
FE
1927 }
1928 $conf->{pending}->{$opt} = $param->{$opt};
202d1f45
DM
1929 } else {
1930 $conf->{pending}->{$opt} = $param->{$opt};
078c109f
SR
1931
1932 if ($opt eq 'boot') {
1933 my $new_bootcfg = PVE::JSONSchema::parse_property_string('pve-qm-boot', $param->{$opt});
1934 if ($new_bootcfg->{order}) {
1935 my @devs = PVE::Tools::split_list($new_bootcfg->{order});
1936 for my $dev (@devs) {
c9c32c1b 1937 my $exists = $conf->{$dev} || $conf->{pending}->{$dev} || $param->{$dev};
078c109f
SR
1938 my $deleted = grep {$_ eq $dev} @delete;
1939 die "invalid bootorder: device '$dev' does not exist'\n"
1940 if !$exists || $deleted;
1941 }
1942
1943 # remove legacy boot order settings if new one set
1944 $conf->{pending}->{$opt} = PVE::QemuServer::print_bootorder(\@devs);
1945 PVE::QemuConfig->add_to_pending_delete($conf, "bootdisk")
1946 if $conf->{bootdisk};
1947 }
1948 }
202d1f45 1949 }
98bc3aeb 1950 PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
ffda963f 1951 PVE::QemuConfig->write_config($vmid, $conf);
202d1f45
DM
1952 }
1953
1954 # remove pending changes when nothing changed
ffda963f 1955 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
98bc3aeb 1956 my $changes = PVE::QemuConfig->cleanup_pending($conf);
ffda963f 1957 PVE::QemuConfig->write_config($vmid, $conf) if $changes;
202d1f45
DM
1958
1959 return if !scalar(keys %{$conf->{pending}});
1960
7bfdeb5f 1961 my $running = PVE::QemuServer::check_running($vmid);
39001640
DM
1962
1963 # apply pending changes
1964
ffda963f 1965 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
39001640 1966
eb5e482d 1967 my $errors = {};
3a11fadb 1968 if ($running) {
3a11fadb 1969 PVE::QemuServer::vmconfig_hotplug_pending($vmid, $conf, $storecfg, $modified, $errors);
3a11fadb 1970 } else {
347dc136
FG
1971 # cloud_init must be skipped if we are in an incoming, remote live migration
1972 PVE::QemuServer::vmconfig_apply_pending($vmid, $conf, $storecfg, $errors, $skip_cloud_init);
3a11fadb 1973 }
eb5e482d 1974 raise_param_exc($errors) if scalar(keys %$errors);
1e68cb19 1975
915d3481 1976 return;
5d39a182
DM
1977 };
1978
5555edea
DM
1979 if ($sync) {
1980 &$worker();
d1c1af4b 1981 return;
5555edea
DM
1982 } else {
1983 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
fcdb0117 1984
5555edea
DM
1985 if ($background_delay) {
1986
1987 # Note: It would be better to do that in the Event based HTTPServer
7043d946 1988 # to avoid blocking call to sleep.
5555edea
DM
1989
1990 my $end_time = time() + $background_delay;
1991
1992 my $task = PVE::Tools::upid_decode($upid);
1993
1994 my $running = 1;
1995 while (time() < $end_time) {
1996 $running = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
1997 last if !$running;
1998 sleep(1); # this gets interrupted when child process ends
1999 }
2000
2001 if (!$running) {
2002 my $status = PVE::Tools::upid_read_status($upid);
872cfcf5 2003 return if !PVE::Tools::upid_status_is_error($status);
ce3fbcd4 2004 die "failed to update VM $vmid: $status\n";
5555edea 2005 }
7043d946 2006 }
5555edea
DM
2007
2008 return $upid;
2009 }
2010 };
2011
ffda963f 2012 return PVE::QemuConfig->lock_config($vmid, $updatefn);
5555edea
DM
2013};
2014
2015my $vm_config_perm_list = [
2016 'VM.Config.Disk',
2017 'VM.Config.CDROM',
2018 'VM.Config.CPU',
2019 'VM.Config.Memory',
2020 'VM.Config.Network',
2021 'VM.Config.HWType',
2022 'VM.Config.Options',
fc701af7 2023 'VM.Config.Cloudinit',
5555edea
DM
2024 ];
2025
2026__PACKAGE__->register_method({
2027 name => 'update_vm_async',
2028 path => '{vmid}/config',
2029 method => 'POST',
2030 protected => 1,
2031 proxyto => 'node',
2032 description => "Set virtual machine options (asynchrounous API).",
2033 permissions => {
2034 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
2035 },
2036 parameters => {
3326ae19 2037 additionalProperties => 0,
5555edea
DM
2038 properties => PVE::QemuServer::json_config_properties(
2039 {
2040 node => get_standard_option('pve-node'),
2041 vmid => get_standard_option('pve-vmid'),
2042 skiplock => get_standard_option('skiplock'),
2043 delete => {
2044 type => 'string', format => 'pve-configid-list',
2045 description => "A list of settings you want to delete.",
2046 optional => 1,
2047 },
4c8365fa
DM
2048 revert => {
2049 type => 'string', format => 'pve-configid-list',
2050 description => "Revert a pending change.",
2051 optional => 1,
2052 },
5555edea
DM
2053 force => {
2054 type => 'boolean',
2055 description => $opt_force_description,
2056 optional => 1,
2057 requires => 'delete',
2058 },
2059 digest => {
2060 type => 'string',
2061 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2062 maxLength => 40,
2063 optional => 1,
2064 },
2065 background_delay => {
2066 type => 'integer',
2067 description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
2068 minimum => 1,
2069 maximum => 30,
2070 optional => 1,
2071 },
c1accf9d
FE
2072 },
2073 1, # with_disk_alloc
2074 ),
5555edea
DM
2075 },
2076 returns => {
2077 type => 'string',
2078 optional => 1,
2079 },
2080 code => $update_vm_api,
2081});
2082
2083__PACKAGE__->register_method({
2084 name => 'update_vm',
2085 path => '{vmid}/config',
2086 method => 'PUT',
2087 protected => 1,
2088 proxyto => 'node',
2089 description => "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
2090 permissions => {
2091 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
2092 },
2093 parameters => {
3326ae19 2094 additionalProperties => 0,
5555edea
DM
2095 properties => PVE::QemuServer::json_config_properties(
2096 {
2097 node => get_standard_option('pve-node'),
335af808 2098 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
5555edea
DM
2099 skiplock => get_standard_option('skiplock'),
2100 delete => {
2101 type => 'string', format => 'pve-configid-list',
2102 description => "A list of settings you want to delete.",
2103 optional => 1,
2104 },
4c8365fa
DM
2105 revert => {
2106 type => 'string', format => 'pve-configid-list',
2107 description => "Revert a pending change.",
2108 optional => 1,
2109 },
5555edea
DM
2110 force => {
2111 type => 'boolean',
2112 description => $opt_force_description,
2113 optional => 1,
2114 requires => 'delete',
2115 },
2116 digest => {
2117 type => 'string',
2118 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
2119 maxLength => 40,
2120 optional => 1,
2121 },
c1accf9d
FE
2122 },
2123 1, # with_disk_alloc
2124 ),
5555edea
DM
2125 },
2126 returns => { type => 'null' },
2127 code => sub {
2128 my ($param) = @_;
2129 &$update_vm_api($param, 1);
d1c1af4b 2130 return;
5555edea
DM
2131 }
2132});
1e3baf05 2133
1e3baf05 2134__PACKAGE__->register_method({
afdb31d5
DM
2135 name => 'destroy_vm',
2136 path => '{vmid}',
1e3baf05
DM
2137 method => 'DELETE',
2138 protected => 1,
2139 proxyto => 'node',
e00319af
TL
2140 description => "Destroy the VM and all used/owned volumes. Removes any VM specific permissions"
2141 ." and firewall rules",
a0d1b1a2
DM
2142 permissions => {
2143 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
2144 },
1e3baf05 2145 parameters => {
3326ae19 2146 additionalProperties => 0,
1e3baf05
DM
2147 properties => {
2148 node => get_standard_option('pve-node'),
335af808 2149 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
3ea94c60 2150 skiplock => get_standard_option('skiplock'),
d9123ef5
CE
2151 purge => {
2152 type => 'boolean',
e00319af 2153 description => "Remove VMID from configurations, like backup & replication jobs and HA.",
d9123ef5
CE
2154 optional => 1,
2155 },
75854662
TL
2156 'destroy-unreferenced-disks' => {
2157 type => 'boolean',
99676a6c
TL
2158 description => "If set, destroy additionally all disks not referenced in the config"
2159 ." but with a matching VMID from all enabled storages.",
75854662 2160 optional => 1,
16e66777 2161 default => 0,
47f9f50b 2162 },
1e3baf05
DM
2163 },
2164 },
afdb31d5 2165 returns => {
5fdbe4f0
DM
2166 type => 'string',
2167 },
1e3baf05
DM
2168 code => sub {
2169 my ($param) = @_;
2170
2171 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 2172 my $authuser = $rpcenv->get_user();
1e3baf05
DM
2173 my $vmid = $param->{vmid};
2174
2175 my $skiplock = $param->{skiplock};
afdb31d5 2176 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2177 if $skiplock && $authuser ne 'root@pam';
1e3baf05 2178
a4c7029d
FG
2179 my $early_checks = sub {
2180 # test if VM exists
2181 my $conf = PVE::QemuConfig->load_config($vmid);
2182 PVE::QemuConfig->check_protection($conf, "can't remove VM $vmid");
7c4351f7 2183
a4c7029d 2184 my $ha_managed = PVE::HA::Config::service_is_configured("vm:$vmid");
e9f2f8e5 2185
a4c7029d
FG
2186 if (!$param->{purge}) {
2187 die "unable to remove VM $vmid - used in HA resources and purge parameter not set.\n"
2188 if $ha_managed;
2189 # don't allow destroy if with replication jobs but no purge param
2190 my $repl_conf = PVE::ReplicationConfig->new();
2191 $repl_conf->check_for_existing_jobs($vmid);
2192 }
628bb7f2 2193
a4c7029d
FG
2194 die "VM $vmid is running - destroy failed\n"
2195 if PVE::QemuServer::check_running($vmid);
2196
2197 return $ha_managed;
2198 };
2199
2200 $early_checks->();
db593da2 2201
5fdbe4f0 2202 my $realcmd = sub {
ff1a2432
DM
2203 my $upid = shift;
2204
a4c7029d
FG
2205 my $storecfg = PVE::Storage::config();
2206
ff1a2432 2207 syslog('info', "destroy VM $vmid: $upid\n");
3e8e214d 2208 PVE::QemuConfig->lock_config($vmid, sub {
a4c7029d
FG
2209 # repeat, config might have changed
2210 my $ha_managed = $early_checks->();
d9123ef5 2211
16e66777 2212 my $purge_unreferenced = $param->{'destroy-unreferenced-disks'};
75854662
TL
2213
2214 PVE::QemuServer::destroy_vm(
2215 $storecfg,
2216 $vmid,
2217 $skiplock, { lock => 'destroyed' },
2218 $purge_unreferenced,
2219 );
d9123ef5 2220
3e8e214d
DJ
2221 PVE::AccessControl::remove_vm_access($vmid);
2222 PVE::Firewall::remove_vmfw_conf($vmid);
d9123ef5 2223 if ($param->{purge}) {
7c4351f7 2224 print "purging VM $vmid from related configurations..\n";
d9123ef5
CE
2225 PVE::ReplicationConfig::remove_vmid_jobs($vmid);
2226 PVE::VZDump::Plugin::remove_vmid_from_backup_jobs($vmid);
7c4351f7
TL
2227
2228 if ($ha_managed) {
2229 PVE::HA::Config::delete_service_from_config("vm:$vmid");
2230 print "NOTE: removed VM $vmid from HA resource configuration.\n";
2231 }
d9123ef5 2232 }
5172770d
TL
2233
2234 # only now remove the zombie config, else we can have reuse race
2235 PVE::QemuConfig->destroy_config($vmid);
3e8e214d 2236 });
5fdbe4f0 2237 };
1e3baf05 2238
a0d1b1a2 2239 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
1e3baf05
DM
2240 }});
2241
2242__PACKAGE__->register_method({
afdb31d5
DM
2243 name => 'unlink',
2244 path => '{vmid}/unlink',
1e3baf05
DM
2245 method => 'PUT',
2246 protected => 1,
2247 proxyto => 'node',
2248 description => "Unlink/delete disk images.",
a0d1b1a2
DM
2249 permissions => {
2250 check => [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
2251 },
1e3baf05 2252 parameters => {
3326ae19 2253 additionalProperties => 0,
1e3baf05
DM
2254 properties => {
2255 node => get_standard_option('pve-node'),
335af808 2256 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1e3baf05
DM
2257 idlist => {
2258 type => 'string', format => 'pve-configid-list',
2259 description => "A list of disk IDs you want to delete.",
2260 },
2261 force => {
2262 type => 'boolean',
2263 description => $opt_force_description,
2264 optional => 1,
2265 },
2266 },
2267 },
2268 returns => { type => 'null'},
2269 code => sub {
2270 my ($param) = @_;
2271
2272 $param->{delete} = extract_param($param, 'idlist');
2273
2274 __PACKAGE__->update_vm($param);
2275
d1c1af4b 2276 return;
1e3baf05
DM
2277 }});
2278
3c5bdde8
TL
2279# uses good entropy, each char is limited to 6 bit to get printable chars simply
2280my $gen_rand_chars = sub {
2281 my ($length) = @_;
2282
2283 die "invalid length $length" if $length < 1;
2284
2285 my $min = ord('!'); # first printable ascii
a4e128a9
FG
2286
2287 my $rand_bytes = Crypt::OpenSSL::Random::random_bytes($length);
2288 die "failed to generate random bytes!\n"
2289 if !$rand_bytes;
2290
2291 my $str = join('', map { chr((ord($_) & 0x3F) + $min) } split('', $rand_bytes));
3c5bdde8
TL
2292
2293 return $str;
2294};
2295
1e3baf05
DM
2296my $sslcert;
2297
2298__PACKAGE__->register_method({
afdb31d5
DM
2299 name => 'vncproxy',
2300 path => '{vmid}/vncproxy',
1e3baf05
DM
2301 method => 'POST',
2302 protected => 1,
2303 permissions => {
378b359e 2304 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1e3baf05
DM
2305 },
2306 description => "Creates a TCP VNC proxy connections.",
2307 parameters => {
3326ae19 2308 additionalProperties => 0,
1e3baf05
DM
2309 properties => {
2310 node => get_standard_option('pve-node'),
2311 vmid => get_standard_option('pve-vmid'),
b4d5c000
SP
2312 websocket => {
2313 optional => 1,
2314 type => 'boolean',
62c19049
FE
2315 description => "Prepare for websocket upgrade (only required when using "
2316 ."serial terminal, otherwise upgrade is always possible).",
b4d5c000 2317 },
3c5bdde8
TL
2318 'generate-password' => {
2319 optional => 1,
2320 type => 'boolean',
2321 default => 0,
2322 description => "Generates a random password to be used as ticket instead of the API ticket.",
2323 },
1e3baf05
DM
2324 },
2325 },
afdb31d5 2326 returns => {
3326ae19 2327 additionalProperties => 0,
1e3baf05
DM
2328 properties => {
2329 user => { type => 'string' },
2330 ticket => { type => 'string' },
3c5bdde8
TL
2331 password => {
2332 optional => 1,
2333 description => "Returned if requested with 'generate-password' param."
2334 ." Consists of printable ASCII characters ('!' .. '~').",
2335 type => 'string',
2336 },
1e3baf05
DM
2337 cert => { type => 'string' },
2338 port => { type => 'integer' },
2339 upid => { type => 'string' },
2340 },
2341 },
2342 code => sub {
2343 my ($param) = @_;
2344
2345 my $rpcenv = PVE::RPCEnvironment::get();
2346
a0d1b1a2 2347 my $authuser = $rpcenv->get_user();
1e3baf05
DM
2348
2349 my $vmid = $param->{vmid};
2350 my $node = $param->{node};
983d4582 2351 my $websocket = $param->{websocket};
1e3baf05 2352
ffda963f 2353 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
326007b2 2354
d3efae29
FG
2355 my $serial;
2356 if ($conf->{vga}) {
2357 my $vga = PVE::QemuServer::parse_vga($conf->{vga});
2358 $serial = $vga->{type} if $vga->{type} =~ m/^serial\d+$/;
2359 }
ef5e2be2 2360
b6f39da2
DM
2361 my $authpath = "/vms/$vmid";
2362
a0d1b1a2 2363 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
3c5bdde8
TL
2364 my $password = $ticket;
2365 if ($param->{'generate-password'}) {
2366 $password = $gen_rand_chars->(8);
2367 }
b6f39da2 2368
1e3baf05
DM
2369 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
2370 if !$sslcert;
2371
414b42d8 2372 my $family;
ef5e2be2 2373 my $remcmd = [];
afdb31d5 2374
4f1be36c 2375 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
414b42d8 2376 (undef, $family) = PVE::Cluster::remote_node_ip($node);
f42ea29b 2377 my $sshinfo = PVE::SSHInfo::get_ssh_info($node);
b4d5c000 2378 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
d3efae29 2379 $remcmd = PVE::SSHInfo::ssh_info_to_command($sshinfo, defined($serial) ? '-t' : '-T');
af0eba7e
WB
2380 } else {
2381 $family = PVE::Tools::get_host_address_family($node);
1e3baf05
DM
2382 }
2383
af0eba7e
WB
2384 my $port = PVE::Tools::next_vnc_port($family);
2385
afdb31d5 2386 my $timeout = 10;
1e3baf05
DM
2387
2388 my $realcmd = sub {
2389 my $upid = shift;
2390
2391 syslog('info', "starting vnc proxy $upid\n");
2392
ef5e2be2 2393 my $cmd;
1e3baf05 2394
d3efae29 2395 if (defined($serial)) {
b4d5c000 2396
d3efae29 2397 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $serial, '-escape', '0' ];
9e6d6e97 2398
ef5e2be2 2399 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
fa8ea931 2400 '-timeout', $timeout, '-authpath', $authpath,
9e6d6e97
DC
2401 '-perm', 'Sys.Console'];
2402
2403 if ($param->{websocket}) {
3c5bdde8 2404 $ENV{PVE_VNC_TICKET} = $password; # pass ticket to vncterm
9e6d6e97
DC
2405 push @$cmd, '-notls', '-listen', 'localhost';
2406 }
2407
2408 push @$cmd, '-c', @$remcmd, @$termcmd;
2409
655d7462 2410 PVE::Tools::run_command($cmd);
9e6d6e97 2411
ef5e2be2 2412 } else {
1e3baf05 2413
62c19049 2414 $ENV{LC_PVE_TICKET} = $password; # set ticket with "qm vncproxy"
3e7567e0 2415
655d7462
WB
2416 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
2417
2418 my $sock = IO::Socket::IP->new(
dd32a466 2419 ReuseAddr => 1,
655d7462
WB
2420 Listen => 1,
2421 LocalPort => $port,
2422 Proto => 'tcp',
2423 GetAddrInfoFlags => 0,
b63f34b8 2424 ) or die "failed to create socket: $!\n";
655d7462
WB
2425 # Inside the worker we shouldn't have any previous alarms
2426 # running anyway...:
2427 alarm(0);
2428 local $SIG{ALRM} = sub { die "connection timed out\n" };
2429 alarm $timeout;
2430 accept(my $cli, $sock) or die "connection failed: $!\n";
058ff55b 2431 alarm(0);
655d7462
WB
2432 close($sock);
2433 if (PVE::Tools::run_command($cmd,
2434 output => '>&'.fileno($cli),
2435 input => '<&'.fileno($cli),
2436 noerr => 1) != 0)
2437 {
2438 die "Failed to run vncproxy.\n";
2439 }
ef5e2be2 2440 }
1e3baf05 2441
1e3baf05
DM
2442 return;
2443 };
2444
2c7fc947 2445 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
1e3baf05 2446
3da85107
DM
2447 PVE::Tools::wait_for_vnc_port($port);
2448
3c5bdde8 2449 my $res = {
a0d1b1a2 2450 user => $authuser,
1e3baf05 2451 ticket => $ticket,
afdb31d5
DM
2452 port => $port,
2453 upid => $upid,
2454 cert => $sslcert,
1e3baf05 2455 };
3c5bdde8
TL
2456 $res->{password} = $password if $param->{'generate-password'};
2457
2458 return $res;
1e3baf05
DM
2459 }});
2460
87302002
DC
2461__PACKAGE__->register_method({
2462 name => 'termproxy',
2463 path => '{vmid}/termproxy',
2464 method => 'POST',
2465 protected => 1,
2466 permissions => {
2467 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2468 },
2469 description => "Creates a TCP proxy connections.",
2470 parameters => {
2471 additionalProperties => 0,
2472 properties => {
2473 node => get_standard_option('pve-node'),
2474 vmid => get_standard_option('pve-vmid'),
2475 serial=> {
2476 optional => 1,
2477 type => 'string',
2478 enum => [qw(serial0 serial1 serial2 serial3)],
2479 description => "opens a serial terminal (defaults to display)",
2480 },
2481 },
2482 },
2483 returns => {
2484 additionalProperties => 0,
2485 properties => {
2486 user => { type => 'string' },
2487 ticket => { type => 'string' },
2488 port => { type => 'integer' },
2489 upid => { type => 'string' },
2490 },
2491 },
2492 code => sub {
2493 my ($param) = @_;
2494
2495 my $rpcenv = PVE::RPCEnvironment::get();
2496
2497 my $authuser = $rpcenv->get_user();
2498
2499 my $vmid = $param->{vmid};
2500 my $node = $param->{node};
2501 my $serial = $param->{serial};
2502
2503 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
2504
2505 if (!defined($serial)) {
d7856be5
FG
2506 if ($conf->{vga}) {
2507 my $vga = PVE::QemuServer::parse_vga($conf->{vga});
2508 $serial = $vga->{type} if $vga->{type} =~ m/^serial\d+$/;
87302002
DC
2509 }
2510 }
2511
2512 my $authpath = "/vms/$vmid";
2513
2514 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
2515
414b42d8
DC
2516 my $family;
2517 my $remcmd = [];
87302002
DC
2518
2519 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
414b42d8 2520 (undef, $family) = PVE::Cluster::remote_node_ip($node);
f42ea29b
FG
2521 my $sshinfo = PVE::SSHInfo::get_ssh_info($node);
2522 $remcmd = PVE::SSHInfo::ssh_info_to_command($sshinfo, '-t');
414b42d8 2523 push @$remcmd, '--';
87302002
DC
2524 } else {
2525 $family = PVE::Tools::get_host_address_family($node);
2526 }
2527
2528 my $port = PVE::Tools::next_vnc_port($family);
2529
ccb88f45 2530 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0'];
87302002
DC
2531 push @$termcmd, '-iface', $serial if $serial;
2532
2533 my $realcmd = sub {
2534 my $upid = shift;
2535
2536 syslog('info', "starting qemu termproxy $upid\n");
2537
2538 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
2539 '--perm', 'VM.Console', '--'];
2540 push @$cmd, @$remcmd, @$termcmd;
2541
2542 PVE::Tools::run_command($cmd);
2543 };
2544
2545 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2546
2547 PVE::Tools::wait_for_vnc_port($port);
2548
2549 return {
2550 user => $authuser,
2551 ticket => $ticket,
2552 port => $port,
2553 upid => $upid,
2554 };
2555 }});
2556
3e7567e0
DM
2557__PACKAGE__->register_method({
2558 name => 'vncwebsocket',
2559 path => '{vmid}/vncwebsocket',
2560 method => 'GET',
3e7567e0 2561 permissions => {
c422ce93 2562 description => "You also need to pass a valid ticket (vncticket).",
3e7567e0
DM
2563 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2564 },
4d00f52f 2565 description => "Opens a weksocket for VNC traffic.",
3e7567e0 2566 parameters => {
3326ae19 2567 additionalProperties => 0,
3e7567e0
DM
2568 properties => {
2569 node => get_standard_option('pve-node'),
2570 vmid => get_standard_option('pve-vmid'),
c422ce93
DM
2571 vncticket => {
2572 description => "Ticket from previous call to vncproxy.",
2573 type => 'string',
2574 maxLength => 512,
2575 },
3e7567e0
DM
2576 port => {
2577 description => "Port number returned by previous vncproxy call.",
2578 type => 'integer',
2579 minimum => 5900,
2580 maximum => 5999,
2581 },
2582 },
2583 },
2584 returns => {
2585 type => "object",
2586 properties => {
2587 port => { type => 'string' },
2588 },
2589 },
2590 code => sub {
2591 my ($param) = @_;
2592
2593 my $rpcenv = PVE::RPCEnvironment::get();
2594
2595 my $authuser = $rpcenv->get_user();
2596
2597 my $vmid = $param->{vmid};
2598 my $node = $param->{node};
2599
c422ce93
DM
2600 my $authpath = "/vms/$vmid";
2601
2602 PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $authuser, $authpath);
2603
ffda963f 2604 my $conf = PVE::QemuConfig->load_config($vmid, $node); # VM exists ?
3e7567e0
DM
2605
2606 # Note: VNC ports are acessible from outside, so we do not gain any
2607 # security if we verify that $param->{port} belongs to VM $vmid. This
2608 # check is done by verifying the VNC ticket (inside VNC protocol).
2609
2610 my $port = $param->{port};
f34ebd52 2611
3e7567e0
DM
2612 return { port => $port };
2613 }});
2614
288eeea8
DM
2615__PACKAGE__->register_method({
2616 name => 'spiceproxy',
2617 path => '{vmid}/spiceproxy',
78252ce7 2618 method => 'POST',
288eeea8 2619 protected => 1,
78252ce7 2620 proxyto => 'node',
288eeea8
DM
2621 permissions => {
2622 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2623 },
2624 description => "Returns a SPICE configuration to connect to the VM.",
2625 parameters => {
3326ae19 2626 additionalProperties => 0,
288eeea8
DM
2627 properties => {
2628 node => get_standard_option('pve-node'),
2629 vmid => get_standard_option('pve-vmid'),
dd25eecf 2630 proxy => get_standard_option('spice-proxy', { optional => 1 }),
288eeea8
DM
2631 },
2632 },
dd25eecf 2633 returns => get_standard_option('remote-viewer-config'),
288eeea8
DM
2634 code => sub {
2635 my ($param) = @_;
2636
2637 my $rpcenv = PVE::RPCEnvironment::get();
2638
2639 my $authuser = $rpcenv->get_user();
2640
2641 my $vmid = $param->{vmid};
2642 my $node = $param->{node};
fb6c7260 2643 my $proxy = $param->{proxy};
288eeea8 2644
ffda963f 2645 my $conf = PVE::QemuConfig->load_config($vmid, $node);
7f9e28e4
TL
2646 my $title = "VM $vmid";
2647 $title .= " - ". $conf->{name} if $conf->{name};
288eeea8 2648
943340a6 2649 my $port = PVE::QemuServer::spice_port($vmid);
dd25eecf 2650
f34ebd52 2651 my ($ticket, undef, $remote_viewer_config) =
dd25eecf 2652 PVE::AccessControl::remote_viewer_config($authuser, $vmid, $node, $proxy, $title, $port);
f34ebd52 2653
0a13e08e
SR
2654 mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket);
2655 mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30");
f34ebd52 2656
dd25eecf 2657 return $remote_viewer_config;
288eeea8
DM
2658 }});
2659
5fdbe4f0
DM
2660__PACKAGE__->register_method({
2661 name => 'vmcmdidx',
afdb31d5 2662 path => '{vmid}/status',
5fdbe4f0
DM
2663 method => 'GET',
2664 proxyto => 'node',
2665 description => "Directory index",
a0d1b1a2
DM
2666 permissions => {
2667 user => 'all',
2668 },
5fdbe4f0 2669 parameters => {
3326ae19 2670 additionalProperties => 0,
5fdbe4f0
DM
2671 properties => {
2672 node => get_standard_option('pve-node'),
2673 vmid => get_standard_option('pve-vmid'),
2674 },
2675 },
2676 returns => {
2677 type => 'array',
2678 items => {
2679 type => "object",
2680 properties => {
2681 subdir => { type => 'string' },
2682 },
2683 },
2684 links => [ { rel => 'child', href => "{subdir}" } ],
2685 },
2686 code => sub {
2687 my ($param) = @_;
2688
2689 # test if VM exists
ffda963f 2690 my $conf = PVE::QemuConfig->load_config($param->{vmid});
5fdbe4f0
DM
2691
2692 my $res = [
2693 { subdir => 'current' },
2694 { subdir => 'start' },
2695 { subdir => 'stop' },
58f9db6a
DC
2696 { subdir => 'reset' },
2697 { subdir => 'shutdown' },
2698 { subdir => 'suspend' },
165411f0 2699 { subdir => 'reboot' },
5fdbe4f0 2700 ];
afdb31d5 2701
5fdbe4f0
DM
2702 return $res;
2703 }});
2704
1e3baf05 2705__PACKAGE__->register_method({
afdb31d5 2706 name => 'vm_status',
5fdbe4f0 2707 path => '{vmid}/status/current',
1e3baf05
DM
2708 method => 'GET',
2709 proxyto => 'node',
2710 protected => 1, # qemu pid files are only readable by root
2711 description => "Get virtual machine status.",
a0d1b1a2
DM
2712 permissions => {
2713 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2714 },
1e3baf05 2715 parameters => {
3326ae19 2716 additionalProperties => 0,
1e3baf05
DM
2717 properties => {
2718 node => get_standard_option('pve-node'),
2719 vmid => get_standard_option('pve-vmid'),
2720 },
2721 },
b1a70cab
DM
2722 returns => {
2723 type => 'object',
2724 properties => {
2725 %$PVE::QemuServer::vmstatus_return_properties,
2726 ha => {
2727 description => "HA manager service status.",
2728 type => 'object',
2729 },
2730 spice => {
7bd9abd2 2731 description => "QEMU VGA configuration supports spice.",
b1a70cab
DM
2732 type => 'boolean',
2733 optional => 1,
2734 },
2735 agent => {
7bd9abd2 2736 description => "QEMU Guest Agent is enabled in config.",
b1a70cab
DM
2737 type => 'boolean',
2738 optional => 1,
2739 },
bfd45955
MF
2740 clipboard => {
2741 description => 'Enable a specific clipboard. If not set, depending on'
2742 .' the display type the SPICE one will be added.',
2743 type => 'string',
2744 enum => ['vnc'],
2745 optional => 1,
2746 },
b1a70cab
DM
2747 },
2748 },
1e3baf05
DM
2749 code => sub {
2750 my ($param) = @_;
2751
2752 # test if VM exists
ffda963f 2753 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1e3baf05 2754
03a33f30 2755 my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid}, 1);
8610701a 2756 my $status = $vmstatus->{$param->{vmid}};
1e3baf05 2757
4d2a734e 2758 $status->{ha} = PVE::HA::Config::get_service_status("vm:$param->{vmid}");
8610701a 2759
3591b62b
TL
2760 if ($conf->{vga}) {
2761 my $vga = PVE::QemuServer::parse_vga($conf->{vga});
e88ceeca
FG
2762 my $spice = defined($vga->{type}) && $vga->{type} =~ /^virtio/;
2763 $spice ||= PVE::QemuServer::vga_conf_has_spice($conf->{vga});
2764 $status->{spice} = 1 if $spice;
bfd45955 2765 $status->{clipboard} = $vga->{clipboard};
3591b62b 2766 }
a2af1bbe 2767 $status->{agent} = 1 if PVE::QemuServer::get_qga_key($conf, 'enabled');
c9a074b8 2768
8610701a 2769 return $status;
1e3baf05
DM
2770 }});
2771
2772__PACKAGE__->register_method({
afdb31d5 2773 name => 'vm_start',
5fdbe4f0
DM
2774 path => '{vmid}/status/start',
2775 method => 'POST',
1e3baf05
DM
2776 protected => 1,
2777 proxyto => 'node',
5fdbe4f0 2778 description => "Start virtual machine.",
a0d1b1a2
DM
2779 permissions => {
2780 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2781 },
1e3baf05 2782 parameters => {
3326ae19 2783 additionalProperties => 0,
1e3baf05
DM
2784 properties => {
2785 node => get_standard_option('pve-node'),
ab5904f7
TL
2786 vmid => get_standard_option('pve-vmid',
2787 { completion => \&PVE::QemuServer::complete_vmid_stopped }),
3ea94c60
DM
2788 skiplock => get_standard_option('skiplock'),
2789 stateuri => get_standard_option('pve-qm-stateuri'),
7e8dcf2c 2790 migratedfrom => get_standard_option('pve-node',{ optional => 1 }),
2de2d6f7
TL
2791 migration_type => {
2792 type => 'string',
2793 enum => ['secure', 'insecure'],
2794 description => "Migration traffic is encrypted using an SSH " .
2795 "tunnel by default. On secure, completely private networks " .
2796 "this can be disabled to increase performance.",
2797 optional => 1,
2798 },
2799 migration_network => {
29ddbe70 2800 type => 'string', format => 'CIDR',
2de2d6f7
TL
2801 description => "CIDR of the (sub) network that is used for migration.",
2802 optional => 1,
2803 },
d58b93a8 2804 machine => get_standard_option('pve-qemu-machine'),
58c64ad5
SR
2805 'force-cpu' => {
2806 description => "Override QEMU's -cpu argument with the given string.",
2807 type => 'string',
2808 optional => 1,
2809 },
bf8fc5a3 2810 targetstorage => get_standard_option('pve-targetstorage'),
ef3f4293
TM
2811 timeout => {
2812 description => "Wait maximal timeout seconds.",
2813 type => 'integer',
2814 minimum => 0,
5a7f7b99 2815 default => 'max(30, vm memory in GiB)',
ef3f4293
TM
2816 optional => 1,
2817 },
1e3baf05
DM
2818 },
2819 },
afdb31d5 2820 returns => {
5fdbe4f0
DM
2821 type => 'string',
2822 },
1e3baf05
DM
2823 code => sub {
2824 my ($param) = @_;
2825
2826 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 2827 my $authuser = $rpcenv->get_user();
1e3baf05
DM
2828
2829 my $node = extract_param($param, 'node');
1e3baf05 2830 my $vmid = extract_param($param, 'vmid');
ef3f4293 2831 my $timeout = extract_param($param, 'timeout');
952958bc
DM
2832 my $machine = extract_param($param, 'machine');
2833
736c92f6
TL
2834 my $get_root_param = sub {
2835 my $value = extract_param($param, $_[0]);
2836 raise_param_exc({ "$_[0]" => "Only root may use this option." })
2837 if $value && $authuser ne 'root@pam';
2838 return $value;
2839 };
2de2d6f7 2840
736c92f6
TL
2841 my $stateuri = $get_root_param->('stateuri');
2842 my $skiplock = $get_root_param->('skiplock');
2843 my $migratedfrom = $get_root_param->('migratedfrom');
2844 my $migration_type = $get_root_param->('migration_type');
2845 my $migration_network = $get_root_param->('migration_network');
2846 my $targetstorage = $get_root_param->('targetstorage');
6ab41628 2847 my $force_cpu = $get_root_param->('force-cpu');
2189246c 2848
bf8fc5a3
FG
2849 my $storagemap;
2850
2851 if ($targetstorage) {
2852 raise_param_exc({ targetstorage => "targetstorage can only by used with migratedfrom." })
2853 if !$migratedfrom;
2854 $storagemap = eval { PVE::JSONSchema::parse_idmap($targetstorage, 'pve-storage-id') };
e214cda8 2855 raise_param_exc({ targetstorage => "failed to parse storage map: $@" })
bf8fc5a3
FG
2856 if $@;
2857 }
2189246c 2858
7c14dcae
DM
2859 # read spice ticket from STDIN
2860 my $spice_ticket;
c4ac8f71 2861 my $nbd_protocol_version = 0;
88126be3 2862 my $replicated_volumes = {};
13d121d7 2863 my $offline_volumes = {};
ccab68c2 2864 if ($stateuri && ($stateuri eq 'tcp' || $stateuri eq 'unix') && $migratedfrom && ($rpcenv->{type} eq 'cli')) {
c4ac8f71 2865 while (defined(my $line = <STDIN>)) {
760fb3c8 2866 chomp $line;
c4ac8f71
ML
2867 if ($line =~ m/^spice_ticket: (.+)$/) {
2868 $spice_ticket = $1;
2869 } elsif ($line =~ m/^nbd_protocol_version: (\d+)$/) {
2870 $nbd_protocol_version = $1;
88126be3
FG
2871 } elsif ($line =~ m/^replicated_volume: (.*)$/) {
2872 $replicated_volumes->{$1} = 1;
13d121d7
FE
2873 } elsif ($line =~ m/^tpmstate0: (.*)$/) { # Deprecated, use offline_volume instead
2874 $offline_volumes->{tpmstate0} = $1;
2875 } elsif ($line =~ m/^offline_volume: ([^:]+): (.*)$/) {
2876 $offline_volumes->{$1} = $2;
399ca0d6 2877 } elsif (!$spice_ticket) {
c4ac8f71
ML
2878 # fallback for old source node
2879 $spice_ticket = $line;
399ca0d6
FG
2880 } else {
2881 warn "unknown 'start' parameter on STDIN: '$line'\n";
c4ac8f71 2882 }
760fb3c8 2883 }
7c14dcae
DM
2884 }
2885
98cbd0f4
WB
2886 PVE::Cluster::check_cfs_quorum();
2887
afdb31d5 2888 my $storecfg = PVE::Storage::config();
5fdbe4f0 2889
a4262553 2890 if (PVE::HA::Config::vm_is_ha_managed($vmid) && !$stateuri && $rpcenv->{type} ne 'ha') {
88fc87b4
DM
2891 my $hacmd = sub {
2892 my $upid = shift;
5fdbe4f0 2893
02765844 2894 print "Requesting HA start for VM $vmid\n";
88fc87b4 2895
a4262553 2896 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'started'];
88fc87b4 2897 PVE::Tools::run_command($cmd);
88fc87b4
DM
2898 return;
2899 };
2900
2901 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
2902
2903 } else {
2904
2905 my $realcmd = sub {
2906 my $upid = shift;
2907
2908 syslog('info', "start VM $vmid: $upid\n");
2909
0c498cca
FG
2910 my $migrate_opts = {
2911 migratedfrom => $migratedfrom,
2912 spice_ticket => $spice_ticket,
2913 network => $migration_network,
2914 type => $migration_type,
bf8fc5a3 2915 storagemap => $storagemap,
0c498cca
FG
2916 nbd_proto_version => $nbd_protocol_version,
2917 replicated_volumes => $replicated_volumes,
13d121d7 2918 offline_volumes => $offline_volumes,
0c498cca
FG
2919 };
2920
2921 my $params = {
2922 statefile => $stateuri,
2923 skiplock => $skiplock,
2924 forcemachine => $machine,
2925 timeout => $timeout,
58c64ad5 2926 forcecpu => $force_cpu,
0c498cca
FG
2927 };
2928
2929 PVE::QemuServer::vm_start($storecfg, $vmid, $params, $migrate_opts);
88fc87b4
DM
2930 return;
2931 };
5fdbe4f0 2932
88fc87b4
DM
2933 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
2934 }
5fdbe4f0
DM
2935 }});
2936
2937__PACKAGE__->register_method({
afdb31d5 2938 name => 'vm_stop',
5fdbe4f0
DM
2939 path => '{vmid}/status/stop',
2940 method => 'POST',
2941 protected => 1,
2942 proxyto => 'node',
346130b2 2943 description => "Stop virtual machine. The qemu process will exit immediately. This" .
d6c747ff 2944 "is akin to pulling the power plug of a running computer and may damage the VM data",
a0d1b1a2
DM
2945 permissions => {
2946 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2947 },
5fdbe4f0 2948 parameters => {
3326ae19 2949 additionalProperties => 0,
5fdbe4f0
DM
2950 properties => {
2951 node => get_standard_option('pve-node'),
ab5904f7
TL
2952 vmid => get_standard_option('pve-vmid',
2953 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 2954 skiplock => get_standard_option('skiplock'),
debe8882 2955 migratedfrom => get_standard_option('pve-node', { optional => 1 }),
c6bb9502
DM
2956 timeout => {
2957 description => "Wait maximal timeout seconds.",
2958 type => 'integer',
2959 minimum => 0,
2960 optional => 1,
254575e9
DM
2961 },
2962 keepActive => {
94a17e1d 2963 description => "Do not deactivate storage volumes.",
254575e9
DM
2964 type => 'boolean',
2965 optional => 1,
2966 default => 0,
c6bb9502 2967 }
5fdbe4f0
DM
2968 },
2969 },
afdb31d5 2970 returns => {
5fdbe4f0
DM
2971 type => 'string',
2972 },
2973 code => sub {
2974 my ($param) = @_;
2975
2976 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 2977 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2978
2979 my $node = extract_param($param, 'node');
5fdbe4f0
DM
2980 my $vmid = extract_param($param, 'vmid');
2981
2982 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2983 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2984 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2985
254575e9 2986 my $keepActive = extract_param($param, 'keepActive');
afdb31d5 2987 raise_param_exc({ keepActive => "Only root may use this option." })
a0d1b1a2 2988 if $keepActive && $authuser ne 'root@pam';
254575e9 2989
af30308f
DM
2990 my $migratedfrom = extract_param($param, 'migratedfrom');
2991 raise_param_exc({ migratedfrom => "Only root may use this option." })
2992 if $migratedfrom && $authuser ne 'root@pam';
2993
2994
ff1a2432
DM
2995 my $storecfg = PVE::Storage::config();
2996
2003f0f8 2997 if (PVE::HA::Config::vm_is_ha_managed($vmid) && ($rpcenv->{type} ne 'ha') && !defined($migratedfrom)) {
5fdbe4f0 2998
88fc87b4
DM
2999 my $hacmd = sub {
3000 my $upid = shift;
5fdbe4f0 3001
02765844 3002 print "Requesting HA stop for VM $vmid\n";
88fc87b4 3003
1805fac3 3004 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", '0'];
88fc87b4 3005 PVE::Tools::run_command($cmd);
88fc87b4
DM
3006 return;
3007 };
3008
3009 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3010
3011 } else {
3012 my $realcmd = sub {
3013 my $upid = shift;
3014
3015 syslog('info', "stop VM $vmid: $upid\n");
3016
3017 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0,
af30308f 3018 $param->{timeout}, 0, 1, $keepActive, $migratedfrom);
88fc87b4
DM
3019 return;
3020 };
3021
3022 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
3023 }
5fdbe4f0
DM
3024 }});
3025
3026__PACKAGE__->register_method({
afdb31d5 3027 name => 'vm_reset',
5fdbe4f0
DM
3028 path => '{vmid}/status/reset',
3029 method => 'POST',
3030 protected => 1,
3031 proxyto => 'node',
3032 description => "Reset virtual machine.",
a0d1b1a2
DM
3033 permissions => {
3034 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3035 },
5fdbe4f0 3036 parameters => {
3326ae19 3037 additionalProperties => 0,
5fdbe4f0
DM
3038 properties => {
3039 node => get_standard_option('pve-node'),
ab5904f7
TL
3040 vmid => get_standard_option('pve-vmid',
3041 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0
DM
3042 skiplock => get_standard_option('skiplock'),
3043 },
3044 },
afdb31d5 3045 returns => {
5fdbe4f0
DM
3046 type => 'string',
3047 },
3048 code => sub {
3049 my ($param) = @_;
3050
3051 my $rpcenv = PVE::RPCEnvironment::get();
3052
a0d1b1a2 3053 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
3054
3055 my $node = extract_param($param, 'node');
3056
3057 my $vmid = extract_param($param, 'vmid');
3058
3059 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 3060 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 3061 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 3062
ff1a2432
DM
3063 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
3064
5fdbe4f0
DM
3065 my $realcmd = sub {
3066 my $upid = shift;
3067
1e3baf05 3068 PVE::QemuServer::vm_reset($vmid, $skiplock);
5fdbe4f0
DM
3069
3070 return;
3071 };
3072
a0d1b1a2 3073 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
5fdbe4f0
DM
3074 }});
3075
3076__PACKAGE__->register_method({
afdb31d5 3077 name => 'vm_shutdown',
5fdbe4f0
DM
3078 path => '{vmid}/status/shutdown',
3079 method => 'POST',
3080 protected => 1,
3081 proxyto => 'node',
d6c747ff
EK
3082 description => "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
3083 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
a0d1b1a2
DM
3084 permissions => {
3085 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3086 },
5fdbe4f0 3087 parameters => {
3326ae19 3088 additionalProperties => 0,
5fdbe4f0
DM
3089 properties => {
3090 node => get_standard_option('pve-node'),
ab5904f7
TL
3091 vmid => get_standard_option('pve-vmid',
3092 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 3093 skiplock => get_standard_option('skiplock'),
c6bb9502
DM
3094 timeout => {
3095 description => "Wait maximal timeout seconds.",
3096 type => 'integer',
3097 minimum => 0,
3098 optional => 1,
9269013a
DM
3099 },
3100 forceStop => {
3101 description => "Make sure the VM stops.",
3102 type => 'boolean',
3103 optional => 1,
3104 default => 0,
254575e9
DM
3105 },
3106 keepActive => {
94a17e1d 3107 description => "Do not deactivate storage volumes.",
254575e9
DM
3108 type => 'boolean',
3109 optional => 1,
3110 default => 0,
c6bb9502 3111 }
5fdbe4f0
DM
3112 },
3113 },
afdb31d5 3114 returns => {
5fdbe4f0
DM
3115 type => 'string',
3116 },
3117 code => sub {
3118 my ($param) = @_;
3119
3120 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 3121 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
3122
3123 my $node = extract_param($param, 'node');
5fdbe4f0
DM
3124 my $vmid = extract_param($param, 'vmid');
3125
3126 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 3127 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 3128 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 3129
254575e9 3130 my $keepActive = extract_param($param, 'keepActive');
afdb31d5 3131 raise_param_exc({ keepActive => "Only root may use this option." })
a0d1b1a2 3132 if $keepActive && $authuser ne 'root@pam';
254575e9 3133
02d07cf5
DM
3134 my $storecfg = PVE::Storage::config();
3135
89897367
DC
3136 my $shutdown = 1;
3137
263c803d
TL
3138 # sending a graceful shutdown command to paused VMs runs into timeouts, and even worse, when
3139 # the VM gets resumed later, it still gets the request delivered and powers off
6f0627d4 3140 if (PVE::QemuServer::vm_is_paused($vmid, 1)) {
89897367
DC
3141 if ($param->{forceStop}) {
3142 warn "VM is paused - stop instead of shutdown\n";
3143 $shutdown = 0;
3144 } else {
3145 die "VM is paused - cannot shutdown\n";
3146 }
3147 }
3148
a4262553 3149 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
5fdbe4f0 3150
1805fac3 3151 my $timeout = $param->{timeout} // 60;
ae849692
DM
3152 my $hacmd = sub {
3153 my $upid = shift;
5fdbe4f0 3154
02765844 3155 print "Requesting HA stop for VM $vmid\n";
ae849692 3156
1805fac3 3157 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", "$timeout"];
ae849692 3158 PVE::Tools::run_command($cmd);
ae849692
DM
3159 return;
3160 };
3161
3162 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3163
3164 } else {
3165
3166 my $realcmd = sub {
3167 my $upid = shift;
3168
3169 syslog('info', "shutdown VM $vmid: $upid\n");
5fdbe4f0 3170
ae849692
DM
3171 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout},
3172 $shutdown, $param->{forceStop}, $keepActive);
ae849692
DM
3173 return;
3174 };
3175
3176 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
3177 }
5fdbe4f0
DM
3178 }});
3179
165411f0
DC
3180__PACKAGE__->register_method({
3181 name => 'vm_reboot',
3182 path => '{vmid}/status/reboot',
3183 method => 'POST',
3184 protected => 1,
3185 proxyto => 'node',
3186 description => "Reboot the VM by shutting it down, and starting it again. Applies pending changes.",
3187 permissions => {
3188 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3189 },
3190 parameters => {
3191 additionalProperties => 0,
3192 properties => {
3193 node => get_standard_option('pve-node'),
3194 vmid => get_standard_option('pve-vmid',
3195 { completion => \&PVE::QemuServer::complete_vmid_running }),
3196 timeout => {
3197 description => "Wait maximal timeout seconds for the shutdown.",
3198 type => 'integer',
3199 minimum => 0,
3200 optional => 1,
3201 },
3202 },
3203 },
3204 returns => {
3205 type => 'string',
3206 },
3207 code => sub {
3208 my ($param) = @_;
3209
3210 my $rpcenv = PVE::RPCEnvironment::get();
3211 my $authuser = $rpcenv->get_user();
3212
3213 my $node = extract_param($param, 'node');
3214 my $vmid = extract_param($param, 'vmid');
3215
6f0627d4 3216 die "VM is paused - cannot shutdown\n" if PVE::QemuServer::vm_is_paused($vmid, 1);
165411f0
DC
3217
3218 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
3219
3220 my $realcmd = sub {
3221 my $upid = shift;
3222
3223 syslog('info', "requesting reboot of VM $vmid: $upid\n");
3224 PVE::QemuServer::vm_reboot($vmid, $param->{timeout});
3225 return;
3226 };
3227
3228 return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd);
3229 }});
3230
5fdbe4f0 3231__PACKAGE__->register_method({
afdb31d5 3232 name => 'vm_suspend',
5fdbe4f0
DM
3233 path => '{vmid}/status/suspend',
3234 method => 'POST',
3235 protected => 1,
3236 proxyto => 'node',
3237 description => "Suspend virtual machine.",
a0d1b1a2 3238 permissions => {
75c24bba
DC
3239 description => "You need 'VM.PowerMgmt' on /vms/{vmid}, and if you have set 'todisk',".
3240 " you need also 'VM.Config.Disk' on /vms/{vmid} and 'Datastore.AllocateSpace'".
3241 " on the storage for the vmstate.",
a0d1b1a2
DM
3242 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3243 },
5fdbe4f0 3244 parameters => {
3326ae19 3245 additionalProperties => 0,
5fdbe4f0
DM
3246 properties => {
3247 node => get_standard_option('pve-node'),
ab5904f7
TL
3248 vmid => get_standard_option('pve-vmid',
3249 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 3250 skiplock => get_standard_option('skiplock'),
22371fe0
DC
3251 todisk => {
3252 type => 'boolean',
3253 default => 0,
3254 optional => 1,
3255 description => 'If set, suspends the VM to disk. Will be resumed on next VM start.',
3256 },
48b4cdc2
DC
3257 statestorage => get_standard_option('pve-storage-id', {
3258 description => "The storage for the VM state",
3259 requires => 'todisk',
3260 optional => 1,
3261 completion => \&PVE::Storage::complete_storage_enabled,
3262 }),
5fdbe4f0
DM
3263 },
3264 },
afdb31d5 3265 returns => {
5fdbe4f0
DM
3266 type => 'string',
3267 },
3268 code => sub {
3269 my ($param) = @_;
3270
3271 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 3272 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
3273
3274 my $node = extract_param($param, 'node');
5fdbe4f0
DM
3275 my $vmid = extract_param($param, 'vmid');
3276
22371fe0
DC
3277 my $todisk = extract_param($param, 'todisk') // 0;
3278
48b4cdc2
DC
3279 my $statestorage = extract_param($param, 'statestorage');
3280
5fdbe4f0 3281 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 3282 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 3283 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 3284
ff1a2432
DM
3285 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
3286
22371fe0
DC
3287 die "Cannot suspend HA managed VM to disk\n"
3288 if $todisk && PVE::HA::Config::vm_is_ha_managed($vmid);
3289
75c24bba
DC
3290 # early check for storage permission, for better user feedback
3291 if ($todisk) {
3292 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
227a298f
DC
3293 my $conf = PVE::QemuConfig->load_config($vmid);
3294
876b24f2 3295 # cannot save the state of a non-virtualized PCIe device, so resume cannot really work
227a298f
DC
3296 for my $key (keys %$conf) {
3297 next if $key !~ /^hostpci\d+/;
161c2dde
TL
3298 die "cannot suspend VM to disk due to passed-through PCI device(s), which lack the"
3299 ." possibility to save/restore their internal state\n";
227a298f 3300 }
75c24bba
DC
3301
3302 if (!$statestorage) {
3303 # get statestorage from config if none is given
75c24bba
DC
3304 my $storecfg = PVE::Storage::config();
3305 $statestorage = PVE::QemuServer::find_vmstate_storage($conf, $storecfg);
3306 }
3307
3308 $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
3309 }
3310
5fdbe4f0
DM
3311 my $realcmd = sub {
3312 my $upid = shift;
3313
3314 syslog('info', "suspend VM $vmid: $upid\n");
3315
48b4cdc2 3316 PVE::QemuServer::vm_suspend($vmid, $skiplock, $todisk, $statestorage);
5fdbe4f0
DM
3317
3318 return;
3319 };
3320
a4262553 3321 my $taskname = $todisk ? 'qmsuspend' : 'qmpause';
f17fb184 3322 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
5fdbe4f0
DM
3323 }});
3324
3325__PACKAGE__->register_method({
afdb31d5 3326 name => 'vm_resume',
5fdbe4f0
DM
3327 path => '{vmid}/status/resume',
3328 method => 'POST',
3329 protected => 1,
3330 proxyto => 'node',
3331 description => "Resume virtual machine.",
a0d1b1a2
DM
3332 permissions => {
3333 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3334 },
5fdbe4f0 3335 parameters => {
3326ae19 3336 additionalProperties => 0,
5fdbe4f0
DM
3337 properties => {
3338 node => get_standard_option('pve-node'),
ab5904f7
TL
3339 vmid => get_standard_option('pve-vmid',
3340 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 3341 skiplock => get_standard_option('skiplock'),
289e0b85
AD
3342 nocheck => { type => 'boolean', optional => 1 },
3343
5fdbe4f0
DM
3344 },
3345 },
afdb31d5 3346 returns => {
5fdbe4f0
DM
3347 type => 'string',
3348 },
3349 code => sub {
3350 my ($param) = @_;
3351
3352 my $rpcenv = PVE::RPCEnvironment::get();
3353
a0d1b1a2 3354 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
3355
3356 my $node = extract_param($param, 'node');
3357
3358 my $vmid = extract_param($param, 'vmid');
3359
3360 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 3361 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 3362 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 3363
a20dc58a
FG
3364 # nocheck is used as part of migration when config file might be still
3365 # be on source node
289e0b85 3366 my $nocheck = extract_param($param, 'nocheck');
4fb85adc
FG
3367 raise_param_exc({ nocheck => "Only root may use this option." })
3368 if $nocheck && $authuser ne 'root@pam';
289e0b85 3369
cd9a035b
TL
3370 my $to_disk_suspended;
3371 eval {
3372 PVE::QemuConfig->lock_config($vmid, sub {
3373 my $conf = PVE::QemuConfig->load_config($vmid);
3374 $to_disk_suspended = PVE::QemuConfig->has_lock($conf, 'suspended');
3375 });
3376 };
3377
3378 die "VM $vmid not running\n"
3379 if !$to_disk_suspended && !PVE::QemuServer::check_running($vmid, $nocheck);
ff1a2432 3380
5fdbe4f0
DM
3381 my $realcmd = sub {
3382 my $upid = shift;
3383
3384 syslog('info', "resume VM $vmid: $upid\n");
3385
cd9a035b
TL
3386 if (!$to_disk_suspended) {
3387 PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck);
3388 } else {
3389 my $storecfg = PVE::Storage::config();
0c498cca 3390 PVE::QemuServer::vm_start($storecfg, $vmid, { skiplock => $skiplock });
cd9a035b 3391 }
1e3baf05 3392
5fdbe4f0
DM
3393 return;
3394 };
3395
a0d1b1a2 3396 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
5fdbe4f0
DM
3397 }});
3398
3399__PACKAGE__->register_method({
afdb31d5 3400 name => 'vm_sendkey',
5fdbe4f0
DM
3401 path => '{vmid}/sendkey',
3402 method => 'PUT',
3403 protected => 1,
3404 proxyto => 'node',
3405 description => "Send key event to virtual machine.",
a0d1b1a2
DM
3406 permissions => {
3407 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
3408 },
5fdbe4f0 3409 parameters => {
3326ae19 3410 additionalProperties => 0,
5fdbe4f0
DM
3411 properties => {
3412 node => get_standard_option('pve-node'),
ab5904f7
TL
3413 vmid => get_standard_option('pve-vmid',
3414 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0
DM
3415 skiplock => get_standard_option('skiplock'),
3416 key => {
3417 description => "The key (qemu monitor encoding).",
3418 type => 'string'
3419 }
3420 },
3421 },
3422 returns => { type => 'null'},
3423 code => sub {
3424 my ($param) = @_;
3425
3426 my $rpcenv = PVE::RPCEnvironment::get();
3427
a0d1b1a2 3428 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
3429
3430 my $node = extract_param($param, 'node');
3431
3432 my $vmid = extract_param($param, 'vmid');
3433
3434 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 3435 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 3436 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0
DM
3437
3438 PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key});
3439
3440 return;
1e3baf05
DM
3441 }});
3442
1ac0d2ee
AD
3443__PACKAGE__->register_method({
3444 name => 'vm_feature',
3445 path => '{vmid}/feature',
3446 method => 'GET',
3447 proxyto => 'node',
75466c4f 3448 protected => 1,
1ac0d2ee
AD
3449 description => "Check if feature for virtual machine is available.",
3450 permissions => {
3451 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3452 },
3453 parameters => {
3326ae19 3454 additionalProperties => 0,
1ac0d2ee
AD
3455 properties => {
3456 node => get_standard_option('pve-node'),
3457 vmid => get_standard_option('pve-vmid'),
3458 feature => {
3459 description => "Feature to check.",
3460 type => 'string',
7758ce86 3461 enum => [ 'snapshot', 'clone', 'copy' ],
1ac0d2ee
AD
3462 },
3463 snapname => get_standard_option('pve-snapshot-name', {
3464 optional => 1,
3465 }),
3466 },
1ac0d2ee
AD
3467 },
3468 returns => {
719893a9
DM
3469 type => "object",
3470 properties => {
3471 hasFeature => { type => 'boolean' },
7043d946 3472 nodes => {
719893a9
DM
3473 type => 'array',
3474 items => { type => 'string' },
3475 }
3476 },
1ac0d2ee
AD
3477 },
3478 code => sub {
3479 my ($param) = @_;
3480
3481 my $node = extract_param($param, 'node');
3482
3483 my $vmid = extract_param($param, 'vmid');
3484
3485 my $snapname = extract_param($param, 'snapname');
3486
3487 my $feature = extract_param($param, 'feature');
3488
3489 my $running = PVE::QemuServer::check_running($vmid);
3490
ffda963f 3491 my $conf = PVE::QemuConfig->load_config($vmid);
1ac0d2ee
AD
3492
3493 if($snapname){
3494 my $snap = $conf->{snapshots}->{$snapname};
3495 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3496 $conf = $snap;
3497 }
3498 my $storecfg = PVE::Storage::config();
3499
719893a9 3500 my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg);
b2c9558d 3501 my $hasFeature = PVE::QemuConfig->has_feature($feature, $conf, $storecfg, $snapname, $running);
7043d946 3502
719893a9
DM
3503 return {
3504 hasFeature => $hasFeature,
3505 nodes => [ keys %$nodelist ],
7043d946 3506 };
1ac0d2ee
AD
3507 }});
3508
6116f729 3509__PACKAGE__->register_method({
9418baad
DM
3510 name => 'clone_vm',
3511 path => '{vmid}/clone',
6116f729
DM
3512 method => 'POST',
3513 protected => 1,
3514 proxyto => 'node',
37329185 3515 description => "Create a copy of virtual machine/template.",
6116f729 3516 permissions => {
9418baad 3517 description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
6116f729 3518 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
9cfd06d9 3519 "'Datastore.AllocateSpace' on any used storage and 'SDN.Use' on any used bridge/vnet",
75466c4f
DM
3520 check =>
3521 [ 'and',
9418baad 3522 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
75466c4f 3523 [ 'or',
6116f729
DM
3524 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
3525 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
3526 ],
3527 ]
3528 },
3529 parameters => {
3326ae19 3530 additionalProperties => 0,
6116f729 3531 properties => {
6116f729 3532 node => get_standard_option('pve-node'),
335af808 3533 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1ae43f8c
DM
3534 newid => get_standard_option('pve-vmid', {
3535 completion => \&PVE::Cluster::complete_next_vmid,
3536 description => 'VMID for the clone.' }),
a60ab1a6
DM
3537 name => {
3538 optional => 1,
3539 type => 'string', format => 'dns-name',
3540 description => "Set a name for the new VM.",
3541 },
3542 description => {
3543 optional => 1,
3544 type => 'string',
3545 description => "Description for the new VM.",
3546 },
75466c4f 3547 pool => {
6116f729
DM
3548 optional => 1,
3549 type => 'string', format => 'pve-poolid',
3550 description => "Add the new VM to the specified pool.",
3551 },
9076d880 3552 snapname => get_standard_option('pve-snapshot-name', {
9076d880
DM
3553 optional => 1,
3554 }),
81f043eb 3555 storage => get_standard_option('pve-storage-id', {
9418baad 3556 description => "Target storage for full clone.",
81f043eb
AD
3557 optional => 1,
3558 }),
55173c6b 3559 'format' => {
fd13b1d0 3560 description => "Target format for file storage. Only valid for full clone.",
42a19c87
AD
3561 type => 'string',
3562 optional => 1,
55173c6b 3563 enum => [ 'raw', 'qcow2', 'vmdk'],
42a19c87 3564 },
6116f729
DM
3565 full => {
3566 optional => 1,
55173c6b 3567 type => 'boolean',
fd13b1d0 3568 description => "Create a full copy of all disks. This is always done when " .
9418baad 3569 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
6116f729 3570 },
75466c4f 3571 target => get_standard_option('pve-node', {
55173c6b
DM
3572 description => "Target node. Only allowed if the original VM is on shared storage.",
3573 optional => 1,
3574 }),
0aab5a16
SI
3575 bwlimit => {
3576 description => "Override I/O bandwidth limit (in KiB/s).",
3577 optional => 1,
3578 type => 'integer',
3579 minimum => '0',
41756a3b 3580 default => 'clone limit from datacenter or storage config',
0aab5a16 3581 },
55173c6b 3582 },
6116f729
DM
3583 },
3584 returns => {
3585 type => 'string',
3586 },
3587 code => sub {
3588 my ($param) = @_;
3589
3590 my $rpcenv = PVE::RPCEnvironment::get();
a85ff91b 3591 my $authuser = $rpcenv->get_user();
6116f729
DM
3592
3593 my $node = extract_param($param, 'node');
6116f729 3594 my $vmid = extract_param($param, 'vmid');
6116f729 3595 my $newid = extract_param($param, 'newid');
6116f729 3596 my $pool = extract_param($param, 'pool');
6116f729 3597
55173c6b 3598 my $snapname = extract_param($param, 'snapname');
81f043eb 3599 my $storage = extract_param($param, 'storage');
42a19c87 3600 my $format = extract_param($param, 'format');
55173c6b
DM
3601 my $target = extract_param($param, 'target');
3602
3603 my $localnode = PVE::INotify::nodename();
3604
e099bad4 3605 if ($target && ($target eq $localnode || $target eq 'localhost')) {
a85ff91b 3606 undef $target;
a85ff91b 3607 }
55173c6b 3608
4df8fe45 3609 my $running = PVE::QemuServer::check_running($vmid) || 0;
d069275f 3610
4df8fe45
FG
3611 my $load_and_check = sub {
3612 $rpcenv->check_pool_exist($pool) if defined($pool);
3613 PVE::Cluster::check_node_exists($target) if $target;
6116f729 3614
4df8fe45 3615 my $storecfg = PVE::Storage::config();
4a5a2590 3616
4df8fe45
FG
3617 if ($storage) {
3618 # check if storage is enabled on local node
3619 PVE::Storage::storage_check_enabled($storecfg, $storage);
3620 if ($target) {
3621 # check if storage is available on target node
3622 PVE::Storage::storage_check_enabled($storecfg, $storage, $target);
3623 # clone only works if target storage is shared
3624 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
3625 die "can't clone to non-shared storage '$storage'\n"
3626 if !$scfg->{shared};
3627 }
3628 }
6116f729 3629
4df8fe45 3630 PVE::Cluster::check_cfs_quorum();
4e4f83fe 3631
ffda963f 3632 my $conf = PVE::QemuConfig->load_config($vmid);
ffda963f 3633 PVE::QemuConfig->check_lock($conf);
6116f729 3634
4e4f83fe 3635 my $verify_running = PVE::QemuServer::check_running($vmid) || 0;
4e4f83fe 3636 die "unexpected state change\n" if $verify_running != $running;
6116f729 3637
75466c4f
DM
3638 die "snapshot '$snapname' does not exist\n"
3639 if $snapname && !defined( $conf->{snapshots}->{$snapname});
6116f729 3640
dbecb46f 3641 my $full = $param->{full} // !PVE::QemuConfig->is_template($conf);
fd13b1d0
DM
3642
3643 die "parameter 'storage' not allowed for linked clones\n"
3644 if defined($storage) && !$full;
3645
3646 die "parameter 'format' not allowed for linked clones\n"
3647 if defined($format) && !$full;
3648
75466c4f 3649 my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
9076d880 3650
9418baad 3651 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
e3971865 3652 PVE::QemuServer::check_mapping_access($rpcenv, $authuser, $oldconf);
6116f729 3653
d6deb7f6 3654 PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $oldconf);
9cfd06d9 3655
a85ff91b
TL
3656 die "can't clone VM to node '$target' (VM uses local storage)\n"
3657 if $target && !$sharedvm;
75466c4f 3658
ffda963f 3659 my $conffile = PVE::QemuConfig->config_file($newid);
6116f729
DM
3660 die "unable to create VM $newid: config file already exists\n"
3661 if -f $conffile;
3662
9418baad 3663 my $newconf = { lock => 'clone' };
829967a9 3664 my $drives = {};
34456bf0 3665 my $fullclone = {};
829967a9
DM
3666 my $vollist = [];
3667
3668 foreach my $opt (keys %$oldconf) {
3669 my $value = $oldconf->{$opt};
3670
3671 # do not copy snapshot related info
3672 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
3673 $opt eq 'vmstate' || $opt eq 'snapstate';
3674
a78ea5df
WL
3675 # no need to copy unused images, because VMID(owner) changes anyways
3676 next if $opt =~ m/^unused\d+$/;
3677
e4a70a41
FE
3678 die "cannot clone TPM state while VM is running\n"
3679 if $full && $running && !$snapname && $opt eq 'tpmstate0';
3680
829967a9
DM
3681 # always change MAC! address
3682 if ($opt =~ m/^net(\d+)$/) {
3683 my $net = PVE::QemuServer::parse_net($value);
b5b99790
WB
3684 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
3685 $net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
829967a9 3686 $newconf->{$opt} = PVE::QemuServer::print_net($net);
74479ee9 3687 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
1f1412d1
DM
3688 my $drive = PVE::QemuServer::parse_drive($opt, $value);
3689 die "unable to parse drive options for '$opt'\n" if !$drive;
7fe8b44c 3690 if (PVE::QemuServer::drive_is_cdrom($drive, 1)) {
829967a9
DM
3691 $newconf->{$opt} = $value; # simply copy configuration
3692 } else {
7fe8b44c 3693 if ($full || PVE::QemuServer::drive_is_cloudinit($drive)) {
6318daca 3694 die "Full clone feature is not supported for drive '$opt'\n"
dba198b0 3695 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running);
34456bf0 3696 $fullclone->{$opt} = 1;
64ff6fe4
SP
3697 } else {
3698 # not full means clone instead of copy
6318daca 3699 die "Linked clone feature is not supported for drive '$opt'\n"
64ff6fe4 3700 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $drive->{file}, $snapname, $running);
dba198b0 3701 }
829967a9 3702 $drives->{$opt} = $drive;
f8c4b2c5 3703 next if PVE::QemuServer::drive_is_cloudinit($drive);
829967a9
DM
3704 push @$vollist, $drive->{file};
3705 }
3706 } else {
3707 # copy everything else
3708 $newconf->{$opt} = $value;
3709 }
3710 }
3711
dbecb46f
FE
3712 return ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone);
3713 };
3714
3715 my $clonefn = sub {
3716 my ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone) = $load_and_check->();
4df8fe45 3717 my $storecfg = PVE::Storage::config();
dbecb46f
FE
3718
3719 # auto generate a new uuid
cd11416f 3720 my $smbios1 = PVE::QemuServer::parse_smbios1($newconf->{smbios1} || '');
6ee499ff 3721 $smbios1->{uuid} = PVE::QemuServer::generate_uuid();
cd11416f 3722 $newconf->{smbios1} = PVE::QemuServer::print_smbios1($smbios1);
a85ff91b 3723 # auto generate a new vmgenid only if the option was set for template
6ee499ff
DC
3724 if ($newconf->{vmgenid}) {
3725 $newconf->{vmgenid} = PVE::QemuServer::generate_uuid();
3726 }
3727
829967a9
DM
3728 delete $newconf->{template};
3729
3730 if ($param->{name}) {
3731 $newconf->{name} = $param->{name};
3732 } else {
a85ff91b 3733 $newconf->{name} = "Copy-of-VM-" . ($oldconf->{name} // $vmid);
829967a9 3734 }
2dd53043 3735
829967a9
DM
3736 if ($param->{description}) {
3737 $newconf->{description} = $param->{description};
3738 }
3739
6116f729 3740 # create empty/temp config - this fails if VM already exists on other node
a85ff91b 3741 # FIXME use PVE::QemuConfig->create_and_lock_config and adapt code
9418baad 3742 PVE::Tools::file_set_contents($conffile, "# qmclone temporary file\nlock: clone\n");
6116f729 3743
dbecb46f 3744 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
68e46b84 3745
dbecb46f
FE
3746 my $newvollist = [];
3747 my $jobs = {};
3748
3749 eval {
3750 local $SIG{INT} =
3751 local $SIG{TERM} =
3752 local $SIG{QUIT} =
3753 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
3754
3755 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
3756
3757 my $bwlimit = extract_param($param, 'bwlimit');
3758
3759 my $total_jobs = scalar(keys %{$drives});
3760 my $i = 1;
3761
3762 foreach my $opt (sort keys %$drives) {
3763 my $drive = $drives->{$opt};
3764 my $skipcomplete = ($total_jobs != $i); # finish after last drive
3765 my $completion = $skipcomplete ? 'skip' : 'complete';
3766
3767 my $src_sid = PVE::Storage::parse_volume_id($drive->{file});
3768 my $storage_list = [ $src_sid ];
3769 push @$storage_list, $storage if defined($storage);
3770 my $clonelimit = PVE::Storage::get_bandwidth_limit('clone', $storage_list, $bwlimit);
3771
1196086f
FE
3772 my $source_info = {
3773 vmid => $vmid,
3774 running => $running,
3775 drivename => $opt,
3776 drive => $drive,
3777 snapname => $snapname,
3778 };
3779
3780 my $dest_info = {
3781 vmid => $newid,
25166060 3782 drivename => $opt,
1196086f
FE
3783 storage => $storage,
3784 format => $format,
3785 };
3786
7344af7b
FE
3787 $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($oldconf)
3788 if $opt eq 'efidisk0';
3789
dbecb46f
FE
3790 my $newdrive = PVE::QemuServer::clone_disk(
3791 $storecfg,
1196086f
FE
3792 $source_info,
3793 $dest_info,
dbecb46f
FE
3794 $fullclone->{$opt},
3795 $newvollist,
3796 $jobs,
3797 $completion,
3798 $oldconf->{agent},
3799 $clonelimit,
dbecb46f
FE
3800 );
3801
3802 $newconf->{$opt} = PVE::QemuServer::print_drive($newdrive);
68e46b84 3803
ffda963f 3804 PVE::QemuConfig->write_config($newid, $newconf);
dbecb46f
FE
3805 $i++;
3806 }
55173c6b 3807
dbecb46f 3808 delete $newconf->{lock};
baca276d 3809
dbecb46f
FE
3810 # do not write pending changes
3811 if (my @changes = keys %{$newconf->{pending}}) {
3812 my $pending = join(',', @changes);
3813 warn "found pending changes for '$pending', discarding for clone\n";
3814 delete $newconf->{pending};
3815 }
d703d4c0 3816
dbecb46f 3817 PVE::QemuConfig->write_config($newid, $newconf);
b83e0181 3818
ee8d2dea
AD
3819 PVE::QemuServer::create_ifaces_ipams_ips($newconf, $newid);
3820
dbecb46f
FE
3821 if ($target) {
3822 # always deactivate volumes - avoid lvm LVs to be active on several nodes
3823 PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
3824 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
c05c90a1 3825
dbecb46f
FE
3826 my $newconffile = PVE::QemuConfig->config_file($newid, $target);
3827 die "Failed to move config to node '$target' - rename failed: $!\n"
3828 if !rename($conffile, $newconffile);
3829 }
c05c90a1 3830
dbecb46f
FE
3831 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
3832 };
3833 if (my $err = $@) {
3834 eval { PVE::QemuServer::qemu_blockjobs_cancel($vmid, $jobs) };
3835 sleep 1; # some storage like rbd need to wait before release volume - really?
990b65ab 3836
dbecb46f
FE
3837 foreach my $volid (@$newvollist) {
3838 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
3839 warn $@ if $@;
6116f729
DM
3840 }
3841
dbecb46f 3842 PVE::Firewall::remove_vmfw_conf($newid);
6116f729 3843
dbecb46f
FE
3844 unlink $conffile; # avoid races -> last thing before die
3845
3846 die "clone failed: $err";
3847 }
457010cc 3848
dbecb46f 3849 return;
6116f729
DM
3850 };
3851
45fd77bb
FG
3852 # Aquire exclusive lock lock for $newid
3853 my $lock_target_vm = sub {
ffda963f 3854 return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn);
45fd77bb 3855 };
6116f729 3856
dbecb46f
FE
3857 my $lock_source_vm = sub {
3858 # exclusive lock if VM is running - else shared lock is enough;
3859 if ($running) {
3860 return PVE::QemuConfig->lock_config_full($vmid, 1, $lock_target_vm);
3861 } else {
3862 return PVE::QemuConfig->lock_config_shared($vmid, 1, $lock_target_vm);
3863 }
3864 };
3865
3866 $load_and_check->(); # early checks before forking/locking
3867
3868 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $lock_source_vm);
6116f729
DM
3869 }});
3870
586bfa78 3871__PACKAGE__->register_method({
43bc02a9
DM
3872 name => 'move_vm_disk',
3873 path => '{vmid}/move_disk',
e2cd75fa 3874 method => 'POST',
586bfa78
AD
3875 protected => 1,
3876 proxyto => 'node',
a9453218 3877 description => "Move volume to different storage or to a different VM.",
586bfa78 3878 permissions => {
a9453218
AL
3879 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
3880 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
3881 "a disk to another VM, you need the permissions on the target VM as well.",
44102492 3882 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
586bfa78
AD
3883 },
3884 parameters => {
3326ae19 3885 additionalProperties => 0,
c07a9e3d 3886 properties => {
586bfa78 3887 node => get_standard_option('pve-node'),
335af808 3888 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
a9453218
AL
3889 'target-vmid' => get_standard_option('pve-vmid', {
3890 completion => \&PVE::QemuServer::complete_vmid,
3891 optional => 1,
3892 }),
586bfa78
AD
3893 disk => {
3894 type => 'string',
3895 description => "The disk you want to move.",
a9453218 3896 enum => [PVE::QemuServer::Drive::valid_drive_names_with_unused()],
586bfa78 3897 },
335af808
DM
3898 storage => get_standard_option('pve-storage-id', {
3899 description => "Target storage.",
3900 completion => \&PVE::QemuServer::complete_storage,
a9453218 3901 optional => 1,
335af808 3902 }),
f519ab0b
TL
3903 'format' => {
3904 type => 'string',
3905 description => "Target Format.",
3906 enum => [ 'raw', 'qcow2', 'vmdk' ],
3907 optional => 1,
3908 },
70d45e33
DM
3909 delete => {
3910 type => 'boolean',
f519ab0b
TL
3911 description => "Delete the original disk after successful copy. By default the"
3912 ." original disk is kept as unused disk.",
70d45e33
DM
3913 optional => 1,
3914 default => 0,
3915 },
586bfa78
AD
3916 digest => {
3917 type => 'string',
f519ab0b
TL
3918 description => 'Prevent changes if current configuration file has different SHA1"
3919 ." digest. This can be used to prevent concurrent modifications.',
586bfa78
AD
3920 maxLength => 40,
3921 optional => 1,
3922 },
0aab5a16
SI
3923 bwlimit => {
3924 description => "Override I/O bandwidth limit (in KiB/s).",
3925 optional => 1,
3926 type => 'integer',
3927 minimum => '0',
41756a3b 3928 default => 'move limit from datacenter or storage config',
0aab5a16 3929 },
a9453218
AL
3930 'target-disk' => {
3931 type => 'string',
f519ab0b
TL
3932 description => "The config key the disk will be moved to on the target VM"
3933 ." (for example, ide0 or scsi1). Default is the source disk key.",
a9453218
AL
3934 enum => [PVE::QemuServer::Drive::valid_drive_names_with_unused()],
3935 optional => 1,
3936 },
3937 'target-digest' => {
3938 type => 'string',
f519ab0b
TL
3939 description => 'Prevent changes if the current config file of the target VM has a"
3940 ." different SHA1 digest. This can be used to detect concurrent modifications.',
a9453218
AL
3941 maxLength => 40,
3942 optional => 1,
3943 },
586bfa78
AD
3944 },
3945 },
e2cd75fa
DM
3946 returns => {
3947 type => 'string',
3948 description => "the task ID.",
3949 },
586bfa78
AD
3950 code => sub {
3951 my ($param) = @_;
3952
3953 my $rpcenv = PVE::RPCEnvironment::get();
586bfa78
AD
3954 my $authuser = $rpcenv->get_user();
3955
3956 my $node = extract_param($param, 'node');
586bfa78 3957 my $vmid = extract_param($param, 'vmid');
a9453218 3958 my $target_vmid = extract_param($param, 'target-vmid');
586bfa78 3959 my $digest = extract_param($param, 'digest');
a9453218 3960 my $target_digest = extract_param($param, 'target-digest');
586bfa78 3961 my $disk = extract_param($param, 'disk');
a9453218 3962 my $target_disk = extract_param($param, 'target-disk') // $disk;
586bfa78 3963 my $storeid = extract_param($param, 'storage');
586bfa78
AD
3964 my $format = extract_param($param, 'format');
3965
586bfa78
AD
3966 my $storecfg = PVE::Storage::config();
3967
bdf6ba1e 3968 my $load_and_check_move = sub {
ffda963f 3969 my $conf = PVE::QemuConfig->load_config($vmid);
dcce9b46
FG
3970 PVE::QemuConfig->check_lock($conf);
3971
44102492 3972 PVE::Tools::assert_if_modified($digest, $conf->{digest});
586bfa78
AD
3973
3974 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3975
3976 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3977
a85ff91b 3978 die "disk '$disk' has no associated volume\n" if !$drive->{file};
931432bd 3979 die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive, 1);
586bfa78 3980
a85ff91b 3981 my $old_volid = $drive->{file};
e2cd75fa 3982 my $oldfmt;
70d45e33 3983 my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
586bfa78
AD
3984 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
3985 $oldfmt = $1;
3986 }
3987
bdf6ba1e
FE
3988 die "you can't move to the same storage with same format\n"
3989 if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format);
586bfa78 3990
9dbf9b54 3991 # this only checks snapshots because $disk is passed!
70c0ad66
AL
3992 my $snapshotted = PVE::QemuServer::Drive::is_volume_in_use(
3993 $storecfg,
3994 $conf,
3995 $disk,
3996 $old_volid
3997 );
9dbf9b54
FG
3998 die "you can't move a disk with snapshots and delete the source\n"
3999 if $snapshotted && $param->{delete};
4000
bdf6ba1e
FE
4001 return ($conf, $drive, $oldstoreid, $snapshotted);
4002 };
4003
4004 my $move_updatefn = sub {
4005 my ($conf, $drive, $oldstoreid, $snapshotted) = $load_and_check_move->();
4006 my $old_volid = $drive->{file};
4007
70c0ad66
AL
4008 PVE::Cluster::log_msg(
4009 'info',
4010 $authuser,
4011 "move disk VM $vmid: move --disk $disk --storage $storeid"
4012 );
586bfa78
AD
4013
4014 my $running = PVE::QemuServer::check_running($vmid);
e2cd75fa
DM
4015
4016 PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]);
4017
bdf6ba1e 4018 my $newvollist = [];
586bfa78 4019
bdf6ba1e
FE
4020 eval {
4021 local $SIG{INT} =
4022 local $SIG{TERM} =
4023 local $SIG{QUIT} =
4024 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
0aab5a16 4025
bdf6ba1e
FE
4026 warn "moving disk with snapshots, snapshots will not be moved!\n"
4027 if $snapshotted;
e2cd75fa 4028
bdf6ba1e
FE
4029 my $bwlimit = extract_param($param, 'bwlimit');
4030 my $movelimit = PVE::Storage::get_bandwidth_limit(
4031 'move',
4032 [$oldstoreid, $storeid],
4033 $bwlimit
4034 );
7043d946 4035
1196086f
FE
4036 my $source_info = {
4037 vmid => $vmid,
4038 running => $running,
4039 drivename => $disk,
4040 drive => $drive,
4041 snapname => undef,
4042 };
4043
4044 my $dest_info = {
4045 vmid => $vmid,
25166060 4046 drivename => $disk,
1196086f
FE
4047 storage => $storeid,
4048 format => $format,
4049 };
4050
7344af7b
FE
4051 $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($conf)
4052 if $disk eq 'efidisk0';
4053
bdf6ba1e
FE
4054 my $newdrive = PVE::QemuServer::clone_disk(
4055 $storecfg,
1196086f
FE
4056 $source_info,
4057 $dest_info,
bdf6ba1e
FE
4058 1,
4059 $newvollist,
4060 undef,
4061 undef,
4062 undef,
4063 $movelimit,
bdf6ba1e
FE
4064 );
4065 $conf->{$disk} = PVE::QemuServer::print_drive($newdrive);
fbd7dcce 4066
bdf6ba1e 4067 PVE::QemuConfig->add_unused_volume($conf, $old_volid) if !$param->{delete};
73272365 4068
bdf6ba1e
FE
4069 # convert moved disk to base if part of template
4070 PVE::QemuServer::template_create($vmid, $conf, $disk)
4071 if PVE::QemuConfig->is_template($conf);
ca662131 4072
bdf6ba1e 4073 PVE::QemuConfig->write_config($vmid, $conf);
70d45e33 4074
bdf6ba1e
FE
4075 my $do_trim = PVE::QemuServer::get_qga_key($conf, 'fstrim_cloned_disks');
4076 if ($running && $do_trim && PVE::QemuServer::qga_check_running($vmid)) {
4077 eval { mon_cmd($vmid, "guest-fstrim") };
70d45e33 4078 }
bdf6ba1e
FE
4079
4080 eval {
4081 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
4082 PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ])
4083 if !$running;
4084 };
4085 warn $@ if $@;
586bfa78 4086 };
bdf6ba1e
FE
4087 if (my $err = $@) {
4088 foreach my $volid (@$newvollist) {
4089 eval { PVE::Storage::vdisk_free($storecfg, $volid) };
4090 warn $@ if $@;
4091 }
4092 die "storage migration failed: $err";
4093 }
586bfa78 4094
bdf6ba1e
FE
4095 if ($param->{delete}) {
4096 eval {
4097 PVE::Storage::deactivate_volumes($storecfg, [$old_volid]);
4098 PVE::Storage::vdisk_free($storecfg, $old_volid);
4099 };
4100 warn $@ if $@;
4101 }
586bfa78 4102 };
e2cd75fa 4103
a9453218
AL
4104 my $load_and_check_reassign_configs = sub {
4105 my $vmlist = PVE::Cluster::get_vmlist()->{ids};
4106
4107 die "could not find VM ${vmid}\n" if !exists($vmlist->{$vmid});
dbc817ba 4108 die "could not find target VM ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
a9453218 4109
dbc817ba
FG
4110 my $source_node = $vmlist->{$vmid}->{node};
4111 my $target_node = $vmlist->{$target_vmid}->{node};
4112
4113 die "Both VMs need to be on the same node ($source_node != $target_node)\n"
4114 if $source_node ne $target_node;
a9453218
AL
4115
4116 my $source_conf = PVE::QemuConfig->load_config($vmid);
4117 PVE::QemuConfig->check_lock($source_conf);
4118 my $target_conf = PVE::QemuConfig->load_config($target_vmid);
4119 PVE::QemuConfig->check_lock($target_conf);
4120
4121 die "Can't move disks from or to template VMs\n"
4122 if ($source_conf->{template} || $target_conf->{template});
4123
4124 if ($digest) {
4125 eval { PVE::Tools::assert_if_modified($digest, $source_conf->{digest}) };
4126 die "VM ${vmid}: $@" if $@;
4127 }
4128
4129 if ($target_digest) {
4130 eval { PVE::Tools::assert_if_modified($target_digest, $target_conf->{digest}) };
4131 die "VM ${target_vmid}: $@" if $@;
4132 }
4133
4134 die "Disk '${disk}' for VM '$vmid' does not exist\n" if !defined($source_conf->{$disk});
4135
4136 die "Target disk key '${target_disk}' is already in use for VM '$target_vmid'\n"
dbc817ba 4137 if $target_conf->{$target_disk};
a9453218
AL
4138
4139 my $drive = PVE::QemuServer::parse_drive(
4140 $disk,
4141 $source_conf->{$disk},
4142 );
dbc817ba
FG
4143 die "failed to parse source disk - $@\n" if !$drive;
4144
4145 my $source_volid = $drive->{file};
a9453218
AL
4146
4147 die "disk '${disk}' has no associated volume\n" if !$source_volid;
4148 die "CD drive contents can't be moved to another VM\n"
4149 if PVE::QemuServer::drive_is_cdrom($drive, 1);
dbc817ba
FG
4150
4151 my $storeid = PVE::Storage::parse_volume_id($source_volid, 1);
4152 die "Volume '$source_volid' not managed by PVE\n" if !defined($storeid);
4153
a9453218
AL
4154 die "Can't move disk used by a snapshot to another VM\n"
4155 if PVE::QemuServer::Drive::is_volume_in_use($storecfg, $source_conf, $disk, $source_volid);
4156 die "Storage does not support moving of this disk to another VM\n"
4157 if (!PVE::Storage::volume_has_feature($storecfg, 'rename', $source_volid));
dbc817ba 4158 die "Cannot move disk to another VM while the source VM is running - detach first\n"
a9453218
AL
4159 if PVE::QemuServer::check_running($vmid) && $disk !~ m/^unused\d+$/;
4160
dbc817ba 4161 # now re-parse using target disk slot format
f4e4c779
FG
4162 if ($target_disk =~ /^unused\d+$/) {
4163 $drive = PVE::QemuServer::parse_drive(
4164 $target_disk,
4165 $source_volid,
4166 );
4167 } else {
4168 $drive = PVE::QemuServer::parse_drive(
4169 $target_disk,
4170 $source_conf->{$disk},
4171 );
4172 }
dbc817ba 4173 die "failed to parse source disk for target disk format - $@\n" if !$drive;
a9453218
AL
4174
4175 my $repl_conf = PVE::ReplicationConfig->new();
dbc817ba
FG
4176 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
4177 my $format = (PVE::Storage::parse_volname($storecfg, $source_volid))[6];
4178 die "Cannot move disk to a replicated VM. Storage does not support replication!\n"
4179 if !PVE::Storage::storage_can_replicate($storecfg, $storeid, $format);
a9453218
AL
4180 }
4181
dbc817ba 4182 return ($source_conf, $target_conf, $drive);
a9453218
AL
4183 };
4184
4185 my $logfunc = sub {
4186 my ($msg) = @_;
4187 print STDERR "$msg\n";
4188 };
4189
4190 my $disk_reassignfn = sub {
4191 return PVE::QemuConfig->lock_config($vmid, sub {
4192 return PVE::QemuConfig->lock_config($target_vmid, sub {
dbc817ba 4193 my ($source_conf, $target_conf, $drive) = &$load_and_check_reassign_configs();
a9453218 4194
dbc817ba 4195 my $source_volid = $drive->{file};
a9453218
AL
4196
4197 print "moving disk '$disk' from VM '$vmid' to '$target_vmid'\n";
4198 my ($storeid, $source_volname) = PVE::Storage::parse_volume_id($source_volid);
4199
4200 my $fmt = (PVE::Storage::parse_volname($storecfg, $source_volid))[6];
4201
4202 my $new_volid = PVE::Storage::rename_volume(
4203 $storecfg,
4204 $source_volid,
4205 $target_vmid,
4206 );
4207
dbc817ba 4208 $drive->{file} = $new_volid;
a9453218 4209
96670745
TL
4210 my $boot_order = PVE::QemuServer::device_bootorder($source_conf);
4211 if (defined(delete $boot_order->{$disk})) {
4212 print "removing disk '$disk' from boot order config\n";
4213 my $boot_devs = [ sort { $boot_order->{$a} <=> $boot_order->{$b} } keys %$boot_order ];
4214 $source_conf->{boot} = PVE::QemuServer::print_bootorder($boot_devs);
4215 }
4216
a9453218
AL
4217 delete $source_conf->{$disk};
4218 print "removing disk '${disk}' from VM '${vmid}' config\n";
4219 PVE::QemuConfig->write_config($vmid, $source_conf);
4220
dbc817ba 4221 my $drive_string = PVE::QemuServer::print_drive($drive);
bf67da2b
AL
4222
4223 if ($target_disk =~ /^unused\d+$/) {
4224 $target_conf->{$target_disk} = $drive_string;
4225 PVE::QemuConfig->write_config($target_vmid, $target_conf);
4226 } else {
4227 &$update_vm_api(
4228 {
4229 node => $node,
4230 vmid => $target_vmid,
4231 digest => $target_digest,
4232 $target_disk => $drive_string,
4233 },
4234 1,
4235 );
4236 }
a9453218
AL
4237
4238 # remove possible replication snapshots
4239 if (PVE::Storage::volume_has_feature(
4240 $storecfg,
4241 'replicate',
4242 $source_volid),
4243 ) {
4244 eval {
4245 PVE::Replication::prepare(
4246 $storecfg,
4247 [$new_volid],
4248 undef,
4249 1,
4250 undef,
4251 $logfunc,
4252 )
4253 };
4254 if (my $err = $@) {
4255 print "Failed to remove replication snapshots on moved disk " .
4256 "'$target_disk'. Manual cleanup could be necessary.\n";
4257 }
4258 }
4259 });
4260 });
4261 };
4262
dbc817ba
FG
4263 if ($target_vmid && $storeid) {
4264 my $msg = "either set 'storage' or 'target-vmid', but not both";
4265 raise_param_exc({ 'target-vmid' => $msg, 'storage' => $msg });
4266 } elsif ($target_vmid) {
a9453218
AL
4267 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
4268 if $authuser ne 'root@pam';
4269
dbc817ba 4270 raise_param_exc({ 'target-vmid' => "must be different than source VMID to reassign disk" })
a9453218
AL
4271 if $vmid eq $target_vmid;
4272
44102492 4273 my (undef, undef, $drive) = &$load_and_check_reassign_configs();
a6273aa8
FG
4274 my $storage = PVE::Storage::parse_volume_id($drive->{file});
4275 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
44102492 4276
a9453218
AL
4277 return $rpcenv->fork_worker(
4278 'qmmove',
4279 "${vmid}-${disk}>${target_vmid}-${target_disk}",
4280 $authuser,
4281 $disk_reassignfn
4282 );
4283 } elsif ($storeid) {
44102492
FG
4284 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4285
a9453218
AL
4286 die "cannot move disk '$disk', only configured disks can be moved to another storage\n"
4287 if $disk =~ m/^unused\d+$/;
bdf6ba1e
FE
4288
4289 $load_and_check_move->(); # early checks before forking/locking
4290
4291 my $realcmd = sub {
4292 PVE::QemuConfig->lock_config($vmid, $move_updatefn);
4293 };
4294
4295 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
a9453218 4296 } else {
dbc817ba
FG
4297 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
4298 raise_param_exc({ 'target-vmid' => $msg, 'storage' => $msg });
a9453218 4299 }
586bfa78
AD
4300 }});
4301
71fc647f
TM
4302my $check_vm_disks_local = sub {
4303 my ($storecfg, $vmconf, $vmid) = @_;
4304
4305 my $local_disks = {};
4306
4307 # add some more information to the disks e.g. cdrom
0b7a0b78 4308 PVE::QemuServer::foreach_volid($vmconf, sub {
71fc647f
TM
4309 my ($volid, $attr) = @_;
4310
4311 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
4312 if ($storeid) {
4313 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
4314 return if $scfg->{shared};
4315 }
4316 # The shared attr here is just a special case where the vdisk
4317 # is marked as shared manually
4318 return if $attr->{shared};
4319 return if $attr->{cdrom} and $volid eq "none";
4320
4321 if (exists $local_disks->{$volid}) {
4322 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
4323 } else {
4324 $local_disks->{$volid} = $attr;
4325 # ensure volid is present in case it's needed
4326 $local_disks->{$volid}->{volid} = $volid;
4327 }
4328 });
4329
4330 return $local_disks;
4331};
4332
4333__PACKAGE__->register_method({
4334 name => 'migrate_vm_precondition',
4335 path => '{vmid}/migrate',
4336 method => 'GET',
4337 protected => 1,
4338 proxyto => 'node',
4339 description => "Get preconditions for migration.",
4340 permissions => {
4341 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4342 },
4343 parameters => {
4344 additionalProperties => 0,
4345 properties => {
4346 node => get_standard_option('pve-node'),
4347 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
4348 target => get_standard_option('pve-node', {
4349 description => "Target node.",
4350 completion => \&PVE::Cluster::complete_migration_target,
4351 optional => 1,
4352 }),
4353 },
4354 },
4355 returns => {
4356 type => "object",
4357 properties => {
4358 running => { type => 'boolean' },
4359 allowed_nodes => {
4360 type => 'array',
4361 optional => 1,
f25852c2
TM
4362 description => "List nodes allowed for offline migration, only passed if VM is offline"
4363 },
4364 not_allowed_nodes => {
4365 type => 'object',
4366 optional => 1,
4367 description => "List not allowed nodes with additional informations, only passed if VM is offline"
71fc647f
TM
4368 },
4369 local_disks => {
4370 type => 'array',
4371 description => "List local disks including CD-Rom, unsused and not referenced disks"
4372 },
4373 local_resources => {
4374 type => 'array',
4375 description => "List local resources e.g. pci, usb"
bc1a5860
DC
4376 },
4377 'mapped-resources' => {
4378 type => 'array',
4379 description => "List of mapped resources e.g. pci, usb"
4380 },
71fc647f
TM
4381 },
4382 },
4383 code => sub {
4384 my ($param) = @_;
4385
4386 my $rpcenv = PVE::RPCEnvironment::get();
4387
4388 my $authuser = $rpcenv->get_user();
4389
4390 PVE::Cluster::check_cfs_quorum();
4391
4392 my $res = {};
4393
4394 my $vmid = extract_param($param, 'vmid');
4395 my $target = extract_param($param, 'target');
4396 my $localnode = PVE::INotify::nodename();
4397
4398
4399 # test if VM exists
4400 my $vmconf = PVE::QemuConfig->load_config($vmid);
4401 my $storecfg = PVE::Storage::config();
4402
4403
4404 # try to detect errors early
4405 PVE::QemuConfig->check_lock($vmconf);
4406
4407 $res->{running} = PVE::QemuServer::check_running($vmid) ? 1:0;
4408
bc1a5860
DC
4409 my ($local_resources, $mapped_resources, $missing_mappings_by_node) =
4410 PVE::QemuServer::check_local_resources($vmconf, 1);
4411 delete $missing_mappings_by_node->{$localnode};
4412
bb30eedf
MF
4413 my $vga = PVE::QemuServer::parse_vga($vmconf->{vga});
4414 if ($res->{running} && $vga->{'clipboard'} && $vga->{'clipboard'} eq 'vnc') {
4415 push $local_resources->@*, "clipboard=vnc";
4416 }
4417
bc1a5860 4418 # if vm is not running, return target nodes where local storage/mapped devices are available
71fc647f
TM
4419 # for offline migration
4420 if (!$res->{running}) {
f25852c2
TM
4421 $res->{allowed_nodes} = [];
4422 my $checked_nodes = PVE::QemuServer::check_local_storage_availability($vmconf, $storecfg);
32075a2c 4423 delete $checked_nodes->{$localnode};
f25852c2 4424
f25852c2 4425 foreach my $node (keys %$checked_nodes) {
bc1a5860
DC
4426 my $missing_mappings = $missing_mappings_by_node->{$node};
4427 if (scalar($missing_mappings->@*)) {
4428 $checked_nodes->{$node}->{'unavailable-resources'} = $missing_mappings;
4429 next;
4430 }
4431
4432 if (!defined($checked_nodes->{$node}->{unavailable_storages})) {
f25852c2 4433 push @{$res->{allowed_nodes}}, $node;
f25852c2 4434 }
71fc647f 4435
f25852c2
TM
4436 }
4437 $res->{not_allowed_nodes} = $checked_nodes;
71fc647f
TM
4438 }
4439
71fc647f
TM
4440 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
4441 $res->{local_disks} = [ values %$local_disks ];;
4442
71fc647f 4443 $res->{local_resources} = $local_resources;
bc1a5860 4444 $res->{'mapped-resources'} = $mapped_resources;
71fc647f
TM
4445
4446 return $res;
4447
4448
4449 }});
4450
3ea94c60 4451__PACKAGE__->register_method({
afdb31d5 4452 name => 'migrate_vm',
3ea94c60
DM
4453 path => '{vmid}/migrate',
4454 method => 'POST',
4455 protected => 1,
4456 proxyto => 'node',
4457 description => "Migrate virtual machine. Creates a new migration task.",
a0d1b1a2
DM
4458 permissions => {
4459 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4460 },
3ea94c60 4461 parameters => {
3326ae19 4462 additionalProperties => 0,
3ea94c60
DM
4463 properties => {
4464 node => get_standard_option('pve-node'),
335af808 4465 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
c2ed338e 4466 target => get_standard_option('pve-node', {
335af808
DM
4467 description => "Target node.",
4468 completion => \&PVE::Cluster::complete_migration_target,
4469 }),
3ea94c60
DM
4470 online => {
4471 type => 'boolean',
13739386 4472 description => "Use online/live migration if VM is running. Ignored if VM is stopped.",
3ea94c60
DM
4473 optional => 1,
4474 },
4475 force => {
4476 type => 'boolean',
4477 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
4478 optional => 1,
4479 },
2de2d6f7
TL
4480 migration_type => {
4481 type => 'string',
4482 enum => ['secure', 'insecure'],
c07a9e3d 4483 description => "Migration traffic is encrypted using an SSH tunnel by default. On secure, completely private networks this can be disabled to increase performance.",
2de2d6f7
TL
4484 optional => 1,
4485 },
4486 migration_network => {
c07a9e3d 4487 type => 'string', format => 'CIDR',
2de2d6f7
TL
4488 description => "CIDR of the (sub) network that is used for migration.",
4489 optional => 1,
4490 },
56af7146
AD
4491 "with-local-disks" => {
4492 type => 'boolean',
4493 description => "Enable live storage migration for local disk",
b74cad8a 4494 optional => 1,
56af7146 4495 },
bf8fc5a3 4496 targetstorage => get_standard_option('pve-targetstorage', {
255e9c54 4497 completion => \&PVE::QemuServer::complete_migration_storage,
56af7146 4498 }),
0aab5a16
SI
4499 bwlimit => {
4500 description => "Override I/O bandwidth limit (in KiB/s).",
4501 optional => 1,
4502 type => 'integer',
4503 minimum => '0',
41756a3b 4504 default => 'migrate limit from datacenter or storage config',
0aab5a16 4505 },
3ea94c60
DM
4506 },
4507 },
afdb31d5 4508 returns => {
3ea94c60
DM
4509 type => 'string',
4510 description => "the task ID.",
4511 },
4512 code => sub {
4513 my ($param) = @_;
4514
4515 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 4516 my $authuser = $rpcenv->get_user();
3ea94c60
DM
4517
4518 my $target = extract_param($param, 'target');
4519
4520 my $localnode = PVE::INotify::nodename();
4521 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
4522
4523 PVE::Cluster::check_cfs_quorum();
4524
4525 PVE::Cluster::check_node_exists($target);
4526
4527 my $targetip = PVE::Cluster::remote_node_ip($target);
4528
4529 my $vmid = extract_param($param, 'vmid');
4530
afdb31d5 4531 raise_param_exc({ force => "Only root may use this option." })
a0d1b1a2 4532 if $param->{force} && $authuser ne 'root@pam';
3ea94c60 4533
2de2d6f7
TL
4534 raise_param_exc({ migration_type => "Only root may use this option." })
4535 if $param->{migration_type} && $authuser ne 'root@pam';
4536
4537 # allow root only until better network permissions are available
4538 raise_param_exc({ migration_network => "Only root may use this option." })
4539 if $param->{migration_network} && $authuser ne 'root@pam';
4540
3ea94c60 4541 # test if VM exists
ffda963f 4542 my $conf = PVE::QemuConfig->load_config($vmid);
3ea94c60
DM
4543
4544 # try to detect errors early
a5ed42d3 4545
ffda963f 4546 PVE::QemuConfig->check_lock($conf);
a5ed42d3 4547
3ea94c60 4548 if (PVE::QemuServer::check_running($vmid)) {
fda72913 4549 die "can't migrate running VM without --online\n" if !$param->{online};
aa491a6e
FE
4550
4551 my $repl_conf = PVE::ReplicationConfig->new();
4552 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
4553 my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
68980d66
FE
4554 if (!$param->{force} && $is_replicated && !$is_replicated_to_target) {
4555 die "Cannot live-migrate replicated VM to node '$target' - not a replication " .
4556 "target. Use 'force' to override.\n";
aa491a6e 4557 }
13739386 4558 } else {
c3ddb94d 4559 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online};
13739386 4560 $param->{online} = 0;
3ea94c60
DM
4561 }
4562
47152e2e 4563 my $storecfg = PVE::Storage::config();
bf8fc5a3
FG
4564 if (my $targetstorage = $param->{targetstorage}) {
4565 my $storagemap = eval { PVE::JSONSchema::parse_idmap($targetstorage, 'pve-storage-id') };
e214cda8 4566 raise_param_exc({ targetstorage => "failed to parse storage map: $@" })
bf8fc5a3
FG
4567 if $@;
4568
aea447bb
FG
4569 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
4570 if !defined($storagemap->{identity});
4571
bd61033e 4572 foreach my $target_sid (values %{$storagemap->{entries}}) {
9fb295d0 4573 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
bf8fc5a3
FG
4574 }
4575
9fb295d0 4576 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
bf8fc5a3
FG
4577 if $storagemap->{default};
4578
4579 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target)
4580 if $storagemap->{identity};
4581
4582 $param->{storagemap} = $storagemap;
d80ad67f
AD
4583 } else {
4584 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target);
4585 }
47152e2e 4586
2003f0f8 4587 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
3ea94c60 4588
88fc87b4
DM
4589 my $hacmd = sub {
4590 my $upid = shift;
3ea94c60 4591
02765844 4592 print "Requesting HA migration for VM $vmid to node $target\n";
88fc87b4 4593
a4262553 4594 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
88fc87b4 4595 PVE::Tools::run_command($cmd);
88fc87b4
DM
4596 return;
4597 };
4598
4599 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
4600
4601 } else {
4602
f53c6ad8 4603 my $realcmd = sub {
f53c6ad8
DM
4604 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
4605 };
88fc87b4 4606
f53c6ad8
DM
4607 my $worker = sub {
4608 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
88fc87b4
DM
4609 };
4610
f53c6ad8 4611 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
88fc87b4 4612 }
3ea94c60 4613
3ea94c60 4614 }});
1e3baf05 4615
06fedff6
FG
4616__PACKAGE__->register_method({
4617 name => 'remote_migrate_vm',
4618 path => '{vmid}/remote_migrate',
4619 method => 'POST',
4620 protected => 1,
4621 proxyto => 'node',
4622 description => "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!",
4623 permissions => {
4624 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4625 },
4626 parameters => {
4627 additionalProperties => 0,
4628 properties => {
4629 node => get_standard_option('pve-node'),
4630 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
4631 'target-vmid' => get_standard_option('pve-vmid', { optional => 1 }),
4632 'target-endpoint' => get_standard_option('proxmox-remote', {
4633 description => "Remote target endpoint",
4634 }),
4635 online => {
4636 type => 'boolean',
4637 description => "Use online/live migration if VM is running. Ignored if VM is stopped.",
4638 optional => 1,
4639 },
4640 delete => {
4641 type => 'boolean',
4642 description => "Delete the original VM and related data after successful migration. By default the original VM is kept on the source cluster in a stopped state.",
4643 optional => 1,
4644 default => 0,
4645 },
4646 'target-storage' => get_standard_option('pve-targetstorage', {
4647 completion => \&PVE::QemuServer::complete_migration_storage,
4648 optional => 0,
4649 }),
4650 'target-bridge' => {
4651 type => 'string',
4652 description => "Mapping from source to target bridges. Providing only a single bridge ID maps all source bridges to that bridge. Providing the special value '1' will map each source bridge to itself.",
4653 format => 'bridge-pair-list',
4654 },
4655 bwlimit => {
4656 description => "Override I/O bandwidth limit (in KiB/s).",
4657 optional => 1,
4658 type => 'integer',
4659 minimum => '0',
4660 default => 'migrate limit from datacenter or storage config',
4661 },
4662 },
4663 },
4664 returns => {
4665 type => 'string',
4666 description => "the task ID.",
4667 },
4668 code => sub {
4669 my ($param) = @_;
4670
4671 my $rpcenv = PVE::RPCEnvironment::get();
4672 my $authuser = $rpcenv->get_user();
4673
4674 my $source_vmid = extract_param($param, 'vmid');
4675 my $target_endpoint = extract_param($param, 'target-endpoint');
4676 my $target_vmid = extract_param($param, 'target-vmid') // $source_vmid;
4677
4678 my $delete = extract_param($param, 'delete') // 0;
4679
4680 PVE::Cluster::check_cfs_quorum();
4681
4682 # test if VM exists
4683 my $conf = PVE::QemuConfig->load_config($source_vmid);
4684
4685 PVE::QemuConfig->check_lock($conf);
4686
4687 raise_param_exc({ vmid => "cannot migrate HA-managed VM to remote cluster" })
4688 if PVE::HA::Config::vm_is_ha_managed($source_vmid);
4689
4690 my $remote = PVE::JSONSchema::parse_property_string('proxmox-remote', $target_endpoint);
4691
4692 # TODO: move this as helper somewhere appropriate?
4693 my $conn_args = {
4694 protocol => 'https',
4695 host => $remote->{host},
4696 port => $remote->{port} // 8006,
4697 apitoken => $remote->{apitoken},
4698 };
4699
4700 my $fp;
4701 if ($fp = $remote->{fingerprint}) {
4702 $conn_args->{cached_fingerprints} = { uc($fp) => 1 };
4703 }
4704
4705 print "Establishing API connection with remote at '$remote->{host}'\n";
4706
4707 my $api_client = PVE::APIClient::LWP->new(%$conn_args);
4708
4709 if (!defined($fp)) {
4710 my $cert_info = $api_client->get("/nodes/localhost/certificates/info");
4711 foreach my $cert (@$cert_info) {
4712 my $filename = $cert->{filename};
4713 next if $filename ne 'pveproxy-ssl.pem' && $filename ne 'pve-ssl.pem';
4714 $fp = $cert->{fingerprint} if !$fp || $filename eq 'pveproxy-ssl.pem';
4715 }
4716 $conn_args->{cached_fingerprints} = { uc($fp) => 1 }
4717 if defined($fp);
4718 }
4719
4720 my $repl_conf = PVE::ReplicationConfig->new();
4721 my $is_replicated = $repl_conf->check_for_existing_jobs($source_vmid, 1);
4722 die "cannot remote-migrate replicated VM\n" if $is_replicated;
4723
4724 if (PVE::QemuServer::check_running($source_vmid)) {
4725 die "can't migrate running VM without --online\n" if !$param->{online};
4726
4727 } else {
4728 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online};
4729 $param->{online} = 0;
4730 }
4731
06fedff6
FG
4732 my $storecfg = PVE::Storage::config();
4733 my $target_storage = extract_param($param, 'target-storage');
4734 my $storagemap = eval { PVE::JSONSchema::parse_idmap($target_storage, 'pve-storage-id') };
4735 raise_param_exc({ 'target-storage' => "failed to parse storage map: $@" })
4736 if $@;
4737
4738 my $target_bridge = extract_param($param, 'target-bridge');
4739 my $bridgemap = eval { PVE::JSONSchema::parse_idmap($target_bridge, 'pve-bridge-id') };
4740 raise_param_exc({ 'target-bridge' => "failed to parse bridge map: $@" })
4741 if $@;
4742
06fedff6
FG
4743 die "remote migration requires explicit storage mapping!\n"
4744 if $storagemap->{identity};
4745
4746 $param->{storagemap} = $storagemap;
4747 $param->{bridgemap} = $bridgemap;
4748 $param->{remote} = {
4749 conn => $conn_args, # re-use fingerprint for tunnel
4750 client => $api_client,
4751 vmid => $target_vmid,
4752 };
4753 $param->{migration_type} = 'websocket';
4754 $param->{'with-local-disks'} = 1;
4755 $param->{delete} = $delete if $delete;
4756
4757 my $cluster_status = $api_client->get("/cluster/status");
4758 my $target_node;
4759 foreach my $entry (@$cluster_status) {
4760 next if $entry->{type} ne 'node';
4761 if ($entry->{local}) {
4762 $target_node = $entry->{name};
4763 last;
4764 }
4765 }
4766
4767 die "couldn't determine endpoint's node name\n"
4768 if !defined($target_node);
4769
4770 my $realcmd = sub {
4771 PVE::QemuMigrate->migrate($target_node, $remote->{host}, $source_vmid, $param);
4772 };
4773
4774 my $worker = sub {
4775 return PVE::GuestHelpers::guest_migration_lock($source_vmid, 10, $realcmd);
4776 };
4777
4778 return $rpcenv->fork_worker('qmigrate', $source_vmid, $authuser, $worker);
4779 }});
4780
91c94f0a 4781__PACKAGE__->register_method({
afdb31d5
DM
4782 name => 'monitor',
4783 path => '{vmid}/monitor',
91c94f0a
DM
4784 method => 'POST',
4785 protected => 1,
4786 proxyto => 'node',
7bd9abd2 4787 description => "Execute QEMU monitor commands.",
a0d1b1a2 4788 permissions => {
a8f2f427 4789 description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
c07a9e3d 4790 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
a0d1b1a2 4791 },
91c94f0a 4792 parameters => {
3326ae19 4793 additionalProperties => 0,
91c94f0a
DM
4794 properties => {
4795 node => get_standard_option('pve-node'),
4796 vmid => get_standard_option('pve-vmid'),
4797 command => {
4798 type => 'string',
4799 description => "The monitor command.",
4800 }
4801 },
4802 },
4803 returns => { type => 'string'},
4804 code => sub {
4805 my ($param) = @_;
4806
a8f2f427
FG
4807 my $rpcenv = PVE::RPCEnvironment::get();
4808 my $authuser = $rpcenv->get_user();
4809
4810 my $is_ro = sub {
4811 my $command = shift;
4812 return $command =~ m/^\s*info(\s+|$)/
4813 || $command =~ m/^\s*help\s*$/;
4814 };
4815
4816 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4817 if !&$is_ro($param->{command});
4818
91c94f0a
DM
4819 my $vmid = $param->{vmid};
4820
ffda963f 4821 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
91c94f0a
DM
4822
4823 my $res = '';
4824 eval {
0a13e08e 4825 $res = PVE::QemuServer::Monitor::hmp_cmd($vmid, $param->{command});
91c94f0a
DM
4826 };
4827 $res = "ERROR: $@" if $@;
4828
4829 return $res;
4830 }});
4831
0d02881c
AD
4832__PACKAGE__->register_method({
4833 name => 'resize_vm',
614e3941 4834 path => '{vmid}/resize',
0d02881c
AD
4835 method => 'PUT',
4836 protected => 1,
4837 proxyto => 'node',
2f48a4f5 4838 description => "Extend volume size.",
0d02881c 4839 permissions => {
3b2773f6 4840 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
0d02881c
AD
4841 },
4842 parameters => {
3326ae19
TL
4843 additionalProperties => 0,
4844 properties => {
2f48a4f5 4845 node => get_standard_option('pve-node'),
335af808 4846 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2f48a4f5
DM
4847 skiplock => get_standard_option('skiplock'),
4848 disk => {
4849 type => 'string',
4850 description => "The disk you want to resize.",
e0fd2b2f 4851 enum => [PVE::QemuServer::Drive::valid_drive_names()],
2f48a4f5
DM
4852 },
4853 size => {
4854 type => 'string',
f91b2e45 4855 pattern => '\+?\d+(\.\d+)?[KMGT]?',
e248477e 4856 description => "The new size. With the `+` sign the value is added to the actual size of the volume and without it, the value is taken as an absolute one. Shrinking disk size is not supported.",
2f48a4f5
DM
4857 },
4858 digest => {
4859 type => 'string',
4860 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4861 maxLength => 40,
4862 optional => 1,
4863 },
4864 },
0d02881c 4865 },
606d9d76
FE
4866 returns => {
4867 type => 'string',
4868 description => "the task ID.",
4869 },
0d02881c
AD
4870 code => sub {
4871 my ($param) = @_;
4872
4873 my $rpcenv = PVE::RPCEnvironment::get();
4874
4875 my $authuser = $rpcenv->get_user();
4876
4877 my $node = extract_param($param, 'node');
4878
4879 my $vmid = extract_param($param, 'vmid');
4880
4881 my $digest = extract_param($param, 'digest');
4882
2f48a4f5 4883 my $disk = extract_param($param, 'disk');
75466c4f 4884
2f48a4f5 4885 my $sizestr = extract_param($param, 'size');
0d02881c 4886
f91b2e45 4887 my $skiplock = extract_param($param, 'skiplock');
0d02881c
AD
4888 raise_param_exc({ skiplock => "Only root may use this option." })
4889 if $skiplock && $authuser ne 'root@pam';
4890
0d02881c
AD
4891 my $storecfg = PVE::Storage::config();
4892
0d02881c
AD
4893 my $updatefn = sub {
4894
ffda963f 4895 my $conf = PVE::QemuConfig->load_config($vmid);
0d02881c
AD
4896
4897 die "checksum missmatch (file change by other user?)\n"
4898 if $digest && $digest ne $conf->{digest};
ffda963f 4899 PVE::QemuConfig->check_lock($conf) if !$skiplock;
0d02881c 4900
f91b2e45
DM
4901 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4902
4903 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
4904
d662790a
WL
4905 my (undef, undef, undef, undef, undef, undef, $format) =
4906 PVE::Storage::parse_volname($storecfg, $drive->{file});
4907
f91b2e45
DM
4908 my $volid = $drive->{file};
4909
4910 die "disk '$disk' has no associated volume\n" if !$volid;
4911
4912 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
4913
4914 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
4915
4916 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4917
b572a606 4918 PVE::Storage::activate_volumes($storecfg, [$volid]);
f91b2e45
DM
4919 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
4920
ed94b2ad 4921 die "Could not determine current size of volume '$volid'\n" if !defined($size);
f8b829aa 4922
f91b2e45
DM
4923 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
4924 my ($ext, $newsize, $unit) = ($1, $2, $4);
4925 if ($unit) {
4926 if ($unit eq 'K') {
4927 $newsize = $newsize * 1024;
4928 } elsif ($unit eq 'M') {
4929 $newsize = $newsize * 1024 * 1024;
4930 } elsif ($unit eq 'G') {
4931 $newsize = $newsize * 1024 * 1024 * 1024;
4932 } elsif ($unit eq 'T') {
4933 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
4934 }
4935 }
4936 $newsize += $size if $ext;
4937 $newsize = int($newsize);
4938
9a478b17 4939 die "shrinking disks is not supported\n" if $newsize < $size;
f91b2e45
DM
4940
4941 return if $size == $newsize;
4942
2f48a4f5 4943 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
0d02881c 4944
f91b2e45 4945 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
75466c4f 4946
e29e5be6 4947 $drive->{size} = $newsize;
71c58bb7 4948 $conf->{$disk} = PVE::QemuServer::print_drive($drive);
f91b2e45 4949
ffda963f 4950 PVE::QemuConfig->write_config($vmid, $conf);
f91b2e45 4951 };
0d02881c 4952
606d9d76
FE
4953 my $worker = sub {
4954 PVE::QemuConfig->lock_config($vmid, $updatefn);
4955 };
4956
4957 return $rpcenv->fork_worker('resize', $vmid, $authuser, $worker);
0d02881c
AD
4958 }});
4959
9dbd1ee4 4960__PACKAGE__->register_method({
7e7d7b61 4961 name => 'snapshot_list',
9dbd1ee4 4962 path => '{vmid}/snapshot',
7e7d7b61
DM
4963 method => 'GET',
4964 description => "List all snapshots.",
4965 permissions => {
4966 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
4967 },
4968 proxyto => 'node',
4969 protected => 1, # qemu pid files are only readable by root
4970 parameters => {
3326ae19 4971 additionalProperties => 0,
7e7d7b61 4972 properties => {
e261de40 4973 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
7e7d7b61
DM
4974 node => get_standard_option('pve-node'),
4975 },
4976 },
4977 returns => {
4978 type => 'array',
4979 items => {
4980 type => "object",
ce9b0a38
DM
4981 properties => {
4982 name => {
4983 description => "Snapshot identifier. Value 'current' identifies the current VM.",
4984 type => 'string',
4985 },
4986 vmstate => {
4987 description => "Snapshot includes RAM.",
4988 type => 'boolean',
4989 optional => 1,
4990 },
4991 description => {
4992 description => "Snapshot description.",
4993 type => 'string',
4994 },
4995 snaptime => {
4996 description => "Snapshot creation time",
4997 type => 'integer',
4998 renderer => 'timestamp',
4999 optional => 1,
5000 },
5001 parent => {
5002 description => "Parent snapshot identifier.",
5003 type => 'string',
5004 optional => 1,
5005 },
5006 },
7e7d7b61
DM
5007 },
5008 links => [ { rel => 'child', href => "{name}" } ],
5009 },
5010 code => sub {
5011 my ($param) = @_;
5012
6aa4651b
DM
5013 my $vmid = $param->{vmid};
5014
ffda963f 5015 my $conf = PVE::QemuConfig->load_config($vmid);
7e7d7b61
DM
5016 my $snaphash = $conf->{snapshots} || {};
5017
5018 my $res = [];
5019
5020 foreach my $name (keys %$snaphash) {
0ea6bc69 5021 my $d = $snaphash->{$name};
75466c4f
DM
5022 my $item = {
5023 name => $name,
5024 snaptime => $d->{snaptime} || 0,
6aa4651b 5025 vmstate => $d->{vmstate} ? 1 : 0,
982c7f12
DM
5026 description => $d->{description} || '',
5027 };
0ea6bc69 5028 $item->{parent} = $d->{parent} if $d->{parent};
3ee28e38 5029 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
0ea6bc69
DM
5030 push @$res, $item;
5031 }
5032
6aa4651b 5033 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
ce9b0a38
DM
5034 my $current = {
5035 name => 'current',
5036 digest => $conf->{digest},
5037 running => $running,
5038 description => "You are here!",
5039 };
d1914468
DM
5040 $current->{parent} = $conf->{parent} if $conf->{parent};
5041
5042 push @$res, $current;
7e7d7b61
DM
5043
5044 return $res;
5045 }});
5046
5047__PACKAGE__->register_method({
5048 name => 'snapshot',
5049 path => '{vmid}/snapshot',
5050 method => 'POST',
9dbd1ee4
AD
5051 protected => 1,
5052 proxyto => 'node',
5053 description => "Snapshot a VM.",
5054 permissions => {
f1baf1df 5055 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
9dbd1ee4
AD
5056 },
5057 parameters => {
5058 additionalProperties => 0,
5059 properties => {
5060 node => get_standard_option('pve-node'),
335af808 5061 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 5062 snapname => get_standard_option('pve-snapshot-name'),
9dbd1ee4
AD
5063 vmstate => {
5064 optional => 1,
5065 type => 'boolean',
5066 description => "Save the vmstate",
5067 },
782f4f75
DM
5068 description => {
5069 optional => 1,
5070 type => 'string',
5071 description => "A textual description or comment.",
5072 },
9dbd1ee4
AD
5073 },
5074 },
7e7d7b61
DM
5075 returns => {
5076 type => 'string',
5077 description => "the task ID.",
5078 },
9dbd1ee4
AD
5079 code => sub {
5080 my ($param) = @_;
5081
5082 my $rpcenv = PVE::RPCEnvironment::get();
5083
5084 my $authuser = $rpcenv->get_user();
5085
5086 my $node = extract_param($param, 'node');
5087
5088 my $vmid = extract_param($param, 'vmid');
5089
9dbd1ee4
AD
5090 my $snapname = extract_param($param, 'snapname');
5091
d1914468
DM
5092 die "unable to use snapshot name 'current' (reserved name)\n"
5093 if $snapname eq 'current';
5094
a85c6be1
FG
5095 die "unable to use snapshot name 'pending' (reserved name)\n"
5096 if lc($snapname) eq 'pending';
5097
7e7d7b61 5098 my $realcmd = sub {
22c377f0 5099 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
c2ed338e 5100 PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate},
af9110dd 5101 $param->{description});
7e7d7b61
DM
5102 };
5103
5104 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
5105 }});
5106
154ccdcd
DM
5107__PACKAGE__->register_method({
5108 name => 'snapshot_cmd_idx',
5109 path => '{vmid}/snapshot/{snapname}',
5110 description => '',
5111 method => 'GET',
5112 permissions => {
5113 user => 'all',
5114 },
5115 parameters => {
3326ae19 5116 additionalProperties => 0,
154ccdcd
DM
5117 properties => {
5118 vmid => get_standard_option('pve-vmid'),
5119 node => get_standard_option('pve-node'),
8abd398b 5120 snapname => get_standard_option('pve-snapshot-name'),
154ccdcd
DM
5121 },
5122 },
5123 returns => {
5124 type => 'array',
5125 items => {
5126 type => "object",
5127 properties => {},
5128 },
5129 links => [ { rel => 'child', href => "{cmd}" } ],
5130 },
5131 code => sub {
5132 my ($param) = @_;
5133
5134 my $res = [];
5135
5136 push @$res, { cmd => 'rollback' };
d788cea6 5137 push @$res, { cmd => 'config' };
154ccdcd
DM
5138
5139 return $res;
5140 }});
5141
d788cea6
DM
5142__PACKAGE__->register_method({
5143 name => 'update_snapshot_config',
5144 path => '{vmid}/snapshot/{snapname}/config',
5145 method => 'PUT',
5146 protected => 1,
5147 proxyto => 'node',
5148 description => "Update snapshot metadata.",
5149 permissions => {
5150 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5151 },
5152 parameters => {
5153 additionalProperties => 0,
5154 properties => {
5155 node => get_standard_option('pve-node'),
5156 vmid => get_standard_option('pve-vmid'),
5157 snapname => get_standard_option('pve-snapshot-name'),
5158 description => {
5159 optional => 1,
5160 type => 'string',
5161 description => "A textual description or comment.",
5162 },
5163 },
5164 },
5165 returns => { type => 'null' },
5166 code => sub {
5167 my ($param) = @_;
5168
5169 my $rpcenv = PVE::RPCEnvironment::get();
5170
5171 my $authuser = $rpcenv->get_user();
5172
5173 my $vmid = extract_param($param, 'vmid');
5174
5175 my $snapname = extract_param($param, 'snapname');
5176
d1c1af4b 5177 return if !defined($param->{description});
d788cea6
DM
5178
5179 my $updatefn = sub {
5180
ffda963f 5181 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6 5182
ffda963f 5183 PVE::QemuConfig->check_lock($conf);
d788cea6
DM
5184
5185 my $snap = $conf->{snapshots}->{$snapname};
5186
75466c4f
DM
5187 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5188
d788cea6
DM
5189 $snap->{description} = $param->{description} if defined($param->{description});
5190
ffda963f 5191 PVE::QemuConfig->write_config($vmid, $conf);
d788cea6
DM
5192 };
5193
ffda963f 5194 PVE::QemuConfig->lock_config($vmid, $updatefn);
d788cea6 5195
d1c1af4b 5196 return;
d788cea6
DM
5197 }});
5198
5199__PACKAGE__->register_method({
5200 name => 'get_snapshot_config',
5201 path => '{vmid}/snapshot/{snapname}/config',
5202 method => 'GET',
5203 proxyto => 'node',
5204 description => "Get snapshot configuration",
5205 permissions => {
65204e92 5206 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any => 1],
d788cea6
DM
5207 },
5208 parameters => {
5209 additionalProperties => 0,
5210 properties => {
5211 node => get_standard_option('pve-node'),
5212 vmid => get_standard_option('pve-vmid'),
5213 snapname => get_standard_option('pve-snapshot-name'),
5214 },
5215 },
5216 returns => { type => "object" },
5217 code => sub {
5218 my ($param) = @_;
5219
5220 my $rpcenv = PVE::RPCEnvironment::get();
5221
5222 my $authuser = $rpcenv->get_user();
5223
5224 my $vmid = extract_param($param, 'vmid');
5225
5226 my $snapname = extract_param($param, 'snapname');
5227
ffda963f 5228 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6
DM
5229
5230 my $snap = $conf->{snapshots}->{$snapname};
5231
75466c4f
DM
5232 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5233
d788cea6
DM
5234 return $snap;
5235 }});
5236
7e7d7b61
DM
5237__PACKAGE__->register_method({
5238 name => 'rollback',
154ccdcd 5239 path => '{vmid}/snapshot/{snapname}/rollback',
7e7d7b61
DM
5240 method => 'POST',
5241 protected => 1,
5242 proxyto => 'node',
5243 description => "Rollback VM state to specified snapshot.",
5244 permissions => {
c268337d 5245 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
7e7d7b61
DM
5246 },
5247 parameters => {
5248 additionalProperties => 0,
5249 properties => {
5250 node => get_standard_option('pve-node'),
335af808 5251 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 5252 snapname => get_standard_option('pve-snapshot-name'),
76b29aaf
SH
5253 start => {
5254 type => 'boolean',
a46d039d
SH
5255 description => "Whether the VM should get started after rolling back successfully."
5256 . " (Note: VMs will be automatically started if the snapshot includes RAM.)",
76b29aaf
SH
5257 optional => 1,
5258 default => 0,
5259 },
7e7d7b61
DM
5260 },
5261 },
5262 returns => {
5263 type => 'string',
5264 description => "the task ID.",
5265 },
5266 code => sub {
5267 my ($param) = @_;
5268
5269 my $rpcenv = PVE::RPCEnvironment::get();
5270
5271 my $authuser = $rpcenv->get_user();
5272
5273 my $node = extract_param($param, 'node');
5274
5275 my $vmid = extract_param($param, 'vmid');
5276
5277 my $snapname = extract_param($param, 'snapname');
5278
7e7d7b61 5279 my $realcmd = sub {
22c377f0 5280 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
b2c9558d 5281 PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
76b29aaf 5282
a46d039d 5283 if ($param->{start} && !PVE::QemuServer::Helpers::vm_running_locally($vmid)) {
76b29aaf
SH
5284 PVE::API2::Qemu->vm_start({ vmid => $vmid, node => $node });
5285 }
7e7d7b61
DM
5286 };
5287
c068c1c3
WL
5288 my $worker = sub {
5289 # hold migration lock, this makes sure that nobody create replication snapshots
5290 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
5291 };
5292
5293 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
7e7d7b61
DM
5294 }});
5295
5296__PACKAGE__->register_method({
5297 name => 'delsnapshot',
5298 path => '{vmid}/snapshot/{snapname}',
5299 method => 'DELETE',
5300 protected => 1,
5301 proxyto => 'node',
5302 description => "Delete a VM snapshot.",
5303 permissions => {
f1baf1df 5304 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
7e7d7b61
DM
5305 },
5306 parameters => {
5307 additionalProperties => 0,
5308 properties => {
5309 node => get_standard_option('pve-node'),
335af808 5310 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 5311 snapname => get_standard_option('pve-snapshot-name'),
3ee28e38
DM
5312 force => {
5313 optional => 1,
5314 type => 'boolean',
5315 description => "For removal from config file, even if removing disk snapshots fails.",
5316 },
7e7d7b61
DM
5317 },
5318 },
5319 returns => {
5320 type => 'string',
5321 description => "the task ID.",
5322 },
5323 code => sub {
5324 my ($param) = @_;
5325
5326 my $rpcenv = PVE::RPCEnvironment::get();
5327
5328 my $authuser = $rpcenv->get_user();
5329
5330 my $node = extract_param($param, 'node');
5331
5332 my $vmid = extract_param($param, 'vmid');
5333
5334 my $snapname = extract_param($param, 'snapname');
5335
1770b70f 5336 my $lock_obtained;
fdbbed2f 5337 my $do_delete = sub {
1770b70f 5338 $lock_obtained = 1;
22c377f0 5339 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
b2c9558d 5340 PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force});
7e7d7b61 5341 };
9dbd1ee4 5342
fdbbed2f
FE
5343 my $realcmd = sub {
5344 if ($param->{force}) {
5345 $do_delete->();
5346 } else {
1770b70f
FG
5347 eval { PVE::GuestHelpers::guest_migration_lock($vmid, 10, $do_delete); };
5348 if (my $err = $@) {
5349 die $err if $lock_obtained;
5350 die "Failed to obtain guest migration lock - replication running?\n";
5351 }
fdbbed2f
FE
5352 }
5353 };
5354
7b2257a8 5355 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
9dbd1ee4
AD
5356 }});
5357
04a69bb4
AD
5358__PACKAGE__->register_method({
5359 name => 'template',
5360 path => '{vmid}/template',
5361 method => 'POST',
5362 protected => 1,
5363 proxyto => 'node',
5364 description => "Create a Template.",
b02691d8 5365 permissions => {
7af0a6c8
DM
5366 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
5367 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
b02691d8 5368 },
04a69bb4
AD
5369 parameters => {
5370 additionalProperties => 0,
5371 properties => {
5372 node => get_standard_option('pve-node'),
335af808 5373 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
04a69bb4
AD
5374 disk => {
5375 optional => 1,
5376 type => 'string',
5377 description => "If you want to convert only 1 disk to base image.",
e0fd2b2f 5378 enum => [PVE::QemuServer::Drive::valid_drive_names()],
04a69bb4
AD
5379 },
5380
5381 },
5382 },
b297918c
FG
5383 returns => {
5384 type => 'string',
5385 description => "the task ID.",
5386 },
04a69bb4
AD
5387 code => sub {
5388 my ($param) = @_;
5389
5390 my $rpcenv = PVE::RPCEnvironment::get();
5391
5392 my $authuser = $rpcenv->get_user();
5393
5394 my $node = extract_param($param, 'node');
5395
5396 my $vmid = extract_param($param, 'vmid');
5397
5398 my $disk = extract_param($param, 'disk');
5399
d2ceac56 5400 my $load_and_check = sub {
ffda963f 5401 my $conf = PVE::QemuConfig->load_config($vmid);
04a69bb4 5402
ffda963f 5403 PVE::QemuConfig->check_lock($conf);
04a69bb4 5404
75466c4f 5405 die "unable to create template, because VM contains snapshots\n"
b91c2aae 5406 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
0402a80b 5407
75466c4f 5408 die "you can't convert a template to a template\n"
ffda963f 5409 if PVE::QemuConfig->is_template($conf) && !$disk;
0402a80b 5410
75466c4f 5411 die "you can't convert a VM to template if VM is running\n"
218cab9a 5412 if PVE::QemuServer::check_running($vmid);
35c5fdef 5413
d2ceac56
FG
5414 return $conf;
5415 };
04a69bb4 5416
d2ceac56 5417 $load_and_check->();
75e7e997 5418
d2ceac56
FG
5419 my $realcmd = sub {
5420 PVE::QemuConfig->lock_config($vmid, sub {
5421 my $conf = $load_and_check->();
5422
5423 $conf->{template} = 1;
5424 PVE::QemuConfig->write_config($vmid, $conf);
5425
5426 PVE::QemuServer::template_create($vmid, $conf, $disk);
5427 });
04a69bb4
AD
5428 };
5429
d2ceac56 5430 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
04a69bb4
AD
5431 }});
5432
73709749
ML
5433__PACKAGE__->register_method({
5434 name => 'cloudinit_generated_config_dump',
5435 path => '{vmid}/cloudinit/dump',
5436 method => 'GET',
5437 proxyto => 'node',
5438 description => "Get automatically generated cloudinit config.",
5439 permissions => {
5440 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5441 },
5442 parameters => {
5443 additionalProperties => 0,
5444 properties => {
5445 node => get_standard_option('pve-node'),
5446 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
5447 type => {
5448 description => 'Config type.',
5449 type => 'string',
5450 enum => ['user', 'network', 'meta'],
5451 },
5452 },
5453 },
5454 returns => {
5455 type => 'string',
5456 },
5457 code => sub {
5458 my ($param) = @_;
5459
5460 my $conf = PVE::QemuConfig->load_config($param->{vmid});
5461
5462 return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, $param->{vmid}, $param->{type});
5463 }});
5464
347dc136
FG
5465__PACKAGE__->register_method({
5466 name => 'mtunnel',
5467 path => '{vmid}/mtunnel',
5468 method => 'POST',
5469 protected => 1,
5470 description => 'Migration tunnel endpoint - only for internal use by VM migration.',
5471 permissions => {
5472 check =>
5473 [ 'and',
5474 ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
5475 ['perm', '/', [ 'Sys.Incoming' ]],
5476 ],
5477 description => "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming" .
5478 " on '/'. Further permission checks happen during the actual migration.",
5479 },
5480 parameters => {
5481 additionalProperties => 0,
5482 properties => {
5483 node => get_standard_option('pve-node'),
5484 vmid => get_standard_option('pve-vmid'),
5485 storages => {
5486 type => 'string',
5487 format => 'pve-storage-id-list',
5488 optional => 1,
5489 description => 'List of storages to check permission and availability. Will be checked again for all actually used storages during migration.',
5490 },
06fedff6
FG
5491 bridges => {
5492 type => 'string',
5493 format => 'pve-bridge-id-list',
5494 optional => 1,
5495 description => 'List of network bridges to check availability. Will be checked again for actually used bridges during migration.',
5496 },
347dc136
FG
5497 },
5498 },
5499 returns => {
5500 additionalProperties => 0,
5501 properties => {
5502 upid => { type => 'string' },
5503 ticket => { type => 'string' },
5504 socket => { type => 'string' },
5505 },
5506 },
5507 code => sub {
5508 my ($param) = @_;
5509
5510 my $rpcenv = PVE::RPCEnvironment::get();
5511 my $authuser = $rpcenv->get_user();
5512
5513 my $node = extract_param($param, 'node');
5514 my $vmid = extract_param($param, 'vmid');
5515
5516 my $storages = extract_param($param, 'storages');
06fedff6 5517 my $bridges = extract_param($param, 'bridges');
347dc136
FG
5518
5519 my $nodename = PVE::INotify::nodename();
5520
5521 raise_param_exc({ node => "node needs to be 'localhost' or local hostname '$nodename'" })
5522 if $node ne 'localhost' && $node ne $nodename;
5523
5524 $node = $nodename;
5525
5526 my $storecfg = PVE::Storage::config();
5527 foreach my $storeid (PVE::Tools::split_list($storages)) {
5528 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storeid, $node);
5529 }
5530
06fedff6
FG
5531 foreach my $bridge (PVE::Tools::split_list($bridges)) {
5532 PVE::Network::read_bridge_mtu($bridge);
5533 }
5534
347dc136
FG
5535 PVE::Cluster::check_cfs_quorum();
5536
5537 my $lock = 'create';
5538 eval { PVE::QemuConfig->create_and_lock_config($vmid, 0, $lock); };
5539
5540 raise_param_exc({ vmid => "unable to create empty VM config - $@"})
5541 if $@;
5542
5543 my $realcmd = sub {
5544 my $state = {
5545 storecfg => PVE::Storage::config(),
5546 lock => $lock,
5547 vmid => $vmid,
5548 };
5549
5550 my $run_locked = sub {
5551 my ($code, $params) = @_;
5552 return PVE::QemuConfig->lock_config($state->{vmid}, sub {
5553 my $conf = PVE::QemuConfig->load_config($state->{vmid});
5554
5555 $state->{conf} = $conf;
5556
5557 die "Encountered wrong lock - aborting mtunnel command handling.\n"
5558 if $state->{lock} && !PVE::QemuConfig->has_lock($conf, $state->{lock});
5559
5560 return $code->($params);
5561 });
5562 };
5563
5564 my $cmd_desc = {
5565 config => {
5566 conf => {
5567 type => 'string',
5568 description => 'Full VM config, adapted for target cluster/node',
5569 },
5570 'firewall-config' => {
5571 type => 'string',
5572 description => 'VM firewall config',
5573 optional => 1,
5574 },
5575 },
5576 disk => {
5577 format => PVE::JSONSchema::get_standard_option('pve-qm-image-format'),
5578 storage => {
5579 type => 'string',
5580 format => 'pve-storage-id',
5581 },
5582 drive => {
5583 type => 'object',
5584 description => 'parsed drive information without volid and format',
5585 },
5586 },
5587 start => {
5588 start_params => {
5589 type => 'object',
5590 description => 'params passed to vm_start_nolock',
5591 },
5592 migrate_opts => {
5593 type => 'object',
5594 description => 'migrate_opts passed to vm_start_nolock',
5595 },
5596 },
5597 ticket => {
5598 path => {
5599 type => 'string',
5600 description => 'socket path for which the ticket should be valid. must be known to current mtunnel instance.',
5601 },
5602 },
5603 quit => {
5604 cleanup => {
5605 type => 'boolean',
5606 description => 'remove VM config and disks, aborting migration',
5607 default => 0,
5608 },
5609 },
5610 'disk-import' => $PVE::StorageTunnel::cmd_schema->{'disk-import'},
5611 'query-disk-import' => $PVE::StorageTunnel::cmd_schema->{'query-disk-import'},
5612 bwlimit => $PVE::StorageTunnel::cmd_schema->{bwlimit},
5613 };
5614
5615 my $cmd_handlers = {
5616 'version' => sub {
5617 # compared against other end's version
5618 # bump/reset for breaking changes
5619 # bump/bump for opt-in changes
5620 return {
eef93bc5 5621 api => $PVE::QemuMigrate::WS_TUNNEL_VERSION,
347dc136
FG
5622 age => 0,
5623 };
5624 },
5625 'config' => sub {
5626 my ($params) = @_;
5627
5628 # parse and write out VM FW config if given
5629 if (my $fw_conf = $params->{'firewall-config'}) {
5630 my ($path, $fh) = PVE::Tools::tempfile_contents($fw_conf, 700);
5631
5632 my $empty_conf = {
5633 rules => [],
5634 options => {},
5635 aliases => {},
5636 ipset => {} ,
5637 ipset_comments => {},
5638 };
5639 my $cluster_fw_conf = PVE::Firewall::load_clusterfw_conf();
5640
5641 # TODO: add flag for strict parsing?
5642 # TODO: add import sub that does all this given raw content?
5643 my $vmfw_conf = PVE::Firewall::generic_fw_config_parser($path, $cluster_fw_conf, $empty_conf, 'vm');
5644 $vmfw_conf->{vmid} = $state->{vmid};
5645 PVE::Firewall::save_vmfw_conf($state->{vmid}, $vmfw_conf);
5646
5647 $state->{cleanup}->{fw} = 1;
5648 }
5649
5650 my $conf_fn = "incoming/qemu-server/$state->{vmid}.conf";
5651 my $new_conf = PVE::QemuServer::parse_vm_config($conf_fn, $params->{conf}, 1);
5652 delete $new_conf->{lock};
5653 delete $new_conf->{digest};
5654
5655 # TODO handle properly?
5656 delete $new_conf->{snapshots};
5657 delete $new_conf->{parent};
5658 delete $new_conf->{pending};
5659
5660 # not handled by update_vm_api
5661 my $vmgenid = delete $new_conf->{vmgenid};
5662 my $meta = delete $new_conf->{meta};
5663 my $cloudinit = delete $new_conf->{cloudinit}; # this is informational only
5664 $new_conf->{skip_cloud_init} = 1; # re-use image from source side
5665
5666 $new_conf->{vmid} = $state->{vmid};
5667 $new_conf->{node} = $node;
5668
5669 PVE::QemuConfig->remove_lock($state->{vmid}, 'create');
5670
5671 eval {
5672 $update_vm_api->($new_conf, 1);
5673 };
5674 if (my $err = $@) {
5675 # revert to locked previous config
5676 my $conf = PVE::QemuConfig->load_config($state->{vmid});
5677 $conf->{lock} = 'create';
5678 PVE::QemuConfig->write_config($state->{vmid}, $conf);
5679
5680 die $err;
5681 }
5682
5683 my $conf = PVE::QemuConfig->load_config($state->{vmid});
5684 $conf->{lock} = 'migrate';
5685 $conf->{vmgenid} = $vmgenid if defined($vmgenid);
5686 $conf->{meta} = $meta if defined($meta);
5687 $conf->{cloudinit} = $cloudinit if defined($cloudinit);
5688 PVE::QemuConfig->write_config($state->{vmid}, $conf);
5689
5690 $state->{lock} = 'migrate';
5691
5692 return;
5693 },
5694 'bwlimit' => sub {
5695 my ($params) = @_;
5696 return PVE::StorageTunnel::handle_bwlimit($params);
5697 },
5698 'disk' => sub {
5699 my ($params) = @_;
5700
5701 my $format = $params->{format};
5702 my $storeid = $params->{storage};
5703 my $drive = $params->{drive};
5704
5705 $check_storage_access_migrate->($rpcenv, $authuser, $state->{storecfg}, $storeid, $node);
5706
5707 my $storagemap = {
5708 default => $storeid,
5709 };
5710
5711 my $source_volumes = {
5712 'disk' => [
5713 undef,
5714 $storeid,
347dc136
FG
5715 $drive,
5716 0,
5717 $format,
5718 ],
5719 };
5720
5721 my $res = PVE::QemuServer::vm_migrate_alloc_nbd_disks($state->{storecfg}, $state->{vmid}, $source_volumes, $storagemap);
5722 if (defined($res->{disk})) {
5723 $state->{cleanup}->{volumes}->{$res->{disk}->{volid}} = 1;
5724 return $res->{disk};
5725 } else {
5726 die "failed to allocate NBD disk..\n";
5727 }
5728 },
5729 'disk-import' => sub {
5730 my ($params) = @_;
5731
5732 $check_storage_access_migrate->(
5733 $rpcenv,
5734 $authuser,
5735 $state->{storecfg},
5736 $params->{storage},
5737 $node
5738 );
5739
5740 $params->{unix} = "/run/qemu-server/$state->{vmid}.storage";
5741
5742 return PVE::StorageTunnel::handle_disk_import($state, $params);
5743 },
5744 'query-disk-import' => sub {
5745 my ($params) = @_;
5746
5747 return PVE::StorageTunnel::handle_query_disk_import($state, $params);
5748 },
5749 'start' => sub {
5750 my ($params) = @_;
5751
5752 my $info = PVE::QemuServer::vm_start_nolock(
5753 $state->{storecfg},
5754 $state->{vmid},
5755 $state->{conf},
5756 $params->{start_params},
5757 $params->{migrate_opts},
5758 );
5759
5760
5761 if ($info->{migrate}->{proto} ne 'unix') {
5762 PVE::QemuServer::vm_stop(undef, $state->{vmid}, 1, 1);
5763 die "migration over non-UNIX sockets not possible\n";
5764 }
5765
5766 my $socket = $info->{migrate}->{addr};
5767 chown $state->{socket_uid}, -1, $socket;
5768 $state->{sockets}->{$socket} = 1;
5769
5770 my $unix_sockets = $info->{migrate}->{unix_sockets};
5771 foreach my $socket (@$unix_sockets) {
5772 chown $state->{socket_uid}, -1, $socket;
5773 $state->{sockets}->{$socket} = 1;
5774 }
5775 return $info;
5776 },
5777 'fstrim' => sub {
5778 if (PVE::QemuServer::qga_check_running($state->{vmid})) {
5779 eval { mon_cmd($state->{vmid}, "guest-fstrim") };
5780 warn "fstrim failed: $@\n" if $@;
5781 }
5782 return;
5783 },
5784 'stop' => sub {
5785 PVE::QemuServer::vm_stop(undef, $state->{vmid}, 1, 1);
5786 return;
5787 },
5788 'nbdstop' => sub {
5789 PVE::QemuServer::nbd_stop($state->{vmid});
5790 return;
5791 },
5792 'resume' => sub {
5793 if (PVE::QemuServer::Helpers::vm_running_locally($state->{vmid})) {
5794 PVE::QemuServer::vm_resume($state->{vmid}, 1, 1);
5795 } else {
5796 die "VM $state->{vmid} not running\n";
5797 }
5798 return;
5799 },
5800 'unlock' => sub {
5801 PVE::QemuConfig->remove_lock($state->{vmid}, $state->{lock});
5802 delete $state->{lock};
5803 return;
5804 },
5805 'ticket' => sub {
5806 my ($params) = @_;
5807
5808 my $path = $params->{path};
5809
5810 die "Not allowed to generate ticket for unknown socket '$path'\n"
5811 if !defined($state->{sockets}->{$path});
5812
5813 return { ticket => PVE::AccessControl::assemble_tunnel_ticket($authuser, "/socket/$path") };
5814 },
5815 'quit' => sub {
5816 my ($params) = @_;
5817
5818 if ($params->{cleanup}) {
5819 if ($state->{cleanup}->{fw}) {
5820 PVE::Firewall::remove_vmfw_conf($state->{vmid});
5821 }
5822
5823 for my $volid (keys $state->{cleanup}->{volumes}->%*) {
5824 print "freeing volume '$volid' as part of cleanup\n";
5825 eval { PVE::Storage::vdisk_free($state->{storecfg}, $volid) };
5826 warn $@ if $@;
5827 }
5828
5829 PVE::QemuServer::destroy_vm($state->{storecfg}, $state->{vmid}, 1);
5830 }
5831
5832 print "switching to exit-mode, waiting for client to disconnect\n";
5833 $state->{exit} = 1;
5834 return;
5835 },
5836 };
5837
5838 $run_locked->(sub {
5839 my $socket_addr = "/run/qemu-server/$state->{vmid}.mtunnel";
5840 unlink $socket_addr;
5841
5842 $state->{socket} = IO::Socket::UNIX->new(
5843 Type => SOCK_STREAM(),
5844 Local => $socket_addr,
5845 Listen => 1,
5846 );
5847
5848 $state->{socket_uid} = getpwnam('www-data')
5849 or die "Failed to resolve user 'www-data' to numeric UID\n";
5850 chown $state->{socket_uid}, -1, $socket_addr;
5851 });
5852
5853 print "mtunnel started\n";
5854
5855 my $conn = eval { PVE::Tools::run_with_timeout(300, sub { $state->{socket}->accept() }) };
5856 if ($@) {
5857 warn "Failed to accept tunnel connection - $@\n";
5858
5859 warn "Removing tunnel socket..\n";
5860 unlink $state->{socket};
5861
5862 warn "Removing temporary VM config..\n";
5863 $run_locked->(sub {
5864 PVE::QemuServer::destroy_vm($state->{storecfg}, $state->{vmid}, 1);
5865 });
5866
5867 die "Exiting mtunnel\n";
5868 }
5869
5870 $state->{conn} = $conn;
5871
5872 my $reply_err = sub {
5873 my ($msg) = @_;
5874
5875 my $reply = JSON::encode_json({
5876 success => JSON::false,
5877 msg => $msg,
5878 });
5879 $conn->print("$reply\n");
5880 $conn->flush();
5881 };
5882
5883 my $reply_ok = sub {
5884 my ($res) = @_;
5885
5886 $res->{success} = JSON::true;
5887 my $reply = JSON::encode_json($res);
5888 $conn->print("$reply\n");
5889 $conn->flush();
5890 };
5891
5892 while (my $line = <$conn>) {
5893 chomp $line;
5894
5895 # untaint, we validate below if needed
5896 ($line) = $line =~ /^(.*)$/;
5897 my $parsed = eval { JSON::decode_json($line) };
5898 if ($@) {
5899 $reply_err->("failed to parse command - $@");
5900 next;
5901 }
5902
5903 my $cmd = delete $parsed->{cmd};
5904 if (!defined($cmd)) {
5905 $reply_err->("'cmd' missing");
5906 } elsif ($state->{exit}) {
5907 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd not possible");
5908 next;
5909 } elsif (my $handler = $cmd_handlers->{$cmd}) {
5910 print "received command '$cmd'\n";
5911 eval {
5912 if ($cmd_desc->{$cmd}) {
5913 PVE::JSONSchema::validate($parsed, $cmd_desc->{$cmd});
5914 } else {
5915 $parsed = {};
5916 }
5917 my $res = $run_locked->($handler, $parsed);
5918 $reply_ok->($res);
5919 };
5920 $reply_err->("failed to handle '$cmd' command - $@")
5921 if $@;
5922 } else {
5923 $reply_err->("unknown command '$cmd' given");
5924 }
5925 }
5926
5927 if ($state->{exit}) {
5928 print "mtunnel exited\n";
5929 } else {
5930 die "mtunnel exited unexpectedly\n";
5931 }
5932 };
5933
5934 my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
5935 my $ticket = PVE::AccessControl::assemble_tunnel_ticket($authuser, "/socket/$socket_addr");
5936 my $upid = $rpcenv->fork_worker('qmtunnel', $vmid, $authuser, $realcmd);
5937
5938 return {
5939 ticket => $ticket,
5940 upid => $upid,
5941 socket => $socket_addr,
5942 };
5943 }});
5944
5945__PACKAGE__->register_method({
5946 name => 'mtunnelwebsocket',
5947 path => '{vmid}/mtunnelwebsocket',
5948 method => 'GET',
5949 permissions => {
5950 description => "You need to pass a ticket valid for the selected socket. Tickets can be created via the mtunnel API call, which will check permissions accordingly.",
5951 user => 'all', # check inside
5952 },
5953 description => 'Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.',
5954 parameters => {
5955 additionalProperties => 0,
5956 properties => {
5957 node => get_standard_option('pve-node'),
5958 vmid => get_standard_option('pve-vmid'),
5959 socket => {
5960 type => "string",
5961 description => "unix socket to forward to",
5962 },
5963 ticket => {
5964 type => "string",
5965 description => "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command",
5966 },
5967 },
5968 },
5969 returns => {
5970 type => "object",
5971 properties => {
5972 port => { type => 'string', optional => 1 },
5973 socket => { type => 'string', optional => 1 },
5974 },
5975 },
5976 code => sub {
5977 my ($param) = @_;
5978
5979 my $rpcenv = PVE::RPCEnvironment::get();
5980 my $authuser = $rpcenv->get_user();
5981
5982 my $nodename = PVE::INotify::nodename();
5983 my $node = extract_param($param, 'node');
5984
5985 raise_param_exc({ node => "node needs to be 'localhost' or local hostname '$nodename'" })
5986 if $node ne 'localhost' && $node ne $nodename;
5987
5988 my $vmid = $param->{vmid};
5989 # check VM exists
5990 PVE::QemuConfig->load_config($vmid);
5991
5992 my $socket = $param->{socket};
5993 PVE::AccessControl::verify_tunnel_ticket($param->{ticket}, $authuser, "/socket/$socket");
5994
5995 return { socket => $socket };
5996 }});
5997
1e3baf05 59981;