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