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