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