]> git.proxmox.com Git - qemu-server.git/blame - PVE/API2/Qemu.pm
api: add remote migrate endpoint
[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
4546 # FIXME: fork worker hear to avoid timeout? or poll these periodically
4547 # in pvestatd and access cached info here? all of the below is actually
4548 # checked at the remote end anyway once we call the mtunnel endpoint,
4549 # we could also punt it to the client and not do it here at all..
4550 my $resources = $api_client->get("/cluster/resources", { type => 'vm' });
4551 if (grep { defined($_->{vmid}) && $_->{vmid} eq $target_vmid } @$resources) {
4552 raise_param_exc({ target_vmid => "Guest with ID '$target_vmid' already exists on remote cluster" });
4553 }
4554
4555 my $storages = $api_client->get("/nodes/localhost/storage", { enabled => 1 });
4556
4557 my $storecfg = PVE::Storage::config();
4558 my $target_storage = extract_param($param, 'target-storage');
4559 my $storagemap = eval { PVE::JSONSchema::parse_idmap($target_storage, 'pve-storage-id') };
4560 raise_param_exc({ 'target-storage' => "failed to parse storage map: $@" })
4561 if $@;
4562
4563 my $target_bridge = extract_param($param, 'target-bridge');
4564 my $bridgemap = eval { PVE::JSONSchema::parse_idmap($target_bridge, 'pve-bridge-id') };
4565 raise_param_exc({ 'target-bridge' => "failed to parse bridge map: $@" })
4566 if $@;
4567
4568 my $check_remote_storage = sub {
4569 my ($storage) = @_;
4570 my $found = [ grep { $_->{storage} eq $storage } @$storages ];
4571 die "remote: storage '$storage' does not exist!\n"
4572 if !@$found;
4573
4574 $found = @$found[0];
4575
4576 my $content_types = [ PVE::Tools::split_list($found->{content}) ];
4577 die "remote: storage '$storage' cannot store images\n"
4578 if !grep { $_ eq 'images' } @$content_types;
4579 };
4580
4581 foreach my $target_sid (values %{$storagemap->{entries}}) {
4582 $check_remote_storage->($target_sid);
4583 }
4584
4585 $check_remote_storage->($storagemap->{default})
4586 if $storagemap->{default};
4587
4588 die "remote migration requires explicit storage mapping!\n"
4589 if $storagemap->{identity};
4590
4591 $param->{storagemap} = $storagemap;
4592 $param->{bridgemap} = $bridgemap;
4593 $param->{remote} = {
4594 conn => $conn_args, # re-use fingerprint for tunnel
4595 client => $api_client,
4596 vmid => $target_vmid,
4597 };
4598 $param->{migration_type} = 'websocket';
4599 $param->{'with-local-disks'} = 1;
4600 $param->{delete} = $delete if $delete;
4601
4602 my $cluster_status = $api_client->get("/cluster/status");
4603 my $target_node;
4604 foreach my $entry (@$cluster_status) {
4605 next if $entry->{type} ne 'node';
4606 if ($entry->{local}) {
4607 $target_node = $entry->{name};
4608 last;
4609 }
4610 }
4611
4612 die "couldn't determine endpoint's node name\n"
4613 if !defined($target_node);
4614
4615 my $realcmd = sub {
4616 PVE::QemuMigrate->migrate($target_node, $remote->{host}, $source_vmid, $param);
4617 };
4618
4619 my $worker = sub {
4620 return PVE::GuestHelpers::guest_migration_lock($source_vmid, 10, $realcmd);
4621 };
4622
4623 return $rpcenv->fork_worker('qmigrate', $source_vmid, $authuser, $worker);
4624 }});
4625
91c94f0a 4626__PACKAGE__->register_method({
afdb31d5
DM
4627 name => 'monitor',
4628 path => '{vmid}/monitor',
91c94f0a
DM
4629 method => 'POST',
4630 protected => 1,
4631 proxyto => 'node',
4632 description => "Execute Qemu monitor commands.",
a0d1b1a2 4633 permissions => {
a8f2f427 4634 description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
c07a9e3d 4635 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
a0d1b1a2 4636 },
91c94f0a 4637 parameters => {
3326ae19 4638 additionalProperties => 0,
91c94f0a
DM
4639 properties => {
4640 node => get_standard_option('pve-node'),
4641 vmid => get_standard_option('pve-vmid'),
4642 command => {
4643 type => 'string',
4644 description => "The monitor command.",
4645 }
4646 },
4647 },
4648 returns => { type => 'string'},
4649 code => sub {
4650 my ($param) = @_;
4651
a8f2f427
FG
4652 my $rpcenv = PVE::RPCEnvironment::get();
4653 my $authuser = $rpcenv->get_user();
4654
4655 my $is_ro = sub {
4656 my $command = shift;
4657 return $command =~ m/^\s*info(\s+|$)/
4658 || $command =~ m/^\s*help\s*$/;
4659 };
4660
4661 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4662 if !&$is_ro($param->{command});
4663
91c94f0a
DM
4664 my $vmid = $param->{vmid};
4665
ffda963f 4666 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
91c94f0a
DM
4667
4668 my $res = '';
4669 eval {
0a13e08e 4670 $res = PVE::QemuServer::Monitor::hmp_cmd($vmid, $param->{command});
91c94f0a
DM
4671 };
4672 $res = "ERROR: $@" if $@;
4673
4674 return $res;
4675 }});
4676
0d02881c
AD
4677__PACKAGE__->register_method({
4678 name => 'resize_vm',
614e3941 4679 path => '{vmid}/resize',
0d02881c
AD
4680 method => 'PUT',
4681 protected => 1,
4682 proxyto => 'node',
2f48a4f5 4683 description => "Extend volume size.",
0d02881c 4684 permissions => {
3b2773f6 4685 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
0d02881c
AD
4686 },
4687 parameters => {
3326ae19
TL
4688 additionalProperties => 0,
4689 properties => {
2f48a4f5 4690 node => get_standard_option('pve-node'),
335af808 4691 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2f48a4f5
DM
4692 skiplock => get_standard_option('skiplock'),
4693 disk => {
4694 type => 'string',
4695 description => "The disk you want to resize.",
e0fd2b2f 4696 enum => [PVE::QemuServer::Drive::valid_drive_names()],
2f48a4f5
DM
4697 },
4698 size => {
4699 type => 'string',
f91b2e45 4700 pattern => '\+?\d+(\.\d+)?[KMGT]?',
e248477e 4701 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
4702 },
4703 digest => {
4704 type => 'string',
4705 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4706 maxLength => 40,
4707 optional => 1,
4708 },
4709 },
0d02881c
AD
4710 },
4711 returns => { type => 'null'},
4712 code => sub {
4713 my ($param) = @_;
4714
4715 my $rpcenv = PVE::RPCEnvironment::get();
4716
4717 my $authuser = $rpcenv->get_user();
4718
4719 my $node = extract_param($param, 'node');
4720
4721 my $vmid = extract_param($param, 'vmid');
4722
4723 my $digest = extract_param($param, 'digest');
4724
2f48a4f5 4725 my $disk = extract_param($param, 'disk');
75466c4f 4726
2f48a4f5 4727 my $sizestr = extract_param($param, 'size');
0d02881c 4728
f91b2e45 4729 my $skiplock = extract_param($param, 'skiplock');
0d02881c
AD
4730 raise_param_exc({ skiplock => "Only root may use this option." })
4731 if $skiplock && $authuser ne 'root@pam';
4732
0d02881c
AD
4733 my $storecfg = PVE::Storage::config();
4734
0d02881c
AD
4735 my $updatefn = sub {
4736
ffda963f 4737 my $conf = PVE::QemuConfig->load_config($vmid);
0d02881c
AD
4738
4739 die "checksum missmatch (file change by other user?)\n"
4740 if $digest && $digest ne $conf->{digest};
ffda963f 4741 PVE::QemuConfig->check_lock($conf) if !$skiplock;
0d02881c 4742
f91b2e45
DM
4743 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4744
4745 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
4746
d662790a
WL
4747 my (undef, undef, undef, undef, undef, undef, $format) =
4748 PVE::Storage::parse_volname($storecfg, $drive->{file});
4749
c2ed338e 4750 die "can't resize volume: $disk if snapshot exists\n"
d662790a
WL
4751 if %{$conf->{snapshots}} && $format eq 'qcow2';
4752
f91b2e45
DM
4753 my $volid = $drive->{file};
4754
4755 die "disk '$disk' has no associated volume\n" if !$volid;
4756
4757 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
4758
4759 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
4760
4761 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4762
b572a606 4763 PVE::Storage::activate_volumes($storecfg, [$volid]);
f91b2e45
DM
4764 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
4765
ed94b2ad 4766 die "Could not determine current size of volume '$volid'\n" if !defined($size);
f8b829aa 4767
f91b2e45
DM
4768 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
4769 my ($ext, $newsize, $unit) = ($1, $2, $4);
4770 if ($unit) {
4771 if ($unit eq 'K') {
4772 $newsize = $newsize * 1024;
4773 } elsif ($unit eq 'M') {
4774 $newsize = $newsize * 1024 * 1024;
4775 } elsif ($unit eq 'G') {
4776 $newsize = $newsize * 1024 * 1024 * 1024;
4777 } elsif ($unit eq 'T') {
4778 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
4779 }
4780 }
4781 $newsize += $size if $ext;
4782 $newsize = int($newsize);
4783
9a478b17 4784 die "shrinking disks is not supported\n" if $newsize < $size;
f91b2e45
DM
4785
4786 return if $size == $newsize;
4787
2f48a4f5 4788 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
0d02881c 4789
f91b2e45 4790 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
75466c4f 4791
e29e5be6 4792 $drive->{size} = $newsize;
71c58bb7 4793 $conf->{$disk} = PVE::QemuServer::print_drive($drive);
f91b2e45 4794
ffda963f 4795 PVE::QemuConfig->write_config($vmid, $conf);
f91b2e45 4796 };
0d02881c 4797
ffda963f 4798 PVE::QemuConfig->lock_config($vmid, $updatefn);
d1c1af4b 4799 return;
0d02881c
AD
4800 }});
4801
9dbd1ee4 4802__PACKAGE__->register_method({
7e7d7b61 4803 name => 'snapshot_list',
9dbd1ee4 4804 path => '{vmid}/snapshot',
7e7d7b61
DM
4805 method => 'GET',
4806 description => "List all snapshots.",
4807 permissions => {
4808 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
4809 },
4810 proxyto => 'node',
4811 protected => 1, # qemu pid files are only readable by root
4812 parameters => {
3326ae19 4813 additionalProperties => 0,
7e7d7b61 4814 properties => {
e261de40 4815 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
7e7d7b61
DM
4816 node => get_standard_option('pve-node'),
4817 },
4818 },
4819 returns => {
4820 type => 'array',
4821 items => {
4822 type => "object",
ce9b0a38
DM
4823 properties => {
4824 name => {
4825 description => "Snapshot identifier. Value 'current' identifies the current VM.",
4826 type => 'string',
4827 },
4828 vmstate => {
4829 description => "Snapshot includes RAM.",
4830 type => 'boolean',
4831 optional => 1,
4832 },
4833 description => {
4834 description => "Snapshot description.",
4835 type => 'string',
4836 },
4837 snaptime => {
4838 description => "Snapshot creation time",
4839 type => 'integer',
4840 renderer => 'timestamp',
4841 optional => 1,
4842 },
4843 parent => {
4844 description => "Parent snapshot identifier.",
4845 type => 'string',
4846 optional => 1,
4847 },
4848 },
7e7d7b61
DM
4849 },
4850 links => [ { rel => 'child', href => "{name}" } ],
4851 },
4852 code => sub {
4853 my ($param) = @_;
4854
6aa4651b
DM
4855 my $vmid = $param->{vmid};
4856
ffda963f 4857 my $conf = PVE::QemuConfig->load_config($vmid);
7e7d7b61
DM
4858 my $snaphash = $conf->{snapshots} || {};
4859
4860 my $res = [];
4861
4862 foreach my $name (keys %$snaphash) {
0ea6bc69 4863 my $d = $snaphash->{$name};
75466c4f
DM
4864 my $item = {
4865 name => $name,
4866 snaptime => $d->{snaptime} || 0,
6aa4651b 4867 vmstate => $d->{vmstate} ? 1 : 0,
982c7f12
DM
4868 description => $d->{description} || '',
4869 };
0ea6bc69 4870 $item->{parent} = $d->{parent} if $d->{parent};
3ee28e38 4871 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
0ea6bc69
DM
4872 push @$res, $item;
4873 }
4874
6aa4651b 4875 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
ce9b0a38
DM
4876 my $current = {
4877 name => 'current',
4878 digest => $conf->{digest},
4879 running => $running,
4880 description => "You are here!",
4881 };
d1914468
DM
4882 $current->{parent} = $conf->{parent} if $conf->{parent};
4883
4884 push @$res, $current;
7e7d7b61
DM
4885
4886 return $res;
4887 }});
4888
4889__PACKAGE__->register_method({
4890 name => 'snapshot',
4891 path => '{vmid}/snapshot',
4892 method => 'POST',
9dbd1ee4
AD
4893 protected => 1,
4894 proxyto => 'node',
4895 description => "Snapshot a VM.",
4896 permissions => {
f1baf1df 4897 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
9dbd1ee4
AD
4898 },
4899 parameters => {
4900 additionalProperties => 0,
4901 properties => {
4902 node => get_standard_option('pve-node'),
335af808 4903 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 4904 snapname => get_standard_option('pve-snapshot-name'),
9dbd1ee4
AD
4905 vmstate => {
4906 optional => 1,
4907 type => 'boolean',
4908 description => "Save the vmstate",
4909 },
782f4f75
DM
4910 description => {
4911 optional => 1,
4912 type => 'string',
4913 description => "A textual description or comment.",
4914 },
9dbd1ee4
AD
4915 },
4916 },
7e7d7b61
DM
4917 returns => {
4918 type => 'string',
4919 description => "the task ID.",
4920 },
9dbd1ee4
AD
4921 code => sub {
4922 my ($param) = @_;
4923
4924 my $rpcenv = PVE::RPCEnvironment::get();
4925
4926 my $authuser = $rpcenv->get_user();
4927
4928 my $node = extract_param($param, 'node');
4929
4930 my $vmid = extract_param($param, 'vmid');
4931
9dbd1ee4
AD
4932 my $snapname = extract_param($param, 'snapname');
4933
d1914468
DM
4934 die "unable to use snapshot name 'current' (reserved name)\n"
4935 if $snapname eq 'current';
4936
a85c6be1
FG
4937 die "unable to use snapshot name 'pending' (reserved name)\n"
4938 if lc($snapname) eq 'pending';
4939
7e7d7b61 4940 my $realcmd = sub {
22c377f0 4941 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
c2ed338e 4942 PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate},
af9110dd 4943 $param->{description});
7e7d7b61
DM
4944 };
4945
4946 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
4947 }});
4948
154ccdcd
DM
4949__PACKAGE__->register_method({
4950 name => 'snapshot_cmd_idx',
4951 path => '{vmid}/snapshot/{snapname}',
4952 description => '',
4953 method => 'GET',
4954 permissions => {
4955 user => 'all',
4956 },
4957 parameters => {
3326ae19 4958 additionalProperties => 0,
154ccdcd
DM
4959 properties => {
4960 vmid => get_standard_option('pve-vmid'),
4961 node => get_standard_option('pve-node'),
8abd398b 4962 snapname => get_standard_option('pve-snapshot-name'),
154ccdcd
DM
4963 },
4964 },
4965 returns => {
4966 type => 'array',
4967 items => {
4968 type => "object",
4969 properties => {},
4970 },
4971 links => [ { rel => 'child', href => "{cmd}" } ],
4972 },
4973 code => sub {
4974 my ($param) = @_;
4975
4976 my $res = [];
4977
4978 push @$res, { cmd => 'rollback' };
d788cea6 4979 push @$res, { cmd => 'config' };
154ccdcd
DM
4980
4981 return $res;
4982 }});
4983
d788cea6
DM
4984__PACKAGE__->register_method({
4985 name => 'update_snapshot_config',
4986 path => '{vmid}/snapshot/{snapname}/config',
4987 method => 'PUT',
4988 protected => 1,
4989 proxyto => 'node',
4990 description => "Update snapshot metadata.",
4991 permissions => {
4992 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
4993 },
4994 parameters => {
4995 additionalProperties => 0,
4996 properties => {
4997 node => get_standard_option('pve-node'),
4998 vmid => get_standard_option('pve-vmid'),
4999 snapname => get_standard_option('pve-snapshot-name'),
5000 description => {
5001 optional => 1,
5002 type => 'string',
5003 description => "A textual description or comment.",
5004 },
5005 },
5006 },
5007 returns => { type => 'null' },
5008 code => sub {
5009 my ($param) = @_;
5010
5011 my $rpcenv = PVE::RPCEnvironment::get();
5012
5013 my $authuser = $rpcenv->get_user();
5014
5015 my $vmid = extract_param($param, 'vmid');
5016
5017 my $snapname = extract_param($param, 'snapname');
5018
d1c1af4b 5019 return if !defined($param->{description});
d788cea6
DM
5020
5021 my $updatefn = sub {
5022
ffda963f 5023 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6 5024
ffda963f 5025 PVE::QemuConfig->check_lock($conf);
d788cea6
DM
5026
5027 my $snap = $conf->{snapshots}->{$snapname};
5028
75466c4f
DM
5029 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5030
d788cea6
DM
5031 $snap->{description} = $param->{description} if defined($param->{description});
5032
ffda963f 5033 PVE::QemuConfig->write_config($vmid, $conf);
d788cea6
DM
5034 };
5035
ffda963f 5036 PVE::QemuConfig->lock_config($vmid, $updatefn);
d788cea6 5037
d1c1af4b 5038 return;
d788cea6
DM
5039 }});
5040
5041__PACKAGE__->register_method({
5042 name => 'get_snapshot_config',
5043 path => '{vmid}/snapshot/{snapname}/config',
5044 method => 'GET',
5045 proxyto => 'node',
5046 description => "Get snapshot configuration",
5047 permissions => {
65204e92 5048 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any => 1],
d788cea6
DM
5049 },
5050 parameters => {
5051 additionalProperties => 0,
5052 properties => {
5053 node => get_standard_option('pve-node'),
5054 vmid => get_standard_option('pve-vmid'),
5055 snapname => get_standard_option('pve-snapshot-name'),
5056 },
5057 },
5058 returns => { type => "object" },
5059 code => sub {
5060 my ($param) = @_;
5061
5062 my $rpcenv = PVE::RPCEnvironment::get();
5063
5064 my $authuser = $rpcenv->get_user();
5065
5066 my $vmid = extract_param($param, 'vmid');
5067
5068 my $snapname = extract_param($param, 'snapname');
5069
ffda963f 5070 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6
DM
5071
5072 my $snap = $conf->{snapshots}->{$snapname};
5073
75466c4f
DM
5074 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5075
d788cea6
DM
5076 return $snap;
5077 }});
5078
7e7d7b61
DM
5079__PACKAGE__->register_method({
5080 name => 'rollback',
154ccdcd 5081 path => '{vmid}/snapshot/{snapname}/rollback',
7e7d7b61
DM
5082 method => 'POST',
5083 protected => 1,
5084 proxyto => 'node',
5085 description => "Rollback VM state to specified snapshot.",
5086 permissions => {
c268337d 5087 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
7e7d7b61
DM
5088 },
5089 parameters => {
5090 additionalProperties => 0,
5091 properties => {
5092 node => get_standard_option('pve-node'),
335af808 5093 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 5094 snapname => get_standard_option('pve-snapshot-name'),
76b29aaf
SH
5095 start => {
5096 type => 'boolean',
5097 description => "Whether the VM should get started after rolling back successfully",
5098 optional => 1,
5099 default => 0,
5100 },
7e7d7b61
DM
5101 },
5102 },
5103 returns => {
5104 type => 'string',
5105 description => "the task ID.",
5106 },
5107 code => sub {
5108 my ($param) = @_;
5109
5110 my $rpcenv = PVE::RPCEnvironment::get();
5111
5112 my $authuser = $rpcenv->get_user();
5113
5114 my $node = extract_param($param, 'node');
5115
5116 my $vmid = extract_param($param, 'vmid');
5117
5118 my $snapname = extract_param($param, 'snapname');
5119
7e7d7b61 5120 my $realcmd = sub {
22c377f0 5121 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
b2c9558d 5122 PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
76b29aaf
SH
5123
5124 if ($param->{start}) {
5125 PVE::API2::Qemu->vm_start({ vmid => $vmid, node => $node });
5126 }
7e7d7b61
DM
5127 };
5128
c068c1c3
WL
5129 my $worker = sub {
5130 # hold migration lock, this makes sure that nobody create replication snapshots
5131 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
5132 };
5133
5134 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
7e7d7b61
DM
5135 }});
5136
5137__PACKAGE__->register_method({
5138 name => 'delsnapshot',
5139 path => '{vmid}/snapshot/{snapname}',
5140 method => 'DELETE',
5141 protected => 1,
5142 proxyto => 'node',
5143 description => "Delete a VM snapshot.",
5144 permissions => {
f1baf1df 5145 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
7e7d7b61
DM
5146 },
5147 parameters => {
5148 additionalProperties => 0,
5149 properties => {
5150 node => get_standard_option('pve-node'),
335af808 5151 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 5152 snapname => get_standard_option('pve-snapshot-name'),
3ee28e38
DM
5153 force => {
5154 optional => 1,
5155 type => 'boolean',
5156 description => "For removal from config file, even if removing disk snapshots fails.",
5157 },
7e7d7b61
DM
5158 },
5159 },
5160 returns => {
5161 type => 'string',
5162 description => "the task ID.",
5163 },
5164 code => sub {
5165 my ($param) = @_;
5166
5167 my $rpcenv = PVE::RPCEnvironment::get();
5168
5169 my $authuser = $rpcenv->get_user();
5170
5171 my $node = extract_param($param, 'node');
5172
5173 my $vmid = extract_param($param, 'vmid');
5174
5175 my $snapname = extract_param($param, 'snapname');
5176
1770b70f 5177 my $lock_obtained;
fdbbed2f 5178 my $do_delete = sub {
1770b70f 5179 $lock_obtained = 1;
22c377f0 5180 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
b2c9558d 5181 PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force});
7e7d7b61 5182 };
9dbd1ee4 5183
fdbbed2f
FE
5184 my $realcmd = sub {
5185 if ($param->{force}) {
5186 $do_delete->();
5187 } else {
1770b70f
FG
5188 eval { PVE::GuestHelpers::guest_migration_lock($vmid, 10, $do_delete); };
5189 if (my $err = $@) {
5190 die $err if $lock_obtained;
5191 die "Failed to obtain guest migration lock - replication running?\n";
5192 }
fdbbed2f
FE
5193 }
5194 };
5195
7b2257a8 5196 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
9dbd1ee4
AD
5197 }});
5198
04a69bb4
AD
5199__PACKAGE__->register_method({
5200 name => 'template',
5201 path => '{vmid}/template',
5202 method => 'POST',
5203 protected => 1,
5204 proxyto => 'node',
5205 description => "Create a Template.",
b02691d8 5206 permissions => {
7af0a6c8
DM
5207 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
5208 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
b02691d8 5209 },
04a69bb4
AD
5210 parameters => {
5211 additionalProperties => 0,
5212 properties => {
5213 node => get_standard_option('pve-node'),
335af808 5214 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
04a69bb4
AD
5215 disk => {
5216 optional => 1,
5217 type => 'string',
5218 description => "If you want to convert only 1 disk to base image.",
e0fd2b2f 5219 enum => [PVE::QemuServer::Drive::valid_drive_names()],
04a69bb4
AD
5220 },
5221
5222 },
5223 },
b297918c
FG
5224 returns => {
5225 type => 'string',
5226 description => "the task ID.",
5227 },
04a69bb4
AD
5228 code => sub {
5229 my ($param) = @_;
5230
5231 my $rpcenv = PVE::RPCEnvironment::get();
5232
5233 my $authuser = $rpcenv->get_user();
5234
5235 my $node = extract_param($param, 'node');
5236
5237 my $vmid = extract_param($param, 'vmid');
5238
5239 my $disk = extract_param($param, 'disk');
5240
d2ceac56 5241 my $load_and_check = sub {
ffda963f 5242 my $conf = PVE::QemuConfig->load_config($vmid);
04a69bb4 5243
ffda963f 5244 PVE::QemuConfig->check_lock($conf);
04a69bb4 5245
75466c4f 5246 die "unable to create template, because VM contains snapshots\n"
b91c2aae 5247 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
0402a80b 5248
75466c4f 5249 die "you can't convert a template to a template\n"
ffda963f 5250 if PVE::QemuConfig->is_template($conf) && !$disk;
0402a80b 5251
75466c4f 5252 die "you can't convert a VM to template if VM is running\n"
218cab9a 5253 if PVE::QemuServer::check_running($vmid);
35c5fdef 5254
d2ceac56
FG
5255 return $conf;
5256 };
04a69bb4 5257
d2ceac56 5258 $load_and_check->();
75e7e997 5259
d2ceac56
FG
5260 my $realcmd = sub {
5261 PVE::QemuConfig->lock_config($vmid, sub {
5262 my $conf = $load_and_check->();
5263
5264 $conf->{template} = 1;
5265 PVE::QemuConfig->write_config($vmid, $conf);
5266
5267 PVE::QemuServer::template_create($vmid, $conf, $disk);
5268 });
04a69bb4
AD
5269 };
5270
d2ceac56 5271 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
04a69bb4
AD
5272 }});
5273
73709749
ML
5274__PACKAGE__->register_method({
5275 name => 'cloudinit_generated_config_dump',
5276 path => '{vmid}/cloudinit/dump',
5277 method => 'GET',
5278 proxyto => 'node',
5279 description => "Get automatically generated cloudinit config.",
5280 permissions => {
5281 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5282 },
5283 parameters => {
5284 additionalProperties => 0,
5285 properties => {
5286 node => get_standard_option('pve-node'),
5287 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
5288 type => {
5289 description => 'Config type.',
5290 type => 'string',
5291 enum => ['user', 'network', 'meta'],
5292 },
5293 },
5294 },
5295 returns => {
5296 type => 'string',
5297 },
5298 code => sub {
5299 my ($param) = @_;
5300
5301 my $conf = PVE::QemuConfig->load_config($param->{vmid});
5302
5303 return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, $param->{vmid}, $param->{type});
5304 }});
5305
347dc136
FG
5306__PACKAGE__->register_method({
5307 name => 'mtunnel',
5308 path => '{vmid}/mtunnel',
5309 method => 'POST',
5310 protected => 1,
5311 description => 'Migration tunnel endpoint - only for internal use by VM migration.',
5312 permissions => {
5313 check =>
5314 [ 'and',
5315 ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
5316 ['perm', '/', [ 'Sys.Incoming' ]],
5317 ],
5318 description => "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming" .
5319 " on '/'. Further permission checks happen during the actual migration.",
5320 },
5321 parameters => {
5322 additionalProperties => 0,
5323 properties => {
5324 node => get_standard_option('pve-node'),
5325 vmid => get_standard_option('pve-vmid'),
5326 storages => {
5327 type => 'string',
5328 format => 'pve-storage-id-list',
5329 optional => 1,
5330 description => 'List of storages to check permission and availability. Will be checked again for all actually used storages during migration.',
5331 },
06fedff6
FG
5332 bridges => {
5333 type => 'string',
5334 format => 'pve-bridge-id-list',
5335 optional => 1,
5336 description => 'List of network bridges to check availability. Will be checked again for actually used bridges during migration.',
5337 },
347dc136
FG
5338 },
5339 },
5340 returns => {
5341 additionalProperties => 0,
5342 properties => {
5343 upid => { type => 'string' },
5344 ticket => { type => 'string' },
5345 socket => { type => 'string' },
5346 },
5347 },
5348 code => sub {
5349 my ($param) = @_;
5350
5351 my $rpcenv = PVE::RPCEnvironment::get();
5352 my $authuser = $rpcenv->get_user();
5353
5354 my $node = extract_param($param, 'node');
5355 my $vmid = extract_param($param, 'vmid');
5356
5357 my $storages = extract_param($param, 'storages');
06fedff6 5358 my $bridges = extract_param($param, 'bridges');
347dc136
FG
5359
5360 my $nodename = PVE::INotify::nodename();
5361
5362 raise_param_exc({ node => "node needs to be 'localhost' or local hostname '$nodename'" })
5363 if $node ne 'localhost' && $node ne $nodename;
5364
5365 $node = $nodename;
5366
5367 my $storecfg = PVE::Storage::config();
5368 foreach my $storeid (PVE::Tools::split_list($storages)) {
5369 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storeid, $node);
5370 }
5371
06fedff6
FG
5372 foreach my $bridge (PVE::Tools::split_list($bridges)) {
5373 PVE::Network::read_bridge_mtu($bridge);
5374 }
5375
347dc136
FG
5376 PVE::Cluster::check_cfs_quorum();
5377
5378 my $lock = 'create';
5379 eval { PVE::QemuConfig->create_and_lock_config($vmid, 0, $lock); };
5380
5381 raise_param_exc({ vmid => "unable to create empty VM config - $@"})
5382 if $@;
5383
5384 my $realcmd = sub {
5385 my $state = {
5386 storecfg => PVE::Storage::config(),
5387 lock => $lock,
5388 vmid => $vmid,
5389 };
5390
5391 my $run_locked = sub {
5392 my ($code, $params) = @_;
5393 return PVE::QemuConfig->lock_config($state->{vmid}, sub {
5394 my $conf = PVE::QemuConfig->load_config($state->{vmid});
5395
5396 $state->{conf} = $conf;
5397
5398 die "Encountered wrong lock - aborting mtunnel command handling.\n"
5399 if $state->{lock} && !PVE::QemuConfig->has_lock($conf, $state->{lock});
5400
5401 return $code->($params);
5402 });
5403 };
5404
5405 my $cmd_desc = {
5406 config => {
5407 conf => {
5408 type => 'string',
5409 description => 'Full VM config, adapted for target cluster/node',
5410 },
5411 'firewall-config' => {
5412 type => 'string',
5413 description => 'VM firewall config',
5414 optional => 1,
5415 },
5416 },
5417 disk => {
5418 format => PVE::JSONSchema::get_standard_option('pve-qm-image-format'),
5419 storage => {
5420 type => 'string',
5421 format => 'pve-storage-id',
5422 },
5423 drive => {
5424 type => 'object',
5425 description => 'parsed drive information without volid and format',
5426 },
5427 },
5428 start => {
5429 start_params => {
5430 type => 'object',
5431 description => 'params passed to vm_start_nolock',
5432 },
5433 migrate_opts => {
5434 type => 'object',
5435 description => 'migrate_opts passed to vm_start_nolock',
5436 },
5437 },
5438 ticket => {
5439 path => {
5440 type => 'string',
5441 description => 'socket path for which the ticket should be valid. must be known to current mtunnel instance.',
5442 },
5443 },
5444 quit => {
5445 cleanup => {
5446 type => 'boolean',
5447 description => 'remove VM config and disks, aborting migration',
5448 default => 0,
5449 },
5450 },
5451 'disk-import' => $PVE::StorageTunnel::cmd_schema->{'disk-import'},
5452 'query-disk-import' => $PVE::StorageTunnel::cmd_schema->{'query-disk-import'},
5453 bwlimit => $PVE::StorageTunnel::cmd_schema->{bwlimit},
5454 };
5455
5456 my $cmd_handlers = {
5457 'version' => sub {
5458 # compared against other end's version
5459 # bump/reset for breaking changes
5460 # bump/bump for opt-in changes
5461 return {
eef93bc5 5462 api => $PVE::QemuMigrate::WS_TUNNEL_VERSION,
347dc136
FG
5463 age => 0,
5464 };
5465 },
5466 'config' => sub {
5467 my ($params) = @_;
5468
5469 # parse and write out VM FW config if given
5470 if (my $fw_conf = $params->{'firewall-config'}) {
5471 my ($path, $fh) = PVE::Tools::tempfile_contents($fw_conf, 700);
5472
5473 my $empty_conf = {
5474 rules => [],
5475 options => {},
5476 aliases => {},
5477 ipset => {} ,
5478 ipset_comments => {},
5479 };
5480 my $cluster_fw_conf = PVE::Firewall::load_clusterfw_conf();
5481
5482 # TODO: add flag for strict parsing?
5483 # TODO: add import sub that does all this given raw content?
5484 my $vmfw_conf = PVE::Firewall::generic_fw_config_parser($path, $cluster_fw_conf, $empty_conf, 'vm');
5485 $vmfw_conf->{vmid} = $state->{vmid};
5486 PVE::Firewall::save_vmfw_conf($state->{vmid}, $vmfw_conf);
5487
5488 $state->{cleanup}->{fw} = 1;
5489 }
5490
5491 my $conf_fn = "incoming/qemu-server/$state->{vmid}.conf";
5492 my $new_conf = PVE::QemuServer::parse_vm_config($conf_fn, $params->{conf}, 1);
5493 delete $new_conf->{lock};
5494 delete $new_conf->{digest};
5495
5496 # TODO handle properly?
5497 delete $new_conf->{snapshots};
5498 delete $new_conf->{parent};
5499 delete $new_conf->{pending};
5500
5501 # not handled by update_vm_api
5502 my $vmgenid = delete $new_conf->{vmgenid};
5503 my $meta = delete $new_conf->{meta};
5504 my $cloudinit = delete $new_conf->{cloudinit}; # this is informational only
5505 $new_conf->{skip_cloud_init} = 1; # re-use image from source side
5506
5507 $new_conf->{vmid} = $state->{vmid};
5508 $new_conf->{node} = $node;
5509
5510 PVE::QemuConfig->remove_lock($state->{vmid}, 'create');
5511
5512 eval {
5513 $update_vm_api->($new_conf, 1);
5514 };
5515 if (my $err = $@) {
5516 # revert to locked previous config
5517 my $conf = PVE::QemuConfig->load_config($state->{vmid});
5518 $conf->{lock} = 'create';
5519 PVE::QemuConfig->write_config($state->{vmid}, $conf);
5520
5521 die $err;
5522 }
5523
5524 my $conf = PVE::QemuConfig->load_config($state->{vmid});
5525 $conf->{lock} = 'migrate';
5526 $conf->{vmgenid} = $vmgenid if defined($vmgenid);
5527 $conf->{meta} = $meta if defined($meta);
5528 $conf->{cloudinit} = $cloudinit if defined($cloudinit);
5529 PVE::QemuConfig->write_config($state->{vmid}, $conf);
5530
5531 $state->{lock} = 'migrate';
5532
5533 return;
5534 },
5535 'bwlimit' => sub {
5536 my ($params) = @_;
5537 return PVE::StorageTunnel::handle_bwlimit($params);
5538 },
5539 'disk' => sub {
5540 my ($params) = @_;
5541
5542 my $format = $params->{format};
5543 my $storeid = $params->{storage};
5544 my $drive = $params->{drive};
5545
5546 $check_storage_access_migrate->($rpcenv, $authuser, $state->{storecfg}, $storeid, $node);
5547
5548 my $storagemap = {
5549 default => $storeid,
5550 };
5551
5552 my $source_volumes = {
5553 'disk' => [
5554 undef,
5555 $storeid,
5556 undef,
5557 $drive,
5558 0,
5559 $format,
5560 ],
5561 };
5562
5563 my $res = PVE::QemuServer::vm_migrate_alloc_nbd_disks($state->{storecfg}, $state->{vmid}, $source_volumes, $storagemap);
5564 if (defined($res->{disk})) {
5565 $state->{cleanup}->{volumes}->{$res->{disk}->{volid}} = 1;
5566 return $res->{disk};
5567 } else {
5568 die "failed to allocate NBD disk..\n";
5569 }
5570 },
5571 'disk-import' => sub {
5572 my ($params) = @_;
5573
5574 $check_storage_access_migrate->(
5575 $rpcenv,
5576 $authuser,
5577 $state->{storecfg},
5578 $params->{storage},
5579 $node
5580 );
5581
5582 $params->{unix} = "/run/qemu-server/$state->{vmid}.storage";
5583
5584 return PVE::StorageTunnel::handle_disk_import($state, $params);
5585 },
5586 'query-disk-import' => sub {
5587 my ($params) = @_;
5588
5589 return PVE::StorageTunnel::handle_query_disk_import($state, $params);
5590 },
5591 'start' => sub {
5592 my ($params) = @_;
5593
5594 my $info = PVE::QemuServer::vm_start_nolock(
5595 $state->{storecfg},
5596 $state->{vmid},
5597 $state->{conf},
5598 $params->{start_params},
5599 $params->{migrate_opts},
5600 );
5601
5602
5603 if ($info->{migrate}->{proto} ne 'unix') {
5604 PVE::QemuServer::vm_stop(undef, $state->{vmid}, 1, 1);
5605 die "migration over non-UNIX sockets not possible\n";
5606 }
5607
5608 my $socket = $info->{migrate}->{addr};
5609 chown $state->{socket_uid}, -1, $socket;
5610 $state->{sockets}->{$socket} = 1;
5611
5612 my $unix_sockets = $info->{migrate}->{unix_sockets};
5613 foreach my $socket (@$unix_sockets) {
5614 chown $state->{socket_uid}, -1, $socket;
5615 $state->{sockets}->{$socket} = 1;
5616 }
5617 return $info;
5618 },
5619 'fstrim' => sub {
5620 if (PVE::QemuServer::qga_check_running($state->{vmid})) {
5621 eval { mon_cmd($state->{vmid}, "guest-fstrim") };
5622 warn "fstrim failed: $@\n" if $@;
5623 }
5624 return;
5625 },
5626 'stop' => sub {
5627 PVE::QemuServer::vm_stop(undef, $state->{vmid}, 1, 1);
5628 return;
5629 },
5630 'nbdstop' => sub {
5631 PVE::QemuServer::nbd_stop($state->{vmid});
5632 return;
5633 },
5634 'resume' => sub {
5635 if (PVE::QemuServer::Helpers::vm_running_locally($state->{vmid})) {
5636 PVE::QemuServer::vm_resume($state->{vmid}, 1, 1);
5637 } else {
5638 die "VM $state->{vmid} not running\n";
5639 }
5640 return;
5641 },
5642 'unlock' => sub {
5643 PVE::QemuConfig->remove_lock($state->{vmid}, $state->{lock});
5644 delete $state->{lock};
5645 return;
5646 },
5647 'ticket' => sub {
5648 my ($params) = @_;
5649
5650 my $path = $params->{path};
5651
5652 die "Not allowed to generate ticket for unknown socket '$path'\n"
5653 if !defined($state->{sockets}->{$path});
5654
5655 return { ticket => PVE::AccessControl::assemble_tunnel_ticket($authuser, "/socket/$path") };
5656 },
5657 'quit' => sub {
5658 my ($params) = @_;
5659
5660 if ($params->{cleanup}) {
5661 if ($state->{cleanup}->{fw}) {
5662 PVE::Firewall::remove_vmfw_conf($state->{vmid});
5663 }
5664
5665 for my $volid (keys $state->{cleanup}->{volumes}->%*) {
5666 print "freeing volume '$volid' as part of cleanup\n";
5667 eval { PVE::Storage::vdisk_free($state->{storecfg}, $volid) };
5668 warn $@ if $@;
5669 }
5670
5671 PVE::QemuServer::destroy_vm($state->{storecfg}, $state->{vmid}, 1);
5672 }
5673
5674 print "switching to exit-mode, waiting for client to disconnect\n";
5675 $state->{exit} = 1;
5676 return;
5677 },
5678 };
5679
5680 $run_locked->(sub {
5681 my $socket_addr = "/run/qemu-server/$state->{vmid}.mtunnel";
5682 unlink $socket_addr;
5683
5684 $state->{socket} = IO::Socket::UNIX->new(
5685 Type => SOCK_STREAM(),
5686 Local => $socket_addr,
5687 Listen => 1,
5688 );
5689
5690 $state->{socket_uid} = getpwnam('www-data')
5691 or die "Failed to resolve user 'www-data' to numeric UID\n";
5692 chown $state->{socket_uid}, -1, $socket_addr;
5693 });
5694
5695 print "mtunnel started\n";
5696
5697 my $conn = eval { PVE::Tools::run_with_timeout(300, sub { $state->{socket}->accept() }) };
5698 if ($@) {
5699 warn "Failed to accept tunnel connection - $@\n";
5700
5701 warn "Removing tunnel socket..\n";
5702 unlink $state->{socket};
5703
5704 warn "Removing temporary VM config..\n";
5705 $run_locked->(sub {
5706 PVE::QemuServer::destroy_vm($state->{storecfg}, $state->{vmid}, 1);
5707 });
5708
5709 die "Exiting mtunnel\n";
5710 }
5711
5712 $state->{conn} = $conn;
5713
5714 my $reply_err = sub {
5715 my ($msg) = @_;
5716
5717 my $reply = JSON::encode_json({
5718 success => JSON::false,
5719 msg => $msg,
5720 });
5721 $conn->print("$reply\n");
5722 $conn->flush();
5723 };
5724
5725 my $reply_ok = sub {
5726 my ($res) = @_;
5727
5728 $res->{success} = JSON::true;
5729 my $reply = JSON::encode_json($res);
5730 $conn->print("$reply\n");
5731 $conn->flush();
5732 };
5733
5734 while (my $line = <$conn>) {
5735 chomp $line;
5736
5737 # untaint, we validate below if needed
5738 ($line) = $line =~ /^(.*)$/;
5739 my $parsed = eval { JSON::decode_json($line) };
5740 if ($@) {
5741 $reply_err->("failed to parse command - $@");
5742 next;
5743 }
5744
5745 my $cmd = delete $parsed->{cmd};
5746 if (!defined($cmd)) {
5747 $reply_err->("'cmd' missing");
5748 } elsif ($state->{exit}) {
5749 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd not possible");
5750 next;
5751 } elsif (my $handler = $cmd_handlers->{$cmd}) {
5752 print "received command '$cmd'\n";
5753 eval {
5754 if ($cmd_desc->{$cmd}) {
5755 PVE::JSONSchema::validate($parsed, $cmd_desc->{$cmd});
5756 } else {
5757 $parsed = {};
5758 }
5759 my $res = $run_locked->($handler, $parsed);
5760 $reply_ok->($res);
5761 };
5762 $reply_err->("failed to handle '$cmd' command - $@")
5763 if $@;
5764 } else {
5765 $reply_err->("unknown command '$cmd' given");
5766 }
5767 }
5768
5769 if ($state->{exit}) {
5770 print "mtunnel exited\n";
5771 } else {
5772 die "mtunnel exited unexpectedly\n";
5773 }
5774 };
5775
5776 my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
5777 my $ticket = PVE::AccessControl::assemble_tunnel_ticket($authuser, "/socket/$socket_addr");
5778 my $upid = $rpcenv->fork_worker('qmtunnel', $vmid, $authuser, $realcmd);
5779
5780 return {
5781 ticket => $ticket,
5782 upid => $upid,
5783 socket => $socket_addr,
5784 };
5785 }});
5786
5787__PACKAGE__->register_method({
5788 name => 'mtunnelwebsocket',
5789 path => '{vmid}/mtunnelwebsocket',
5790 method => 'GET',
5791 permissions => {
5792 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.",
5793 user => 'all', # check inside
5794 },
5795 description => 'Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.',
5796 parameters => {
5797 additionalProperties => 0,
5798 properties => {
5799 node => get_standard_option('pve-node'),
5800 vmid => get_standard_option('pve-vmid'),
5801 socket => {
5802 type => "string",
5803 description => "unix socket to forward to",
5804 },
5805 ticket => {
5806 type => "string",
5807 description => "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command",
5808 },
5809 },
5810 },
5811 returns => {
5812 type => "object",
5813 properties => {
5814 port => { type => 'string', optional => 1 },
5815 socket => { type => 'string', optional => 1 },
5816 },
5817 },
5818 code => sub {
5819 my ($param) = @_;
5820
5821 my $rpcenv = PVE::RPCEnvironment::get();
5822 my $authuser = $rpcenv->get_user();
5823
5824 my $nodename = PVE::INotify::nodename();
5825 my $node = extract_param($param, 'node');
5826
5827 raise_param_exc({ node => "node needs to be 'localhost' or local hostname '$nodename'" })
5828 if $node ne 'localhost' && $node ne $nodename;
5829
5830 my $vmid = $param->{vmid};
5831 # check VM exists
5832 PVE::QemuConfig->load_config($vmid);
5833
5834 my $socket = $param->{socket};
5835 PVE::AccessControl::verify_tunnel_ticket($param->{ticket}, $authuser, "/socket/$socket");
5836
5837 return { socket => $socket };
5838 }});
5839
1e3baf05 58401;