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