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