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