]> git.proxmox.com Git - qemu-server.git/blame - PVE/API2/Qemu.pm
api2: add cloudinit config api
[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
5555edea
DM
1382# POST/PUT {vmid}/config implementation
1383#
1384# The original API used PUT (idempotent) an we assumed that all operations
1385# are fast. But it turned out that almost any configuration change can
1386# involve hot-plug actions, or disk alloc/free. Such actions can take long
1387# time to complete and have side effects (not idempotent).
1388#
7043d946 1389# The new implementation uses POST and forks a worker process. We added
5555edea 1390# a new option 'background_delay'. If specified we wait up to
7043d946 1391# 'background_delay' second for the worker task to complete. It returns null
5555edea 1392# if the task is finished within that time, else we return the UPID.
7043d946 1393
5555edea
DM
1394my $update_vm_api = sub {
1395 my ($param, $sync) = @_;
a0d1b1a2 1396
5555edea 1397 my $rpcenv = PVE::RPCEnvironment::get();
1e3baf05 1398
5555edea 1399 my $authuser = $rpcenv->get_user();
1e3baf05 1400
5555edea 1401 my $node = extract_param($param, 'node');
1e3baf05 1402
5555edea 1403 my $vmid = extract_param($param, 'vmid');
1e3baf05 1404
5555edea 1405 my $digest = extract_param($param, 'digest');
1e3baf05 1406
5555edea 1407 my $background_delay = extract_param($param, 'background_delay');
1e3baf05 1408
cefb41fa
WB
1409 if (defined(my $cipassword = $param->{cipassword})) {
1410 # Same logic as in cloud-init (but with the regex fixed...)
1411 $param->{cipassword} = PVE::Tools::encrypt_pw($cipassword)
1412 if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
1413 }
0c9a7596 1414
5555edea 1415 my @paramarr = (); # used for log message
edd48c32 1416 foreach my $key (sort keys %$param) {
cefb41fa
WB
1417 my $value = $key eq 'cipassword' ? '<hidden>' : $param->{$key};
1418 push @paramarr, "-$key", $value;
5555edea 1419 }
0532bc63 1420
5555edea
DM
1421 my $skiplock = extract_param($param, 'skiplock');
1422 raise_param_exc({ skiplock => "Only root may use this option." })
1423 if $skiplock && $authuser ne 'root@pam';
1e3baf05 1424
5555edea 1425 my $delete_str = extract_param($param, 'delete');
0532bc63 1426
d3df8cf3
DM
1427 my $revert_str = extract_param($param, 'revert');
1428
5555edea 1429 my $force = extract_param($param, 'force');
1e68cb19 1430
0c9a7596 1431 if (defined(my $ssh_keys = $param->{sshkeys})) {
231f824b
WB
1432 $ssh_keys = URI::Escape::uri_unescape($ssh_keys);
1433 PVE::Tools::validate_ssh_public_keys($ssh_keys);
0c9a7596
AD
1434 }
1435
dbc45fdf
FE
1436 $param->{cpuunits} = PVE::GuestHelpers::get_cpuunits($param->{cpuunits})
1437 if defined($param->{cpuunits}); # clamp value depending on cgroup version
1438
d3df8cf3 1439 die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
7bfdeb5f 1440
5555edea 1441 my $storecfg = PVE::Storage::config();
1e68cb19 1442
5555edea 1443 my $defaults = PVE::QemuServer::load_defaults();
1e68cb19 1444
5555edea 1445 &$resolve_cdrom_alias($param);
0532bc63 1446
5555edea 1447 # now try to verify all parameters
ae57f6b3 1448
d3df8cf3
DM
1449 my $revert = {};
1450 foreach my $opt (PVE::Tools::split_list($revert_str)) {
1451 if (!PVE::QemuServer::option_exists($opt)) {
1452 raise_param_exc({ revert => "unknown option '$opt'" });
1453 }
1454
1455 raise_param_exc({ delete => "you can't use '-$opt' and " .
1456 "-revert $opt' at the same time" })
1457 if defined($param->{$opt});
1458
1459 $revert->{$opt} = 1;
1460 }
1461
5555edea
DM
1462 my @delete = ();
1463 foreach my $opt (PVE::Tools::split_list($delete_str)) {
1464 $opt = 'ide2' if $opt eq 'cdrom';
d3df8cf3 1465
5555edea
DM
1466 raise_param_exc({ delete => "you can't use '-$opt' and " .
1467 "-delete $opt' at the same time" })
1468 if defined($param->{$opt});
7043d946 1469
d3df8cf3
DM
1470 raise_param_exc({ revert => "you can't use '-delete $opt' and " .
1471 "-revert $opt' at the same time" })
1472 if $revert->{$opt};
1473
5555edea
DM
1474 if (!PVE::QemuServer::option_exists($opt)) {
1475 raise_param_exc({ delete => "unknown option '$opt'" });
0532bc63 1476 }
1e3baf05 1477
5555edea
DM
1478 push @delete, $opt;
1479 }
1480
17677004
WB
1481 my $repl_conf = PVE::ReplicationConfig->new();
1482 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
1483 my $check_replication = sub {
1484 my ($drive) = @_;
1485 return if !$is_replicated;
1486 my $volid = $drive->{file};
1487 return if !$volid || !($drive->{replicate}//1);
1488 return if PVE::QemuServer::drive_is_cdrom($drive);
21e1ee7b
ML
1489
1490 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
35171ddb
FG
1491 die "cannot add non-managed/pass-through volume to a replicated VM\n"
1492 if !defined($storeid);
1493
e3d31944 1494 return if defined($volname) && $volname eq 'cloudinit';
21e1ee7b
ML
1495
1496 my $format;
17677004
WB
1497 if ($volid =~ $NEW_DISK_RE) {
1498 $storeid = $2;
1499 $format = $drive->{format} || PVE::Storage::storage_default_format($storecfg, $storeid);
1500 } else {
17677004
WB
1501 $format = (PVE::Storage::parse_volname($storecfg, $volid))[6];
1502 }
1503 return if PVE::Storage::storage_can_replicate($storecfg, $storeid, $format);
9b1396ed
WB
1504 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
1505 return if $scfg->{shared};
17677004
WB
1506 die "cannot add non-replicatable volume to a replicated VM\n";
1507 };
1508
7979bbcd
FE
1509 $check_drive_param->($param, $storecfg, $check_replication);
1510
5555edea 1511 foreach my $opt (keys %$param) {
7979bbcd 1512 if ($opt =~ m/^net(\d+)$/) {
5555edea
DM
1513 # add macaddr
1514 my $net = PVE::QemuServer::parse_net($param->{$opt});
1515 $param->{$opt} = PVE::QemuServer::print_net($net);
6ee499ff
DC
1516 } elsif ($opt eq 'vmgenid') {
1517 if ($param->{$opt} eq '1') {
1518 $param->{$opt} = PVE::QemuServer::generate_uuid();
1519 }
9e784b11
DC
1520 } elsif ($opt eq 'hookscript') {
1521 eval { PVE::GuestHelpers::check_hookscript($param->{$opt}, $storecfg); };
1522 raise_param_exc({ $opt => $@ }) if $@;
1e68cb19 1523 }
5555edea 1524 }
1e3baf05 1525
5555edea 1526 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]);
ae57f6b3 1527
e30f75c5 1528 &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
ae57f6b3 1529
5555edea 1530 &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
1e3baf05 1531
5555edea 1532 my $updatefn = sub {
1e3baf05 1533
ffda963f 1534 my $conf = PVE::QemuConfig->load_config($vmid);
1e3baf05 1535
5555edea
DM
1536 die "checksum missmatch (file change by other user?)\n"
1537 if $digest && $digest ne $conf->{digest};
1538
6e72f90b
SR
1539 &$check_cpu_model_access($rpcenv, $authuser, $param, $conf);
1540
546644e2
TL
1541 # FIXME: 'suspended' lock should probabyl be a state or "weak" lock?!
1542 if (scalar(@delete) && grep { $_ eq 'vmstate'} @delete) {
1543 if (defined($conf->{lock}) && $conf->{lock} eq 'suspended') {
1544 delete $conf->{lock}; # for check lock check, not written out
1545 push @delete, 'lock'; # this is the real deal to write it out
1546 }
1547 push @delete, 'runningmachine' if $conf->{runningmachine};
ea1c2110 1548 push @delete, 'runningcpu' if $conf->{runningcpu};
546644e2
TL
1549 }
1550
ffda963f 1551 PVE::QemuConfig->check_lock($conf) if !$skiplock;
7043d946 1552
d3df8cf3
DM
1553 foreach my $opt (keys %$revert) {
1554 if (defined($conf->{$opt})) {
1555 $param->{$opt} = $conf->{$opt};
1556 } elsif (defined($conf->{pending}->{$opt})) {
1557 push @delete, $opt;
1558 }
1559 }
1560
5555edea 1561 if ($param->{memory} || defined($param->{balloon})) {
6ca8b698
DM
1562 my $maxmem = $param->{memory} || $conf->{pending}->{memory} || $conf->{memory} || $defaults->{memory};
1563 my $balloon = defined($param->{balloon}) ? $param->{balloon} : $conf->{pending}->{balloon} || $conf->{balloon};
7043d946 1564
5555edea
DM
1565 die "balloon value too large (must be smaller than assigned memory)\n"
1566 if $balloon && $balloon > $maxmem;
1567 }
1e3baf05 1568
5555edea 1569 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join (' ', @paramarr));
1e3baf05 1570
5555edea 1571 my $worker = sub {
7bfdeb5f 1572
5555edea 1573 print "update VM $vmid: " . join (' ', @paramarr) . "\n";
c2a64aa7 1574
202d1f45
DM
1575 # write updates to pending section
1576
3a11fadb
DM
1577 my $modified = {}; # record what $option we modify
1578
11c601e9
TL
1579 my @bootorder;
1580 if (my $boot = $conf->{boot}) {
1581 my $bootcfg = PVE::JSONSchema::parse_property_string('pve-qm-boot', $boot);
1582 @bootorder = PVE::Tools::split_list($bootcfg->{order}) if $bootcfg && $bootcfg->{order};
1583 }
078c109f
SR
1584 my $bootorder_deleted = grep {$_ eq 'bootorder'} @delete;
1585
bb660bc3
DC
1586 my $check_drive_perms = sub {
1587 my ($opt, $val) = @_;
c1accf9d 1588 my $drive = PVE::QemuServer::parse_drive($opt, $val, 1);
bb660bc3
DC
1589 # FIXME: cloudinit: CDROM or Disk?
1590 if (PVE::QemuServer::drive_is_cdrom($drive)) { # CDROM
1591 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
1592 } else {
1593 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
1594 }
1595 };
1596
202d1f45 1597 foreach my $opt (@delete) {
3a11fadb 1598 $modified->{$opt} = 1;
ffda963f 1599 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
f70a6ea9
TL
1600
1601 # value of what we want to delete, independent if pending or not
1602 my $val = $conf->{$opt} // $conf->{pending}->{$opt};
1603 if (!defined($val)) {
d2c6bf93
FG
1604 warn "cannot delete '$opt' - not set in current configuration!\n";
1605 $modified->{$opt} = 0;
1606 next;
1607 }
f70a6ea9 1608 my $is_pending_val = defined($conf->{pending}->{$opt});
6aa43f92 1609 delete $conf->{pending}->{$opt};
d2c6bf93 1610
078c109f
SR
1611 # remove from bootorder if necessary
1612 if (!$bootorder_deleted && @bootorder && grep {$_ eq $opt} @bootorder) {
1613 @bootorder = grep {$_ ne $opt} @bootorder;
1614 $conf->{pending}->{boot} = PVE::QemuServer::print_bootorder(\@bootorder);
1615 $modified->{boot} = 1;
1616 }
1617
202d1f45 1618 if ($opt =~ m/^unused/) {
f70a6ea9 1619 my $drive = PVE::QemuServer::parse_drive($opt, $val);
ffda963f 1620 PVE::QemuConfig->check_protection($conf, "can't remove unused disk '$drive->{file}'");
4d8d55f1 1621 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
3dc38fbb
WB
1622 if (PVE::QemuServer::try_deallocate_drive($storecfg, $vmid, $conf, $opt, $drive, $rpcenv, $authuser)) {
1623 delete $conf->{$opt};
ffda963f 1624 PVE::QemuConfig->write_config($vmid, $conf);
202d1f45 1625 }
6afb6794
DC
1626 } elsif ($opt eq 'vmstate') {
1627 PVE::QemuConfig->check_protection($conf, "can't remove vmstate '$val'");
6afb6794
DC
1628 if (PVE::QemuServer::try_deallocate_drive($storecfg, $vmid, $conf, $opt, { file => $val }, $rpcenv, $authuser, 1)) {
1629 delete $conf->{$opt};
1630 PVE::QemuConfig->write_config($vmid, $conf);
1631 }
74479ee9 1632 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
ffda963f 1633 PVE::QemuConfig->check_protection($conf, "can't remove drive '$opt'");
bb660bc3 1634 $check_drive_perms->($opt, $val);
f70a6ea9
TL
1635 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $val))
1636 if $is_pending_val;
98bc3aeb 1637 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
ffda963f 1638 PVE::QemuConfig->write_config($vmid, $conf);
e30f75c5 1639 } elsif ($opt =~ m/^serial\d+$/) {
f70a6ea9 1640 if ($val eq 'socket') {
e5453043
DC
1641 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1642 } elsif ($authuser ne 'root@pam') {
1643 die "only root can delete '$opt' config for real devices\n";
1644 }
98bc3aeb 1645 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
e5453043 1646 PVE::QemuConfig->write_config($vmid, $conf);
165be267 1647 } elsif ($opt =~ m/^usb\d+$/) {
f70a6ea9 1648 if ($val =~ m/spice/) {
165be267
DC
1649 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1650 } elsif ($authuser ne 'root@pam') {
1651 die "only root can delete '$opt' config for real devices\n";
1652 }
98bc3aeb 1653 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
165be267 1654 PVE::QemuConfig->write_config($vmid, $conf);
202d1f45 1655 } else {
98bc3aeb 1656 PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
ffda963f 1657 PVE::QemuConfig->write_config($vmid, $conf);
202d1f45 1658 }
5d39a182 1659 }
1e3baf05 1660
202d1f45 1661 foreach my $opt (keys %$param) { # add/change
3a11fadb 1662 $modified->{$opt} = 1;
ffda963f 1663 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
202d1f45
DM
1664 next if defined($conf->{pending}->{$opt}) && ($param->{$opt} eq $conf->{pending}->{$opt}); # skip if nothing changed
1665
de64f101 1666 my $arch = PVE::QemuServer::get_vm_arch($conf);
96ed3574 1667
74479ee9 1668 if (PVE::QemuServer::is_valid_drivename($opt)) {
2c44ec49
DC
1669 # old drive
1670 if ($conf->{$opt}) {
1671 $check_drive_perms->($opt, $conf->{$opt});
1672 }
1673
1674 # new drive
bb660bc3 1675 $check_drive_perms->($opt, $param->{$opt});
055d554d 1676 PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
202d1f45
DM
1677 if defined($conf->{pending}->{$opt});
1678
367e6bf4
FE
1679 my (undef, $created_opts) = $create_disks->(
1680 $rpcenv,
1681 $authuser,
1682 $conf,
1683 $arch,
1684 $storecfg,
1685 $vmid,
1686 undef,
1687 {$opt => $param->{$opt}},
1688 );
1689 $conf->{pending}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
deb734e3 1690
a2e22f9f
DC
1691 # default legacy boot order implies all cdroms anyway
1692 if (@bootorder) {
1693 # append new CD drives to bootorder to mark them bootable
c1accf9d 1694 my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}, 1);
a2e22f9f
DC
1695 if (PVE::QemuServer::drive_is_cdrom($drive, 1) && !grep(/^$opt$/, @bootorder)) {
1696 push @bootorder, $opt;
1697 $conf->{pending}->{boot} = PVE::QemuServer::print_bootorder(\@bootorder);
1698 $modified->{boot} = 1;
1699 }
deb734e3 1700 }
e30f75c5
DC
1701 } elsif ($opt =~ m/^serial\d+/) {
1702 if ((!defined($conf->{$opt}) || $conf->{$opt} eq 'socket') && $param->{$opt} eq 'socket') {
1703 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1704 } elsif ($authuser ne 'root@pam') {
1705 die "only root can modify '$opt' config for real devices\n";
1706 }
1707 $conf->{pending}->{$opt} = $param->{$opt};
165be267
DC
1708 } elsif ($opt =~ m/^usb\d+/) {
1709 if ((!defined($conf->{$opt}) || $conf->{$opt} =~ m/spice/) && $param->{$opt} =~ m/spice/) {
1710 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
1711 } elsif ($authuser ne 'root@pam') {
1712 die "only root can modify '$opt' config for real devices\n";
1713 }
1714 $conf->{pending}->{$opt} = $param->{$opt};
202d1f45
DM
1715 } else {
1716 $conf->{pending}->{$opt} = $param->{$opt};
078c109f
SR
1717
1718 if ($opt eq 'boot') {
1719 my $new_bootcfg = PVE::JSONSchema::parse_property_string('pve-qm-boot', $param->{$opt});
1720 if ($new_bootcfg->{order}) {
1721 my @devs = PVE::Tools::split_list($new_bootcfg->{order});
1722 for my $dev (@devs) {
c9c32c1b 1723 my $exists = $conf->{$dev} || $conf->{pending}->{$dev} || $param->{$dev};
078c109f
SR
1724 my $deleted = grep {$_ eq $dev} @delete;
1725 die "invalid bootorder: device '$dev' does not exist'\n"
1726 if !$exists || $deleted;
1727 }
1728
1729 # remove legacy boot order settings if new one set
1730 $conf->{pending}->{$opt} = PVE::QemuServer::print_bootorder(\@devs);
1731 PVE::QemuConfig->add_to_pending_delete($conf, "bootdisk")
1732 if $conf->{bootdisk};
1733 }
1734 }
202d1f45 1735 }
98bc3aeb 1736 PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
ffda963f 1737 PVE::QemuConfig->write_config($vmid, $conf);
202d1f45
DM
1738 }
1739
1740 # remove pending changes when nothing changed
ffda963f 1741 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
98bc3aeb 1742 my $changes = PVE::QemuConfig->cleanup_pending($conf);
ffda963f 1743 PVE::QemuConfig->write_config($vmid, $conf) if $changes;
202d1f45
DM
1744
1745 return if !scalar(keys %{$conf->{pending}});
1746
7bfdeb5f 1747 my $running = PVE::QemuServer::check_running($vmid);
39001640
DM
1748
1749 # apply pending changes
1750
ffda963f 1751 $conf = PVE::QemuConfig->load_config($vmid); # update/reload
39001640 1752
eb5e482d 1753 my $errors = {};
3a11fadb 1754 if ($running) {
3a11fadb 1755 PVE::QemuServer::vmconfig_hotplug_pending($vmid, $conf, $storecfg, $modified, $errors);
3a11fadb 1756 } else {
4a5cb613 1757 PVE::QemuServer::vmconfig_apply_pending($vmid, $conf, $storecfg, $errors);
3a11fadb 1758 }
eb5e482d 1759 raise_param_exc($errors) if scalar(keys %$errors);
1e68cb19 1760
915d3481 1761 return;
5d39a182
DM
1762 };
1763
5555edea
DM
1764 if ($sync) {
1765 &$worker();
d1c1af4b 1766 return;
5555edea
DM
1767 } else {
1768 my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker);
fcdb0117 1769
5555edea
DM
1770 if ($background_delay) {
1771
1772 # Note: It would be better to do that in the Event based HTTPServer
7043d946 1773 # to avoid blocking call to sleep.
5555edea
DM
1774
1775 my $end_time = time() + $background_delay;
1776
1777 my $task = PVE::Tools::upid_decode($upid);
1778
1779 my $running = 1;
1780 while (time() < $end_time) {
1781 $running = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
1782 last if !$running;
1783 sleep(1); # this gets interrupted when child process ends
1784 }
1785
1786 if (!$running) {
1787 my $status = PVE::Tools::upid_read_status($upid);
872cfcf5 1788 return if !PVE::Tools::upid_status_is_error($status);
ce3fbcd4 1789 die "failed to update VM $vmid: $status\n";
5555edea 1790 }
7043d946 1791 }
5555edea
DM
1792
1793 return $upid;
1794 }
1795 };
1796
ffda963f 1797 return PVE::QemuConfig->lock_config($vmid, $updatefn);
5555edea
DM
1798};
1799
1800my $vm_config_perm_list = [
1801 'VM.Config.Disk',
1802 'VM.Config.CDROM',
1803 'VM.Config.CPU',
1804 'VM.Config.Memory',
1805 'VM.Config.Network',
1806 'VM.Config.HWType',
1807 'VM.Config.Options',
fc701af7 1808 'VM.Config.Cloudinit',
5555edea
DM
1809 ];
1810
1811__PACKAGE__->register_method({
1812 name => 'update_vm_async',
1813 path => '{vmid}/config',
1814 method => 'POST',
1815 protected => 1,
1816 proxyto => 'node',
1817 description => "Set virtual machine options (asynchrounous API).",
1818 permissions => {
1819 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1820 },
1821 parameters => {
3326ae19 1822 additionalProperties => 0,
5555edea
DM
1823 properties => PVE::QemuServer::json_config_properties(
1824 {
1825 node => get_standard_option('pve-node'),
1826 vmid => get_standard_option('pve-vmid'),
1827 skiplock => get_standard_option('skiplock'),
1828 delete => {
1829 type => 'string', format => 'pve-configid-list',
1830 description => "A list of settings you want to delete.",
1831 optional => 1,
1832 },
4c8365fa
DM
1833 revert => {
1834 type => 'string', format => 'pve-configid-list',
1835 description => "Revert a pending change.",
1836 optional => 1,
1837 },
5555edea
DM
1838 force => {
1839 type => 'boolean',
1840 description => $opt_force_description,
1841 optional => 1,
1842 requires => 'delete',
1843 },
1844 digest => {
1845 type => 'string',
1846 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1847 maxLength => 40,
1848 optional => 1,
1849 },
1850 background_delay => {
1851 type => 'integer',
1852 description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
1853 minimum => 1,
1854 maximum => 30,
1855 optional => 1,
1856 },
c1accf9d
FE
1857 },
1858 1, # with_disk_alloc
1859 ),
5555edea
DM
1860 },
1861 returns => {
1862 type => 'string',
1863 optional => 1,
1864 },
1865 code => $update_vm_api,
1866});
1867
1868__PACKAGE__->register_method({
1869 name => 'update_vm',
1870 path => '{vmid}/config',
1871 method => 'PUT',
1872 protected => 1,
1873 proxyto => 'node',
1874 description => "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.",
1875 permissions => {
1876 check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
1877 },
1878 parameters => {
3326ae19 1879 additionalProperties => 0,
5555edea
DM
1880 properties => PVE::QemuServer::json_config_properties(
1881 {
1882 node => get_standard_option('pve-node'),
335af808 1883 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
5555edea
DM
1884 skiplock => get_standard_option('skiplock'),
1885 delete => {
1886 type => 'string', format => 'pve-configid-list',
1887 description => "A list of settings you want to delete.",
1888 optional => 1,
1889 },
4c8365fa
DM
1890 revert => {
1891 type => 'string', format => 'pve-configid-list',
1892 description => "Revert a pending change.",
1893 optional => 1,
1894 },
5555edea
DM
1895 force => {
1896 type => 'boolean',
1897 description => $opt_force_description,
1898 optional => 1,
1899 requires => 'delete',
1900 },
1901 digest => {
1902 type => 'string',
1903 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1904 maxLength => 40,
1905 optional => 1,
1906 },
c1accf9d
FE
1907 },
1908 1, # with_disk_alloc
1909 ),
5555edea
DM
1910 },
1911 returns => { type => 'null' },
1912 code => sub {
1913 my ($param) = @_;
1914 &$update_vm_api($param, 1);
d1c1af4b 1915 return;
5555edea
DM
1916 }
1917});
1e3baf05 1918
1e3baf05 1919__PACKAGE__->register_method({
afdb31d5
DM
1920 name => 'destroy_vm',
1921 path => '{vmid}',
1e3baf05
DM
1922 method => 'DELETE',
1923 protected => 1,
1924 proxyto => 'node',
e00319af
TL
1925 description => "Destroy the VM and all used/owned volumes. Removes any VM specific permissions"
1926 ." and firewall rules",
a0d1b1a2
DM
1927 permissions => {
1928 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
1929 },
1e3baf05 1930 parameters => {
3326ae19 1931 additionalProperties => 0,
1e3baf05
DM
1932 properties => {
1933 node => get_standard_option('pve-node'),
335af808 1934 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
3ea94c60 1935 skiplock => get_standard_option('skiplock'),
d9123ef5
CE
1936 purge => {
1937 type => 'boolean',
e00319af 1938 description => "Remove VMID from configurations, like backup & replication jobs and HA.",
d9123ef5
CE
1939 optional => 1,
1940 },
75854662
TL
1941 'destroy-unreferenced-disks' => {
1942 type => 'boolean',
99676a6c
TL
1943 description => "If set, destroy additionally all disks not referenced in the config"
1944 ." but with a matching VMID from all enabled storages.",
75854662 1945 optional => 1,
16e66777 1946 default => 0,
47f9f50b 1947 },
1e3baf05
DM
1948 },
1949 },
afdb31d5 1950 returns => {
5fdbe4f0
DM
1951 type => 'string',
1952 },
1e3baf05
DM
1953 code => sub {
1954 my ($param) = @_;
1955
1956 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 1957 my $authuser = $rpcenv->get_user();
1e3baf05
DM
1958 my $vmid = $param->{vmid};
1959
1960 my $skiplock = $param->{skiplock};
afdb31d5 1961 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 1962 if $skiplock && $authuser ne 'root@pam';
1e3baf05 1963
a4c7029d
FG
1964 my $early_checks = sub {
1965 # test if VM exists
1966 my $conf = PVE::QemuConfig->load_config($vmid);
1967 PVE::QemuConfig->check_protection($conf, "can't remove VM $vmid");
7c4351f7 1968
a4c7029d 1969 my $ha_managed = PVE::HA::Config::service_is_configured("vm:$vmid");
e9f2f8e5 1970
a4c7029d
FG
1971 if (!$param->{purge}) {
1972 die "unable to remove VM $vmid - used in HA resources and purge parameter not set.\n"
1973 if $ha_managed;
1974 # don't allow destroy if with replication jobs but no purge param
1975 my $repl_conf = PVE::ReplicationConfig->new();
1976 $repl_conf->check_for_existing_jobs($vmid);
1977 }
628bb7f2 1978
a4c7029d
FG
1979 die "VM $vmid is running - destroy failed\n"
1980 if PVE::QemuServer::check_running($vmid);
1981
1982 return $ha_managed;
1983 };
1984
1985 $early_checks->();
db593da2 1986
5fdbe4f0 1987 my $realcmd = sub {
ff1a2432
DM
1988 my $upid = shift;
1989
a4c7029d
FG
1990 my $storecfg = PVE::Storage::config();
1991
ff1a2432 1992 syslog('info', "destroy VM $vmid: $upid\n");
3e8e214d 1993 PVE::QemuConfig->lock_config($vmid, sub {
a4c7029d
FG
1994 # repeat, config might have changed
1995 my $ha_managed = $early_checks->();
d9123ef5 1996
16e66777 1997 my $purge_unreferenced = $param->{'destroy-unreferenced-disks'};
75854662
TL
1998
1999 PVE::QemuServer::destroy_vm(
2000 $storecfg,
2001 $vmid,
2002 $skiplock, { lock => 'destroyed' },
2003 $purge_unreferenced,
2004 );
d9123ef5 2005
3e8e214d
DJ
2006 PVE::AccessControl::remove_vm_access($vmid);
2007 PVE::Firewall::remove_vmfw_conf($vmid);
d9123ef5 2008 if ($param->{purge}) {
7c4351f7 2009 print "purging VM $vmid from related configurations..\n";
d9123ef5
CE
2010 PVE::ReplicationConfig::remove_vmid_jobs($vmid);
2011 PVE::VZDump::Plugin::remove_vmid_from_backup_jobs($vmid);
7c4351f7
TL
2012
2013 if ($ha_managed) {
2014 PVE::HA::Config::delete_service_from_config("vm:$vmid");
2015 print "NOTE: removed VM $vmid from HA resource configuration.\n";
2016 }
d9123ef5 2017 }
5172770d
TL
2018
2019 # only now remove the zombie config, else we can have reuse race
2020 PVE::QemuConfig->destroy_config($vmid);
3e8e214d 2021 });
5fdbe4f0 2022 };
1e3baf05 2023
a0d1b1a2 2024 return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd);
1e3baf05
DM
2025 }});
2026
2027__PACKAGE__->register_method({
afdb31d5
DM
2028 name => 'unlink',
2029 path => '{vmid}/unlink',
1e3baf05
DM
2030 method => 'PUT',
2031 protected => 1,
2032 proxyto => 'node',
2033 description => "Unlink/delete disk images.",
a0d1b1a2
DM
2034 permissions => {
2035 check => [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']],
2036 },
1e3baf05 2037 parameters => {
3326ae19 2038 additionalProperties => 0,
1e3baf05
DM
2039 properties => {
2040 node => get_standard_option('pve-node'),
335af808 2041 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1e3baf05
DM
2042 idlist => {
2043 type => 'string', format => 'pve-configid-list',
2044 description => "A list of disk IDs you want to delete.",
2045 },
2046 force => {
2047 type => 'boolean',
2048 description => $opt_force_description,
2049 optional => 1,
2050 },
2051 },
2052 },
2053 returns => { type => 'null'},
2054 code => sub {
2055 my ($param) = @_;
2056
2057 $param->{delete} = extract_param($param, 'idlist');
2058
2059 __PACKAGE__->update_vm($param);
2060
d1c1af4b 2061 return;
1e3baf05
DM
2062 }});
2063
3c5bdde8
TL
2064# uses good entropy, each char is limited to 6 bit to get printable chars simply
2065my $gen_rand_chars = sub {
2066 my ($length) = @_;
2067
2068 die "invalid length $length" if $length < 1;
2069
2070 my $min = ord('!'); # first printable ascii
a4e128a9
FG
2071
2072 my $rand_bytes = Crypt::OpenSSL::Random::random_bytes($length);
2073 die "failed to generate random bytes!\n"
2074 if !$rand_bytes;
2075
2076 my $str = join('', map { chr((ord($_) & 0x3F) + $min) } split('', $rand_bytes));
3c5bdde8
TL
2077
2078 return $str;
2079};
2080
1e3baf05
DM
2081my $sslcert;
2082
2083__PACKAGE__->register_method({
afdb31d5
DM
2084 name => 'vncproxy',
2085 path => '{vmid}/vncproxy',
1e3baf05
DM
2086 method => 'POST',
2087 protected => 1,
2088 permissions => {
378b359e 2089 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1e3baf05
DM
2090 },
2091 description => "Creates a TCP VNC proxy connections.",
2092 parameters => {
3326ae19 2093 additionalProperties => 0,
1e3baf05
DM
2094 properties => {
2095 node => get_standard_option('pve-node'),
2096 vmid => get_standard_option('pve-vmid'),
b4d5c000
SP
2097 websocket => {
2098 optional => 1,
2099 type => 'boolean',
2100 description => "starts websockify instead of vncproxy",
2101 },
3c5bdde8
TL
2102 'generate-password' => {
2103 optional => 1,
2104 type => 'boolean',
2105 default => 0,
2106 description => "Generates a random password to be used as ticket instead of the API ticket.",
2107 },
1e3baf05
DM
2108 },
2109 },
afdb31d5 2110 returns => {
3326ae19 2111 additionalProperties => 0,
1e3baf05
DM
2112 properties => {
2113 user => { type => 'string' },
2114 ticket => { type => 'string' },
3c5bdde8
TL
2115 password => {
2116 optional => 1,
2117 description => "Returned if requested with 'generate-password' param."
2118 ." Consists of printable ASCII characters ('!' .. '~').",
2119 type => 'string',
2120 },
1e3baf05
DM
2121 cert => { type => 'string' },
2122 port => { type => 'integer' },
2123 upid => { type => 'string' },
2124 },
2125 },
2126 code => sub {
2127 my ($param) = @_;
2128
2129 my $rpcenv = PVE::RPCEnvironment::get();
2130
a0d1b1a2 2131 my $authuser = $rpcenv->get_user();
1e3baf05
DM
2132
2133 my $vmid = $param->{vmid};
2134 my $node = $param->{node};
983d4582 2135 my $websocket = $param->{websocket};
1e3baf05 2136
ffda963f 2137 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
326007b2 2138
d3efae29
FG
2139 my $serial;
2140 if ($conf->{vga}) {
2141 my $vga = PVE::QemuServer::parse_vga($conf->{vga});
2142 $serial = $vga->{type} if $vga->{type} =~ m/^serial\d+$/;
2143 }
ef5e2be2 2144
b6f39da2
DM
2145 my $authpath = "/vms/$vmid";
2146
a0d1b1a2 2147 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
3c5bdde8
TL
2148 my $password = $ticket;
2149 if ($param->{'generate-password'}) {
2150 $password = $gen_rand_chars->(8);
2151 }
b6f39da2 2152
1e3baf05
DM
2153 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
2154 if !$sslcert;
2155
414b42d8 2156 my $family;
ef5e2be2 2157 my $remcmd = [];
afdb31d5 2158
4f1be36c 2159 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
414b42d8 2160 (undef, $family) = PVE::Cluster::remote_node_ip($node);
f42ea29b 2161 my $sshinfo = PVE::SSHInfo::get_ssh_info($node);
b4d5c000 2162 # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
d3efae29 2163 $remcmd = PVE::SSHInfo::ssh_info_to_command($sshinfo, defined($serial) ? '-t' : '-T');
af0eba7e
WB
2164 } else {
2165 $family = PVE::Tools::get_host_address_family($node);
1e3baf05
DM
2166 }
2167
af0eba7e
WB
2168 my $port = PVE::Tools::next_vnc_port($family);
2169
afdb31d5 2170 my $timeout = 10;
1e3baf05
DM
2171
2172 my $realcmd = sub {
2173 my $upid = shift;
2174
2175 syslog('info', "starting vnc proxy $upid\n");
2176
ef5e2be2 2177 my $cmd;
1e3baf05 2178
d3efae29 2179 if (defined($serial)) {
b4d5c000 2180
d3efae29 2181 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $serial, '-escape', '0' ];
9e6d6e97 2182
ef5e2be2 2183 $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
fa8ea931 2184 '-timeout', $timeout, '-authpath', $authpath,
9e6d6e97
DC
2185 '-perm', 'Sys.Console'];
2186
2187 if ($param->{websocket}) {
3c5bdde8 2188 $ENV{PVE_VNC_TICKET} = $password; # pass ticket to vncterm
9e6d6e97
DC
2189 push @$cmd, '-notls', '-listen', 'localhost';
2190 }
2191
2192 push @$cmd, '-c', @$remcmd, @$termcmd;
2193
655d7462 2194 PVE::Tools::run_command($cmd);
9e6d6e97 2195
ef5e2be2 2196 } else {
1e3baf05 2197
3c5bdde8 2198 $ENV{LC_PVE_TICKET} = $password if $websocket; # set ticket with "qm vncproxy"
3e7567e0 2199
655d7462
WB
2200 $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
2201
2202 my $sock = IO::Socket::IP->new(
dd32a466 2203 ReuseAddr => 1,
655d7462
WB
2204 Listen => 1,
2205 LocalPort => $port,
2206 Proto => 'tcp',
2207 GetAddrInfoFlags => 0,
b63f34b8 2208 ) or die "failed to create socket: $!\n";
655d7462
WB
2209 # Inside the worker we shouldn't have any previous alarms
2210 # running anyway...:
2211 alarm(0);
2212 local $SIG{ALRM} = sub { die "connection timed out\n" };
2213 alarm $timeout;
2214 accept(my $cli, $sock) or die "connection failed: $!\n";
058ff55b 2215 alarm(0);
655d7462
WB
2216 close($sock);
2217 if (PVE::Tools::run_command($cmd,
2218 output => '>&'.fileno($cli),
2219 input => '<&'.fileno($cli),
2220 noerr => 1) != 0)
2221 {
2222 die "Failed to run vncproxy.\n";
2223 }
ef5e2be2 2224 }
1e3baf05 2225
1e3baf05
DM
2226 return;
2227 };
2228
2c7fc947 2229 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
1e3baf05 2230
3da85107
DM
2231 PVE::Tools::wait_for_vnc_port($port);
2232
3c5bdde8 2233 my $res = {
a0d1b1a2 2234 user => $authuser,
1e3baf05 2235 ticket => $ticket,
afdb31d5
DM
2236 port => $port,
2237 upid => $upid,
2238 cert => $sslcert,
1e3baf05 2239 };
3c5bdde8
TL
2240 $res->{password} = $password if $param->{'generate-password'};
2241
2242 return $res;
1e3baf05
DM
2243 }});
2244
87302002
DC
2245__PACKAGE__->register_method({
2246 name => 'termproxy',
2247 path => '{vmid}/termproxy',
2248 method => 'POST',
2249 protected => 1,
2250 permissions => {
2251 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2252 },
2253 description => "Creates a TCP proxy connections.",
2254 parameters => {
2255 additionalProperties => 0,
2256 properties => {
2257 node => get_standard_option('pve-node'),
2258 vmid => get_standard_option('pve-vmid'),
2259 serial=> {
2260 optional => 1,
2261 type => 'string',
2262 enum => [qw(serial0 serial1 serial2 serial3)],
2263 description => "opens a serial terminal (defaults to display)",
2264 },
2265 },
2266 },
2267 returns => {
2268 additionalProperties => 0,
2269 properties => {
2270 user => { type => 'string' },
2271 ticket => { type => 'string' },
2272 port => { type => 'integer' },
2273 upid => { type => 'string' },
2274 },
2275 },
2276 code => sub {
2277 my ($param) = @_;
2278
2279 my $rpcenv = PVE::RPCEnvironment::get();
2280
2281 my $authuser = $rpcenv->get_user();
2282
2283 my $vmid = $param->{vmid};
2284 my $node = $param->{node};
2285 my $serial = $param->{serial};
2286
2287 my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
2288
2289 if (!defined($serial)) {
d7856be5
FG
2290 if ($conf->{vga}) {
2291 my $vga = PVE::QemuServer::parse_vga($conf->{vga});
2292 $serial = $vga->{type} if $vga->{type} =~ m/^serial\d+$/;
87302002
DC
2293 }
2294 }
2295
2296 my $authpath = "/vms/$vmid";
2297
2298 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
2299
414b42d8
DC
2300 my $family;
2301 my $remcmd = [];
87302002
DC
2302
2303 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
414b42d8 2304 (undef, $family) = PVE::Cluster::remote_node_ip($node);
f42ea29b
FG
2305 my $sshinfo = PVE::SSHInfo::get_ssh_info($node);
2306 $remcmd = PVE::SSHInfo::ssh_info_to_command($sshinfo, '-t');
414b42d8 2307 push @$remcmd, '--';
87302002
DC
2308 } else {
2309 $family = PVE::Tools::get_host_address_family($node);
2310 }
2311
2312 my $port = PVE::Tools::next_vnc_port($family);
2313
ccb88f45 2314 my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0'];
87302002
DC
2315 push @$termcmd, '-iface', $serial if $serial;
2316
2317 my $realcmd = sub {
2318 my $upid = shift;
2319
2320 syslog('info', "starting qemu termproxy $upid\n");
2321
2322 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
2323 '--perm', 'VM.Console', '--'];
2324 push @$cmd, @$remcmd, @$termcmd;
2325
2326 PVE::Tools::run_command($cmd);
2327 };
2328
2329 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
2330
2331 PVE::Tools::wait_for_vnc_port($port);
2332
2333 return {
2334 user => $authuser,
2335 ticket => $ticket,
2336 port => $port,
2337 upid => $upid,
2338 };
2339 }});
2340
3e7567e0
DM
2341__PACKAGE__->register_method({
2342 name => 'vncwebsocket',
2343 path => '{vmid}/vncwebsocket',
2344 method => 'GET',
3e7567e0 2345 permissions => {
c422ce93 2346 description => "You also need to pass a valid ticket (vncticket).",
3e7567e0
DM
2347 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2348 },
4d00f52f 2349 description => "Opens a weksocket for VNC traffic.",
3e7567e0 2350 parameters => {
3326ae19 2351 additionalProperties => 0,
3e7567e0
DM
2352 properties => {
2353 node => get_standard_option('pve-node'),
2354 vmid => get_standard_option('pve-vmid'),
c422ce93
DM
2355 vncticket => {
2356 description => "Ticket from previous call to vncproxy.",
2357 type => 'string',
2358 maxLength => 512,
2359 },
3e7567e0
DM
2360 port => {
2361 description => "Port number returned by previous vncproxy call.",
2362 type => 'integer',
2363 minimum => 5900,
2364 maximum => 5999,
2365 },
2366 },
2367 },
2368 returns => {
2369 type => "object",
2370 properties => {
2371 port => { type => 'string' },
2372 },
2373 },
2374 code => sub {
2375 my ($param) = @_;
2376
2377 my $rpcenv = PVE::RPCEnvironment::get();
2378
2379 my $authuser = $rpcenv->get_user();
2380
2381 my $vmid = $param->{vmid};
2382 my $node = $param->{node};
2383
c422ce93
DM
2384 my $authpath = "/vms/$vmid";
2385
2386 PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $authuser, $authpath);
2387
ffda963f 2388 my $conf = PVE::QemuConfig->load_config($vmid, $node); # VM exists ?
3e7567e0
DM
2389
2390 # Note: VNC ports are acessible from outside, so we do not gain any
2391 # security if we verify that $param->{port} belongs to VM $vmid. This
2392 # check is done by verifying the VNC ticket (inside VNC protocol).
2393
2394 my $port = $param->{port};
f34ebd52 2395
3e7567e0
DM
2396 return { port => $port };
2397 }});
2398
288eeea8
DM
2399__PACKAGE__->register_method({
2400 name => 'spiceproxy',
2401 path => '{vmid}/spiceproxy',
78252ce7 2402 method => 'POST',
288eeea8 2403 protected => 1,
78252ce7 2404 proxyto => 'node',
288eeea8
DM
2405 permissions => {
2406 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
2407 },
2408 description => "Returns a SPICE configuration to connect to the VM.",
2409 parameters => {
3326ae19 2410 additionalProperties => 0,
288eeea8
DM
2411 properties => {
2412 node => get_standard_option('pve-node'),
2413 vmid => get_standard_option('pve-vmid'),
dd25eecf 2414 proxy => get_standard_option('spice-proxy', { optional => 1 }),
288eeea8
DM
2415 },
2416 },
dd25eecf 2417 returns => get_standard_option('remote-viewer-config'),
288eeea8
DM
2418 code => sub {
2419 my ($param) = @_;
2420
2421 my $rpcenv = PVE::RPCEnvironment::get();
2422
2423 my $authuser = $rpcenv->get_user();
2424
2425 my $vmid = $param->{vmid};
2426 my $node = $param->{node};
fb6c7260 2427 my $proxy = $param->{proxy};
288eeea8 2428
ffda963f 2429 my $conf = PVE::QemuConfig->load_config($vmid, $node);
7f9e28e4
TL
2430 my $title = "VM $vmid";
2431 $title .= " - ". $conf->{name} if $conf->{name};
288eeea8 2432
943340a6 2433 my $port = PVE::QemuServer::spice_port($vmid);
dd25eecf 2434
f34ebd52 2435 my ($ticket, undef, $remote_viewer_config) =
dd25eecf 2436 PVE::AccessControl::remote_viewer_config($authuser, $vmid, $node, $proxy, $title, $port);
f34ebd52 2437
0a13e08e
SR
2438 mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket);
2439 mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30");
f34ebd52 2440
dd25eecf 2441 return $remote_viewer_config;
288eeea8
DM
2442 }});
2443
5fdbe4f0
DM
2444__PACKAGE__->register_method({
2445 name => 'vmcmdidx',
afdb31d5 2446 path => '{vmid}/status',
5fdbe4f0
DM
2447 method => 'GET',
2448 proxyto => 'node',
2449 description => "Directory index",
a0d1b1a2
DM
2450 permissions => {
2451 user => 'all',
2452 },
5fdbe4f0 2453 parameters => {
3326ae19 2454 additionalProperties => 0,
5fdbe4f0
DM
2455 properties => {
2456 node => get_standard_option('pve-node'),
2457 vmid => get_standard_option('pve-vmid'),
2458 },
2459 },
2460 returns => {
2461 type => 'array',
2462 items => {
2463 type => "object",
2464 properties => {
2465 subdir => { type => 'string' },
2466 },
2467 },
2468 links => [ { rel => 'child', href => "{subdir}" } ],
2469 },
2470 code => sub {
2471 my ($param) = @_;
2472
2473 # test if VM exists
ffda963f 2474 my $conf = PVE::QemuConfig->load_config($param->{vmid});
5fdbe4f0
DM
2475
2476 my $res = [
2477 { subdir => 'current' },
2478 { subdir => 'start' },
2479 { subdir => 'stop' },
58f9db6a
DC
2480 { subdir => 'reset' },
2481 { subdir => 'shutdown' },
2482 { subdir => 'suspend' },
165411f0 2483 { subdir => 'reboot' },
5fdbe4f0 2484 ];
afdb31d5 2485
5fdbe4f0
DM
2486 return $res;
2487 }});
2488
1e3baf05 2489__PACKAGE__->register_method({
afdb31d5 2490 name => 'vm_status',
5fdbe4f0 2491 path => '{vmid}/status/current',
1e3baf05
DM
2492 method => 'GET',
2493 proxyto => 'node',
2494 protected => 1, # qemu pid files are only readable by root
2495 description => "Get virtual machine status.",
a0d1b1a2
DM
2496 permissions => {
2497 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2498 },
1e3baf05 2499 parameters => {
3326ae19 2500 additionalProperties => 0,
1e3baf05
DM
2501 properties => {
2502 node => get_standard_option('pve-node'),
2503 vmid => get_standard_option('pve-vmid'),
2504 },
2505 },
b1a70cab
DM
2506 returns => {
2507 type => 'object',
2508 properties => {
2509 %$PVE::QemuServer::vmstatus_return_properties,
2510 ha => {
2511 description => "HA manager service status.",
2512 type => 'object',
2513 },
2514 spice => {
2515 description => "Qemu VGA configuration supports spice.",
2516 type => 'boolean',
2517 optional => 1,
2518 },
2519 agent => {
2520 description => "Qemu GuestAgent enabled in config.",
2521 type => 'boolean',
2522 optional => 1,
2523 },
2524 },
2525 },
1e3baf05
DM
2526 code => sub {
2527 my ($param) = @_;
2528
2529 # test if VM exists
ffda963f 2530 my $conf = PVE::QemuConfig->load_config($param->{vmid});
1e3baf05 2531
03a33f30 2532 my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid}, 1);
8610701a 2533 my $status = $vmstatus->{$param->{vmid}};
1e3baf05 2534
4d2a734e 2535 $status->{ha} = PVE::HA::Config::get_service_status("vm:$param->{vmid}");
8610701a 2536
3591b62b
TL
2537 if ($conf->{vga}) {
2538 my $vga = PVE::QemuServer::parse_vga($conf->{vga});
e88ceeca
FG
2539 my $spice = defined($vga->{type}) && $vga->{type} =~ /^virtio/;
2540 $spice ||= PVE::QemuServer::vga_conf_has_spice($conf->{vga});
2541 $status->{spice} = 1 if $spice;
3591b62b 2542 }
a2af1bbe 2543 $status->{agent} = 1 if PVE::QemuServer::get_qga_key($conf, 'enabled');
c9a074b8 2544
8610701a 2545 return $status;
1e3baf05
DM
2546 }});
2547
2548__PACKAGE__->register_method({
afdb31d5 2549 name => 'vm_start',
5fdbe4f0
DM
2550 path => '{vmid}/status/start',
2551 method => 'POST',
1e3baf05
DM
2552 protected => 1,
2553 proxyto => 'node',
5fdbe4f0 2554 description => "Start virtual machine.",
a0d1b1a2
DM
2555 permissions => {
2556 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2557 },
1e3baf05 2558 parameters => {
3326ae19 2559 additionalProperties => 0,
1e3baf05
DM
2560 properties => {
2561 node => get_standard_option('pve-node'),
ab5904f7
TL
2562 vmid => get_standard_option('pve-vmid',
2563 { completion => \&PVE::QemuServer::complete_vmid_stopped }),
3ea94c60
DM
2564 skiplock => get_standard_option('skiplock'),
2565 stateuri => get_standard_option('pve-qm-stateuri'),
7e8dcf2c 2566 migratedfrom => get_standard_option('pve-node',{ optional => 1 }),
2de2d6f7
TL
2567 migration_type => {
2568 type => 'string',
2569 enum => ['secure', 'insecure'],
2570 description => "Migration traffic is encrypted using an SSH " .
2571 "tunnel by default. On secure, completely private networks " .
2572 "this can be disabled to increase performance.",
2573 optional => 1,
2574 },
2575 migration_network => {
29ddbe70 2576 type => 'string', format => 'CIDR',
2de2d6f7
TL
2577 description => "CIDR of the (sub) network that is used for migration.",
2578 optional => 1,
2579 },
d58b93a8 2580 machine => get_standard_option('pve-qemu-machine'),
58c64ad5
SR
2581 'force-cpu' => {
2582 description => "Override QEMU's -cpu argument with the given string.",
2583 type => 'string',
2584 optional => 1,
2585 },
bf8fc5a3 2586 targetstorage => get_standard_option('pve-targetstorage'),
ef3f4293
TM
2587 timeout => {
2588 description => "Wait maximal timeout seconds.",
2589 type => 'integer',
2590 minimum => 0,
5a7f7b99 2591 default => 'max(30, vm memory in GiB)',
ef3f4293
TM
2592 optional => 1,
2593 },
1e3baf05
DM
2594 },
2595 },
afdb31d5 2596 returns => {
5fdbe4f0
DM
2597 type => 'string',
2598 },
1e3baf05
DM
2599 code => sub {
2600 my ($param) = @_;
2601
2602 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 2603 my $authuser = $rpcenv->get_user();
1e3baf05
DM
2604
2605 my $node = extract_param($param, 'node');
1e3baf05 2606 my $vmid = extract_param($param, 'vmid');
ef3f4293 2607 my $timeout = extract_param($param, 'timeout');
952958bc
DM
2608 my $machine = extract_param($param, 'machine');
2609
736c92f6
TL
2610 my $get_root_param = sub {
2611 my $value = extract_param($param, $_[0]);
2612 raise_param_exc({ "$_[0]" => "Only root may use this option." })
2613 if $value && $authuser ne 'root@pam';
2614 return $value;
2615 };
2de2d6f7 2616
736c92f6
TL
2617 my $stateuri = $get_root_param->('stateuri');
2618 my $skiplock = $get_root_param->('skiplock');
2619 my $migratedfrom = $get_root_param->('migratedfrom');
2620 my $migration_type = $get_root_param->('migration_type');
2621 my $migration_network = $get_root_param->('migration_network');
2622 my $targetstorage = $get_root_param->('targetstorage');
6ab41628 2623 my $force_cpu = $get_root_param->('force-cpu');
2189246c 2624
bf8fc5a3
FG
2625 my $storagemap;
2626
2627 if ($targetstorage) {
2628 raise_param_exc({ targetstorage => "targetstorage can only by used with migratedfrom." })
2629 if !$migratedfrom;
2630 $storagemap = eval { PVE::JSONSchema::parse_idmap($targetstorage, 'pve-storage-id') };
e214cda8 2631 raise_param_exc({ targetstorage => "failed to parse storage map: $@" })
bf8fc5a3
FG
2632 if $@;
2633 }
2189246c 2634
7c14dcae
DM
2635 # read spice ticket from STDIN
2636 my $spice_ticket;
c4ac8f71 2637 my $nbd_protocol_version = 0;
88126be3 2638 my $replicated_volumes = {};
13d121d7 2639 my $offline_volumes = {};
ccab68c2 2640 if ($stateuri && ($stateuri eq 'tcp' || $stateuri eq 'unix') && $migratedfrom && ($rpcenv->{type} eq 'cli')) {
c4ac8f71 2641 while (defined(my $line = <STDIN>)) {
760fb3c8 2642 chomp $line;
c4ac8f71
ML
2643 if ($line =~ m/^spice_ticket: (.+)$/) {
2644 $spice_ticket = $1;
2645 } elsif ($line =~ m/^nbd_protocol_version: (\d+)$/) {
2646 $nbd_protocol_version = $1;
88126be3
FG
2647 } elsif ($line =~ m/^replicated_volume: (.*)$/) {
2648 $replicated_volumes->{$1} = 1;
13d121d7
FE
2649 } elsif ($line =~ m/^tpmstate0: (.*)$/) { # Deprecated, use offline_volume instead
2650 $offline_volumes->{tpmstate0} = $1;
2651 } elsif ($line =~ m/^offline_volume: ([^:]+): (.*)$/) {
2652 $offline_volumes->{$1} = $2;
399ca0d6 2653 } elsif (!$spice_ticket) {
c4ac8f71
ML
2654 # fallback for old source node
2655 $spice_ticket = $line;
399ca0d6
FG
2656 } else {
2657 warn "unknown 'start' parameter on STDIN: '$line'\n";
c4ac8f71 2658 }
760fb3c8 2659 }
7c14dcae
DM
2660 }
2661
98cbd0f4
WB
2662 PVE::Cluster::check_cfs_quorum();
2663
afdb31d5 2664 my $storecfg = PVE::Storage::config();
5fdbe4f0 2665
a4262553 2666 if (PVE::HA::Config::vm_is_ha_managed($vmid) && !$stateuri && $rpcenv->{type} ne 'ha') {
88fc87b4
DM
2667 my $hacmd = sub {
2668 my $upid = shift;
5fdbe4f0 2669
02765844 2670 print "Requesting HA start for VM $vmid\n";
88fc87b4 2671
a4262553 2672 my $cmd = ['ha-manager', 'set', "vm:$vmid", '--state', 'started'];
88fc87b4 2673 PVE::Tools::run_command($cmd);
88fc87b4
DM
2674 return;
2675 };
2676
2677 return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
2678
2679 } else {
2680
2681 my $realcmd = sub {
2682 my $upid = shift;
2683
2684 syslog('info', "start VM $vmid: $upid\n");
2685
0c498cca
FG
2686 my $migrate_opts = {
2687 migratedfrom => $migratedfrom,
2688 spice_ticket => $spice_ticket,
2689 network => $migration_network,
2690 type => $migration_type,
bf8fc5a3 2691 storagemap => $storagemap,
0c498cca
FG
2692 nbd_proto_version => $nbd_protocol_version,
2693 replicated_volumes => $replicated_volumes,
13d121d7 2694 offline_volumes => $offline_volumes,
0c498cca
FG
2695 };
2696
2697 my $params = {
2698 statefile => $stateuri,
2699 skiplock => $skiplock,
2700 forcemachine => $machine,
2701 timeout => $timeout,
58c64ad5 2702 forcecpu => $force_cpu,
0c498cca
FG
2703 };
2704
2705 PVE::QemuServer::vm_start($storecfg, $vmid, $params, $migrate_opts);
88fc87b4
DM
2706 return;
2707 };
5fdbe4f0 2708
88fc87b4
DM
2709 return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd);
2710 }
5fdbe4f0
DM
2711 }});
2712
2713__PACKAGE__->register_method({
afdb31d5 2714 name => 'vm_stop',
5fdbe4f0
DM
2715 path => '{vmid}/status/stop',
2716 method => 'POST',
2717 protected => 1,
2718 proxyto => 'node',
346130b2 2719 description => "Stop virtual machine. The qemu process will exit immediately. This" .
d6c747ff 2720 "is akin to pulling the power plug of a running computer and may damage the VM data",
a0d1b1a2
DM
2721 permissions => {
2722 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2723 },
5fdbe4f0 2724 parameters => {
3326ae19 2725 additionalProperties => 0,
5fdbe4f0
DM
2726 properties => {
2727 node => get_standard_option('pve-node'),
ab5904f7
TL
2728 vmid => get_standard_option('pve-vmid',
2729 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 2730 skiplock => get_standard_option('skiplock'),
debe8882 2731 migratedfrom => get_standard_option('pve-node', { optional => 1 }),
c6bb9502
DM
2732 timeout => {
2733 description => "Wait maximal timeout seconds.",
2734 type => 'integer',
2735 minimum => 0,
2736 optional => 1,
254575e9
DM
2737 },
2738 keepActive => {
94a17e1d 2739 description => "Do not deactivate storage volumes.",
254575e9
DM
2740 type => 'boolean',
2741 optional => 1,
2742 default => 0,
c6bb9502 2743 }
5fdbe4f0
DM
2744 },
2745 },
afdb31d5 2746 returns => {
5fdbe4f0
DM
2747 type => 'string',
2748 },
2749 code => sub {
2750 my ($param) = @_;
2751
2752 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 2753 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2754
2755 my $node = extract_param($param, 'node');
5fdbe4f0
DM
2756 my $vmid = extract_param($param, 'vmid');
2757
2758 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2759 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2760 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2761
254575e9 2762 my $keepActive = extract_param($param, 'keepActive');
afdb31d5 2763 raise_param_exc({ keepActive => "Only root may use this option." })
a0d1b1a2 2764 if $keepActive && $authuser ne 'root@pam';
254575e9 2765
af30308f
DM
2766 my $migratedfrom = extract_param($param, 'migratedfrom');
2767 raise_param_exc({ migratedfrom => "Only root may use this option." })
2768 if $migratedfrom && $authuser ne 'root@pam';
2769
2770
ff1a2432
DM
2771 my $storecfg = PVE::Storage::config();
2772
2003f0f8 2773 if (PVE::HA::Config::vm_is_ha_managed($vmid) && ($rpcenv->{type} ne 'ha') && !defined($migratedfrom)) {
5fdbe4f0 2774
88fc87b4
DM
2775 my $hacmd = sub {
2776 my $upid = shift;
5fdbe4f0 2777
02765844 2778 print "Requesting HA stop for VM $vmid\n";
88fc87b4 2779
1805fac3 2780 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", '0'];
88fc87b4 2781 PVE::Tools::run_command($cmd);
88fc87b4
DM
2782 return;
2783 };
2784
2785 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2786
2787 } else {
2788 my $realcmd = sub {
2789 my $upid = shift;
2790
2791 syslog('info', "stop VM $vmid: $upid\n");
2792
2793 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0,
af30308f 2794 $param->{timeout}, 0, 1, $keepActive, $migratedfrom);
88fc87b4
DM
2795 return;
2796 };
2797
2798 return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd);
2799 }
5fdbe4f0
DM
2800 }});
2801
2802__PACKAGE__->register_method({
afdb31d5 2803 name => 'vm_reset',
5fdbe4f0
DM
2804 path => '{vmid}/status/reset',
2805 method => 'POST',
2806 protected => 1,
2807 proxyto => 'node',
2808 description => "Reset virtual machine.",
a0d1b1a2
DM
2809 permissions => {
2810 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2811 },
5fdbe4f0 2812 parameters => {
3326ae19 2813 additionalProperties => 0,
5fdbe4f0
DM
2814 properties => {
2815 node => get_standard_option('pve-node'),
ab5904f7
TL
2816 vmid => get_standard_option('pve-vmid',
2817 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0
DM
2818 skiplock => get_standard_option('skiplock'),
2819 },
2820 },
afdb31d5 2821 returns => {
5fdbe4f0
DM
2822 type => 'string',
2823 },
2824 code => sub {
2825 my ($param) = @_;
2826
2827 my $rpcenv = PVE::RPCEnvironment::get();
2828
a0d1b1a2 2829 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2830
2831 my $node = extract_param($param, 'node');
2832
2833 my $vmid = extract_param($param, 'vmid');
2834
2835 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2836 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2837 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2838
ff1a2432
DM
2839 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2840
5fdbe4f0
DM
2841 my $realcmd = sub {
2842 my $upid = shift;
2843
1e3baf05 2844 PVE::QemuServer::vm_reset($vmid, $skiplock);
5fdbe4f0
DM
2845
2846 return;
2847 };
2848
a0d1b1a2 2849 return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd);
5fdbe4f0
DM
2850 }});
2851
2852__PACKAGE__->register_method({
afdb31d5 2853 name => 'vm_shutdown',
5fdbe4f0
DM
2854 path => '{vmid}/status/shutdown',
2855 method => 'POST',
2856 protected => 1,
2857 proxyto => 'node',
d6c747ff
EK
2858 description => "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
2859 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
a0d1b1a2
DM
2860 permissions => {
2861 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2862 },
5fdbe4f0 2863 parameters => {
3326ae19 2864 additionalProperties => 0,
5fdbe4f0
DM
2865 properties => {
2866 node => get_standard_option('pve-node'),
ab5904f7
TL
2867 vmid => get_standard_option('pve-vmid',
2868 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 2869 skiplock => get_standard_option('skiplock'),
c6bb9502
DM
2870 timeout => {
2871 description => "Wait maximal timeout seconds.",
2872 type => 'integer',
2873 minimum => 0,
2874 optional => 1,
9269013a
DM
2875 },
2876 forceStop => {
2877 description => "Make sure the VM stops.",
2878 type => 'boolean',
2879 optional => 1,
2880 default => 0,
254575e9
DM
2881 },
2882 keepActive => {
94a17e1d 2883 description => "Do not deactivate storage volumes.",
254575e9
DM
2884 type => 'boolean',
2885 optional => 1,
2886 default => 0,
c6bb9502 2887 }
5fdbe4f0
DM
2888 },
2889 },
afdb31d5 2890 returns => {
5fdbe4f0
DM
2891 type => 'string',
2892 },
2893 code => sub {
2894 my ($param) = @_;
2895
2896 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 2897 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
2898
2899 my $node = extract_param($param, 'node');
5fdbe4f0
DM
2900 my $vmid = extract_param($param, 'vmid');
2901
2902 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 2903 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 2904 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 2905
254575e9 2906 my $keepActive = extract_param($param, 'keepActive');
afdb31d5 2907 raise_param_exc({ keepActive => "Only root may use this option." })
a0d1b1a2 2908 if $keepActive && $authuser ne 'root@pam';
254575e9 2909
02d07cf5
DM
2910 my $storecfg = PVE::Storage::config();
2911
89897367
DC
2912 my $shutdown = 1;
2913
2914 # if vm is paused, do not shutdown (but stop if forceStop = 1)
2915 # otherwise, we will infer a shutdown command, but run into the timeout,
2916 # then when the vm is resumed, it will instantly shutdown
2917 #
2918 # checking the qmp status here to get feedback to the gui/cli/api
2919 # and the status query should not take too long
b08c37c3 2920 if (PVE::QemuServer::vm_is_paused($vmid)) {
89897367
DC
2921 if ($param->{forceStop}) {
2922 warn "VM is paused - stop instead of shutdown\n";
2923 $shutdown = 0;
2924 } else {
2925 die "VM is paused - cannot shutdown\n";
2926 }
2927 }
2928
a4262553 2929 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
5fdbe4f0 2930
1805fac3 2931 my $timeout = $param->{timeout} // 60;
ae849692
DM
2932 my $hacmd = sub {
2933 my $upid = shift;
5fdbe4f0 2934
02765844 2935 print "Requesting HA stop for VM $vmid\n";
ae849692 2936
1805fac3 2937 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", "$timeout"];
ae849692 2938 PVE::Tools::run_command($cmd);
ae849692
DM
2939 return;
2940 };
2941
2942 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
2943
2944 } else {
2945
2946 my $realcmd = sub {
2947 my $upid = shift;
2948
2949 syslog('info', "shutdown VM $vmid: $upid\n");
5fdbe4f0 2950
ae849692
DM
2951 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout},
2952 $shutdown, $param->{forceStop}, $keepActive);
ae849692
DM
2953 return;
2954 };
2955
2956 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
2957 }
5fdbe4f0
DM
2958 }});
2959
165411f0
DC
2960__PACKAGE__->register_method({
2961 name => 'vm_reboot',
2962 path => '{vmid}/status/reboot',
2963 method => 'POST',
2964 protected => 1,
2965 proxyto => 'node',
2966 description => "Reboot the VM by shutting it down, and starting it again. Applies pending changes.",
2967 permissions => {
2968 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
2969 },
2970 parameters => {
2971 additionalProperties => 0,
2972 properties => {
2973 node => get_standard_option('pve-node'),
2974 vmid => get_standard_option('pve-vmid',
2975 { completion => \&PVE::QemuServer::complete_vmid_running }),
2976 timeout => {
2977 description => "Wait maximal timeout seconds for the shutdown.",
2978 type => 'integer',
2979 minimum => 0,
2980 optional => 1,
2981 },
2982 },
2983 },
2984 returns => {
2985 type => 'string',
2986 },
2987 code => sub {
2988 my ($param) = @_;
2989
2990 my $rpcenv = PVE::RPCEnvironment::get();
2991 my $authuser = $rpcenv->get_user();
2992
2993 my $node = extract_param($param, 'node');
2994 my $vmid = extract_param($param, 'vmid');
2995
b08c37c3 2996 die "VM is paused - cannot shutdown\n" if PVE::QemuServer::vm_is_paused($vmid);
165411f0
DC
2997
2998 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
2999
3000 my $realcmd = sub {
3001 my $upid = shift;
3002
3003 syslog('info', "requesting reboot of VM $vmid: $upid\n");
3004 PVE::QemuServer::vm_reboot($vmid, $param->{timeout});
3005 return;
3006 };
3007
3008 return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd);
3009 }});
3010
5fdbe4f0 3011__PACKAGE__->register_method({
afdb31d5 3012 name => 'vm_suspend',
5fdbe4f0
DM
3013 path => '{vmid}/status/suspend',
3014 method => 'POST',
3015 protected => 1,
3016 proxyto => 'node',
3017 description => "Suspend virtual machine.",
a0d1b1a2 3018 permissions => {
75c24bba
DC
3019 description => "You need 'VM.PowerMgmt' on /vms/{vmid}, and if you have set 'todisk',".
3020 " you need also 'VM.Config.Disk' on /vms/{vmid} and 'Datastore.AllocateSpace'".
3021 " on the storage for the vmstate.",
a0d1b1a2
DM
3022 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3023 },
5fdbe4f0 3024 parameters => {
3326ae19 3025 additionalProperties => 0,
5fdbe4f0
DM
3026 properties => {
3027 node => get_standard_option('pve-node'),
ab5904f7
TL
3028 vmid => get_standard_option('pve-vmid',
3029 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 3030 skiplock => get_standard_option('skiplock'),
22371fe0
DC
3031 todisk => {
3032 type => 'boolean',
3033 default => 0,
3034 optional => 1,
3035 description => 'If set, suspends the VM to disk. Will be resumed on next VM start.',
3036 },
48b4cdc2
DC
3037 statestorage => get_standard_option('pve-storage-id', {
3038 description => "The storage for the VM state",
3039 requires => 'todisk',
3040 optional => 1,
3041 completion => \&PVE::Storage::complete_storage_enabled,
3042 }),
5fdbe4f0
DM
3043 },
3044 },
afdb31d5 3045 returns => {
5fdbe4f0
DM
3046 type => 'string',
3047 },
3048 code => sub {
3049 my ($param) = @_;
3050
3051 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 3052 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
3053
3054 my $node = extract_param($param, 'node');
5fdbe4f0
DM
3055 my $vmid = extract_param($param, 'vmid');
3056
22371fe0
DC
3057 my $todisk = extract_param($param, 'todisk') // 0;
3058
48b4cdc2
DC
3059 my $statestorage = extract_param($param, 'statestorage');
3060
5fdbe4f0 3061 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 3062 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 3063 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 3064
ff1a2432
DM
3065 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
3066
22371fe0
DC
3067 die "Cannot suspend HA managed VM to disk\n"
3068 if $todisk && PVE::HA::Config::vm_is_ha_managed($vmid);
3069
75c24bba
DC
3070 # early check for storage permission, for better user feedback
3071 if ($todisk) {
3072 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
227a298f
DC
3073 my $conf = PVE::QemuConfig->load_config($vmid);
3074
876b24f2 3075 # cannot save the state of a non-virtualized PCIe device, so resume cannot really work
227a298f
DC
3076 for my $key (keys %$conf) {
3077 next if $key !~ /^hostpci\d+/;
161c2dde
TL
3078 die "cannot suspend VM to disk due to passed-through PCI device(s), which lack the"
3079 ." possibility to save/restore their internal state\n";
227a298f 3080 }
75c24bba
DC
3081
3082 if (!$statestorage) {
3083 # get statestorage from config if none is given
75c24bba
DC
3084 my $storecfg = PVE::Storage::config();
3085 $statestorage = PVE::QemuServer::find_vmstate_storage($conf, $storecfg);
3086 }
3087
3088 $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
3089 }
3090
5fdbe4f0
DM
3091 my $realcmd = sub {
3092 my $upid = shift;
3093
3094 syslog('info', "suspend VM $vmid: $upid\n");
3095
48b4cdc2 3096 PVE::QemuServer::vm_suspend($vmid, $skiplock, $todisk, $statestorage);
5fdbe4f0
DM
3097
3098 return;
3099 };
3100
a4262553 3101 my $taskname = $todisk ? 'qmsuspend' : 'qmpause';
f17fb184 3102 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
5fdbe4f0
DM
3103 }});
3104
3105__PACKAGE__->register_method({
afdb31d5 3106 name => 'vm_resume',
5fdbe4f0
DM
3107 path => '{vmid}/status/resume',
3108 method => 'POST',
3109 protected => 1,
3110 proxyto => 'node',
3111 description => "Resume virtual machine.",
a0d1b1a2
DM
3112 permissions => {
3113 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3114 },
5fdbe4f0 3115 parameters => {
3326ae19 3116 additionalProperties => 0,
5fdbe4f0
DM
3117 properties => {
3118 node => get_standard_option('pve-node'),
ab5904f7
TL
3119 vmid => get_standard_option('pve-vmid',
3120 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 3121 skiplock => get_standard_option('skiplock'),
289e0b85
AD
3122 nocheck => { type => 'boolean', optional => 1 },
3123
5fdbe4f0
DM
3124 },
3125 },
afdb31d5 3126 returns => {
5fdbe4f0
DM
3127 type => 'string',
3128 },
3129 code => sub {
3130 my ($param) = @_;
3131
3132 my $rpcenv = PVE::RPCEnvironment::get();
3133
a0d1b1a2 3134 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
3135
3136 my $node = extract_param($param, 'node');
3137
3138 my $vmid = extract_param($param, 'vmid');
3139
3140 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 3141 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 3142 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 3143
289e0b85 3144 my $nocheck = extract_param($param, 'nocheck');
4fb85adc
FG
3145 raise_param_exc({ nocheck => "Only root may use this option." })
3146 if $nocheck && $authuser ne 'root@pam';
289e0b85 3147
cd9a035b
TL
3148 my $to_disk_suspended;
3149 eval {
3150 PVE::QemuConfig->lock_config($vmid, sub {
3151 my $conf = PVE::QemuConfig->load_config($vmid);
3152 $to_disk_suspended = PVE::QemuConfig->has_lock($conf, 'suspended');
3153 });
3154 };
3155
3156 die "VM $vmid not running\n"
3157 if !$to_disk_suspended && !PVE::QemuServer::check_running($vmid, $nocheck);
ff1a2432 3158
5fdbe4f0
DM
3159 my $realcmd = sub {
3160 my $upid = shift;
3161
3162 syslog('info', "resume VM $vmid: $upid\n");
3163
cd9a035b
TL
3164 if (!$to_disk_suspended) {
3165 PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck);
3166 } else {
3167 my $storecfg = PVE::Storage::config();
0c498cca 3168 PVE::QemuServer::vm_start($storecfg, $vmid, { skiplock => $skiplock });
cd9a035b 3169 }
1e3baf05 3170
5fdbe4f0
DM
3171 return;
3172 };
3173
a0d1b1a2 3174 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
5fdbe4f0
DM
3175 }});
3176
3177__PACKAGE__->register_method({
afdb31d5 3178 name => 'vm_sendkey',
5fdbe4f0
DM
3179 path => '{vmid}/sendkey',
3180 method => 'PUT',
3181 protected => 1,
3182 proxyto => 'node',
3183 description => "Send key event to virtual machine.",
a0d1b1a2
DM
3184 permissions => {
3185 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
3186 },
5fdbe4f0 3187 parameters => {
3326ae19 3188 additionalProperties => 0,
5fdbe4f0
DM
3189 properties => {
3190 node => get_standard_option('pve-node'),
ab5904f7
TL
3191 vmid => get_standard_option('pve-vmid',
3192 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0
DM
3193 skiplock => get_standard_option('skiplock'),
3194 key => {
3195 description => "The key (qemu monitor encoding).",
3196 type => 'string'
3197 }
3198 },
3199 },
3200 returns => { type => 'null'},
3201 code => sub {
3202 my ($param) = @_;
3203
3204 my $rpcenv = PVE::RPCEnvironment::get();
3205
a0d1b1a2 3206 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
3207
3208 my $node = extract_param($param, 'node');
3209
3210 my $vmid = extract_param($param, 'vmid');
3211
3212 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 3213 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 3214 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0
DM
3215
3216 PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key});
3217
3218 return;
1e3baf05
DM
3219 }});
3220
1ac0d2ee
AD
3221__PACKAGE__->register_method({
3222 name => 'vm_feature',
3223 path => '{vmid}/feature',
3224 method => 'GET',
3225 proxyto => 'node',
75466c4f 3226 protected => 1,
1ac0d2ee
AD
3227 description => "Check if feature for virtual machine is available.",
3228 permissions => {
3229 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3230 },
3231 parameters => {
3326ae19 3232 additionalProperties => 0,
1ac0d2ee
AD
3233 properties => {
3234 node => get_standard_option('pve-node'),
3235 vmid => get_standard_option('pve-vmid'),
3236 feature => {
3237 description => "Feature to check.",
3238 type => 'string',
7758ce86 3239 enum => [ 'snapshot', 'clone', 'copy' ],
1ac0d2ee
AD
3240 },
3241 snapname => get_standard_option('pve-snapshot-name', {
3242 optional => 1,
3243 }),
3244 },
1ac0d2ee
AD
3245 },
3246 returns => {
719893a9
DM
3247 type => "object",
3248 properties => {
3249 hasFeature => { type => 'boolean' },
7043d946 3250 nodes => {
719893a9
DM
3251 type => 'array',
3252 items => { type => 'string' },
3253 }
3254 },
1ac0d2ee
AD
3255 },
3256 code => sub {
3257 my ($param) = @_;
3258
3259 my $node = extract_param($param, 'node');
3260
3261 my $vmid = extract_param($param, 'vmid');
3262
3263 my $snapname = extract_param($param, 'snapname');
3264
3265 my $feature = extract_param($param, 'feature');
3266
3267 my $running = PVE::QemuServer::check_running($vmid);
3268
ffda963f 3269 my $conf = PVE::QemuConfig->load_config($vmid);
1ac0d2ee
AD
3270
3271 if($snapname){
3272 my $snap = $conf->{snapshots}->{$snapname};
3273 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3274 $conf = $snap;
3275 }
3276 my $storecfg = PVE::Storage::config();
3277
719893a9 3278 my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg);
b2c9558d 3279 my $hasFeature = PVE::QemuConfig->has_feature($feature, $conf, $storecfg, $snapname, $running);
7043d946 3280
719893a9
DM
3281 return {
3282 hasFeature => $hasFeature,
3283 nodes => [ keys %$nodelist ],
7043d946 3284 };
1ac0d2ee
AD
3285 }});
3286
6116f729 3287__PACKAGE__->register_method({
9418baad
DM
3288 name => 'clone_vm',
3289 path => '{vmid}/clone',
6116f729
DM
3290 method => 'POST',
3291 protected => 1,
3292 proxyto => 'node',
37329185 3293 description => "Create a copy of virtual machine/template.",
6116f729 3294 permissions => {
9418baad 3295 description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
6116f729
DM
3296 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
3297 "'Datastore.AllocateSpace' on any used storage.",
75466c4f
DM
3298 check =>
3299 [ 'and',
9418baad 3300 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
75466c4f 3301 [ 'or',
6116f729
DM
3302 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
3303 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
3304 ],
3305 ]
3306 },
3307 parameters => {
3326ae19 3308 additionalProperties => 0,
6116f729 3309 properties => {
6116f729 3310 node => get_standard_option('pve-node'),
335af808 3311 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1ae43f8c
DM
3312 newid => get_standard_option('pve-vmid', {
3313 completion => \&PVE::Cluster::complete_next_vmid,
3314 description => 'VMID for the clone.' }),
a60ab1a6
DM
3315 name => {
3316 optional => 1,
3317 type => 'string', format => 'dns-name',
3318 description => "Set a name for the new VM.",
3319 },
3320 description => {
3321 optional => 1,
3322 type => 'string',
3323 description => "Description for the new VM.",
3324 },
75466c4f 3325 pool => {
6116f729
DM
3326 optional => 1,
3327 type => 'string', format => 'pve-poolid',
3328 description => "Add the new VM to the specified pool.",
3329 },
9076d880 3330 snapname => get_standard_option('pve-snapshot-name', {
9076d880
DM
3331 optional => 1,
3332 }),
81f043eb 3333 storage => get_standard_option('pve-storage-id', {
9418baad 3334 description => "Target storage for full clone.",
81f043eb
AD
3335 optional => 1,
3336 }),
55173c6b 3337 'format' => {
fd13b1d0 3338 description => "Target format for file storage. Only valid for full clone.",
42a19c87
AD
3339 type => 'string',
3340 optional => 1,
55173c6b 3341 enum => [ 'raw', 'qcow2', 'vmdk'],
42a19c87 3342 },
6116f729
DM
3343 full => {
3344 optional => 1,
55173c6b 3345 type => 'boolean',
fd13b1d0 3346 description => "Create a full copy of all disks. This is always done when " .
9418baad 3347 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
6116f729 3348 },
75466c4f 3349 target => get_standard_option('pve-node', {
55173c6b
DM
3350 description => "Target node. Only allowed if the original VM is on shared storage.",
3351 optional => 1,
3352 }),
0aab5a16
SI
3353 bwlimit => {
3354 description => "Override I/O bandwidth limit (in KiB/s).",
3355 optional => 1,
3356 type => 'integer',
3357 minimum => '0',
41756a3b 3358 default => 'clone limit from datacenter or storage config',
0aab5a16 3359 },
55173c6b 3360 },
6116f729
DM
3361 },
3362 returns => {
3363 type => 'string',
3364 },
3365 code => sub {
3366 my ($param) = @_;
3367
3368 my $rpcenv = PVE::RPCEnvironment::get();
a85ff91b 3369 my $authuser = $rpcenv->get_user();
6116f729
DM
3370
3371 my $node = extract_param($param, 'node');
6116f729 3372 my $vmid = extract_param($param, 'vmid');
6116f729 3373 my $newid = extract_param($param, 'newid');
6116f729 3374 my $pool = extract_param($param, 'pool');
6116f729 3375
55173c6b 3376 my $snapname = extract_param($param, 'snapname');
81f043eb 3377 my $storage = extract_param($param, 'storage');
42a19c87 3378 my $format = extract_param($param, 'format');
55173c6b
DM
3379 my $target = extract_param($param, 'target');
3380
3381 my $localnode = PVE::INotify::nodename();
3382
e099bad4 3383 if ($target && ($target eq $localnode || $target eq 'localhost')) {
a85ff91b 3384 undef $target;
a85ff91b 3385 }
55173c6b 3386
4df8fe45 3387 my $running = PVE::QemuServer::check_running($vmid) || 0;
d069275f 3388
4df8fe45
FG
3389 my $load_and_check = sub {
3390 $rpcenv->check_pool_exist($pool) if defined($pool);
3391 PVE::Cluster::check_node_exists($target) if $target;
6116f729 3392
4df8fe45 3393 my $storecfg = PVE::Storage::config();
4a5a2590 3394
4df8fe45
FG
3395 if ($storage) {
3396 # check if storage is enabled on local node
3397 PVE::Storage::storage_check_enabled($storecfg, $storage);
3398 if ($target) {
3399 # check if storage is available on target node
3400 PVE::Storage::storage_check_enabled($storecfg, $storage, $target);
3401 # clone only works if target storage is shared
3402 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
3403 die "can't clone to non-shared storage '$storage'\n"
3404 if !$scfg->{shared};
3405 }
3406 }
6116f729 3407
4df8fe45 3408 PVE::Cluster::check_cfs_quorum();
4e4f83fe 3409
ffda963f 3410 my $conf = PVE::QemuConfig->load_config($vmid);
ffda963f 3411 PVE::QemuConfig->check_lock($conf);
6116f729 3412
4e4f83fe 3413 my $verify_running = PVE::QemuServer::check_running($vmid) || 0;
4e4f83fe 3414 die "unexpected state change\n" if $verify_running != $running;
6116f729 3415
75466c4f
DM
3416 die "snapshot '$snapname' does not exist\n"
3417 if $snapname && !defined( $conf->{snapshots}->{$snapname});
6116f729 3418
dbecb46f 3419 my $full = $param->{full} // !PVE::QemuConfig->is_template($conf);
fd13b1d0
DM
3420
3421 die "parameter 'storage' not allowed for linked clones\n"
3422 if defined($storage) && !$full;
3423
3424 die "parameter 'format' not allowed for linked clones\n"
3425 if defined($format) && !$full;
3426
75466c4f 3427 my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
9076d880 3428
9418baad 3429 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
6116f729 3430
a85ff91b
TL
3431 die "can't clone VM to node '$target' (VM uses local storage)\n"
3432 if $target && !$sharedvm;
75466c4f 3433
ffda963f 3434 my $conffile = PVE::QemuConfig->config_file($newid);
6116f729
DM
3435 die "unable to create VM $newid: config file already exists\n"
3436 if -f $conffile;
3437
9418baad 3438 my $newconf = { lock => 'clone' };
829967a9 3439 my $drives = {};
34456bf0 3440 my $fullclone = {};
829967a9
DM
3441 my $vollist = [];
3442
3443 foreach my $opt (keys %$oldconf) {
3444 my $value = $oldconf->{$opt};
3445
3446 # do not copy snapshot related info
3447 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
3448 $opt eq 'vmstate' || $opt eq 'snapstate';
3449
a78ea5df
WL
3450 # no need to copy unused images, because VMID(owner) changes anyways
3451 next if $opt =~ m/^unused\d+$/;
3452
e4a70a41
FE
3453 die "cannot clone TPM state while VM is running\n"
3454 if $full && $running && !$snapname && $opt eq 'tpmstate0';
3455
829967a9
DM
3456 # always change MAC! address
3457 if ($opt =~ m/^net(\d+)$/) {
3458 my $net = PVE::QemuServer::parse_net($value);
b5b99790
WB
3459 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
3460 $net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
829967a9 3461 $newconf->{$opt} = PVE::QemuServer::print_net($net);
74479ee9 3462 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
1f1412d1
DM
3463 my $drive = PVE::QemuServer::parse_drive($opt, $value);
3464 die "unable to parse drive options for '$opt'\n" if !$drive;
7fe8b44c 3465 if (PVE::QemuServer::drive_is_cdrom($drive, 1)) {
829967a9
DM
3466 $newconf->{$opt} = $value; # simply copy configuration
3467 } else {
7fe8b44c 3468 if ($full || PVE::QemuServer::drive_is_cloudinit($drive)) {
6318daca 3469 die "Full clone feature is not supported for drive '$opt'\n"
dba198b0 3470 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running);
34456bf0 3471 $fullclone->{$opt} = 1;
64ff6fe4
SP
3472 } else {
3473 # not full means clone instead of copy
6318daca 3474 die "Linked clone feature is not supported for drive '$opt'\n"
64ff6fe4 3475 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $drive->{file}, $snapname, $running);
dba198b0 3476 }
829967a9 3477 $drives->{$opt} = $drive;
f8c4b2c5 3478 next if PVE::QemuServer::drive_is_cloudinit($drive);
829967a9
DM
3479 push @$vollist, $drive->{file};
3480 }
3481 } else {
3482 # copy everything else
3483 $newconf->{$opt} = $value;
3484 }
3485 }
3486
dbecb46f
FE
3487 return ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone);
3488 };
3489
3490 my $clonefn = sub {
3491 my ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone) = $load_and_check->();
4df8fe45 3492 my $storecfg = PVE::Storage::config();
dbecb46f
FE
3493
3494 # auto generate a new uuid
cd11416f 3495 my $smbios1 = PVE::QemuServer::parse_smbios1($newconf->{smbios1} || '');
6ee499ff 3496 $smbios1->{uuid} = PVE::QemuServer::generate_uuid();
cd11416f 3497 $newconf->{smbios1} = PVE::QemuServer::print_smbios1($smbios1);
a85ff91b 3498 # auto generate a new vmgenid only if the option was set for template
6ee499ff
DC
3499 if ($newconf->{vmgenid}) {
3500 $newconf->{vmgenid} = PVE::QemuServer::generate_uuid();
3501 }
3502
829967a9
DM
3503 delete $newconf->{template};
3504
3505 if ($param->{name}) {
3506 $newconf->{name} = $param->{name};
3507 } else {
a85ff91b 3508 $newconf->{name} = "Copy-of-VM-" . ($oldconf->{name} // $vmid);
829967a9 3509 }
2dd53043 3510
829967a9
DM
3511 if ($param->{description}) {
3512 $newconf->{description} = $param->{description};
3513 }
3514
6116f729 3515 # create empty/temp config - this fails if VM already exists on other node
a85ff91b 3516 # FIXME use PVE::QemuConfig->create_and_lock_config and adapt code
9418baad 3517 PVE::Tools::file_set_contents($conffile, "# qmclone temporary file\nlock: clone\n");
6116f729 3518
dbecb46f 3519 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
68e46b84 3520
dbecb46f
FE
3521 my $newvollist = [];
3522 my $jobs = {};
3523
3524 eval {
3525 local $SIG{INT} =
3526 local $SIG{TERM} =
3527 local $SIG{QUIT} =
3528 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
3529
3530 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
3531
3532 my $bwlimit = extract_param($param, 'bwlimit');
3533
3534 my $total_jobs = scalar(keys %{$drives});
3535 my $i = 1;
3536
3537 foreach my $opt (sort keys %$drives) {
3538 my $drive = $drives->{$opt};
3539 my $skipcomplete = ($total_jobs != $i); # finish after last drive
3540 my $completion = $skipcomplete ? 'skip' : 'complete';
3541
3542 my $src_sid = PVE::Storage::parse_volume_id($drive->{file});
3543 my $storage_list = [ $src_sid ];
3544 push @$storage_list, $storage if defined($storage);
3545 my $clonelimit = PVE::Storage::get_bandwidth_limit('clone', $storage_list, $bwlimit);
3546
1196086f
FE
3547 my $source_info = {
3548 vmid => $vmid,
3549 running => $running,
3550 drivename => $opt,
3551 drive => $drive,
3552 snapname => $snapname,
3553 };
3554
3555 my $dest_info = {
3556 vmid => $newid,
25166060 3557 drivename => $opt,
1196086f
FE
3558 storage => $storage,
3559 format => $format,
3560 };
3561
7344af7b
FE
3562 $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($oldconf)
3563 if $opt eq 'efidisk0';
3564
dbecb46f
FE
3565 my $newdrive = PVE::QemuServer::clone_disk(
3566 $storecfg,
1196086f
FE
3567 $source_info,
3568 $dest_info,
dbecb46f
FE
3569 $fullclone->{$opt},
3570 $newvollist,
3571 $jobs,
3572 $completion,
3573 $oldconf->{agent},
3574 $clonelimit,
dbecb46f
FE
3575 );
3576
3577 $newconf->{$opt} = PVE::QemuServer::print_drive($newdrive);
68e46b84 3578
ffda963f 3579 PVE::QemuConfig->write_config($newid, $newconf);
dbecb46f
FE
3580 $i++;
3581 }
55173c6b 3582
dbecb46f 3583 delete $newconf->{lock};
baca276d 3584
dbecb46f
FE
3585 # do not write pending changes
3586 if (my @changes = keys %{$newconf->{pending}}) {
3587 my $pending = join(',', @changes);
3588 warn "found pending changes for '$pending', discarding for clone\n";
3589 delete $newconf->{pending};
3590 }
d703d4c0 3591
dbecb46f 3592 PVE::QemuConfig->write_config($newid, $newconf);
b83e0181 3593
dbecb46f
FE
3594 if ($target) {
3595 # always deactivate volumes - avoid lvm LVs to be active on several nodes
3596 PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
3597 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
c05c90a1 3598
dbecb46f
FE
3599 my $newconffile = PVE::QemuConfig->config_file($newid, $target);
3600 die "Failed to move config to node '$target' - rename failed: $!\n"
3601 if !rename($conffile, $newconffile);
3602 }
c05c90a1 3603
dbecb46f
FE
3604 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
3605 };
3606 if (my $err = $@) {
3607 eval { PVE::QemuServer::qemu_blockjobs_cancel($vmid, $jobs) };
3608 sleep 1; # some storage like rbd need to wait before release volume - really?
990b65ab 3609
dbecb46f
FE
3610 foreach my $volid (@$newvollist) {
3611 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
3612 warn $@ if $@;
6116f729
DM
3613 }
3614
dbecb46f 3615 PVE::Firewall::remove_vmfw_conf($newid);
6116f729 3616
dbecb46f
FE
3617 unlink $conffile; # avoid races -> last thing before die
3618
3619 die "clone failed: $err";
3620 }
457010cc 3621
dbecb46f 3622 return;
6116f729
DM
3623 };
3624
45fd77bb
FG
3625 # Aquire exclusive lock lock for $newid
3626 my $lock_target_vm = sub {
ffda963f 3627 return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn);
45fd77bb 3628 };
6116f729 3629
dbecb46f
FE
3630 my $lock_source_vm = sub {
3631 # exclusive lock if VM is running - else shared lock is enough;
3632 if ($running) {
3633 return PVE::QemuConfig->lock_config_full($vmid, 1, $lock_target_vm);
3634 } else {
3635 return PVE::QemuConfig->lock_config_shared($vmid, 1, $lock_target_vm);
3636 }
3637 };
3638
3639 $load_and_check->(); # early checks before forking/locking
3640
3641 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $lock_source_vm);
6116f729
DM
3642 }});
3643
586bfa78 3644__PACKAGE__->register_method({
43bc02a9
DM
3645 name => 'move_vm_disk',
3646 path => '{vmid}/move_disk',
e2cd75fa 3647 method => 'POST',
586bfa78
AD
3648 protected => 1,
3649 proxyto => 'node',
a9453218 3650 description => "Move volume to different storage or to a different VM.",
586bfa78 3651 permissions => {
a9453218
AL
3652 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
3653 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
3654 "a disk to another VM, you need the permissions on the target VM as well.",
44102492 3655 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
586bfa78
AD
3656 },
3657 parameters => {
3326ae19 3658 additionalProperties => 0,
c07a9e3d 3659 properties => {
586bfa78 3660 node => get_standard_option('pve-node'),
335af808 3661 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
a9453218
AL
3662 'target-vmid' => get_standard_option('pve-vmid', {
3663 completion => \&PVE::QemuServer::complete_vmid,
3664 optional => 1,
3665 }),
586bfa78
AD
3666 disk => {
3667 type => 'string',
3668 description => "The disk you want to move.",
a9453218 3669 enum => [PVE::QemuServer::Drive::valid_drive_names_with_unused()],
586bfa78 3670 },
335af808
DM
3671 storage => get_standard_option('pve-storage-id', {
3672 description => "Target storage.",
3673 completion => \&PVE::QemuServer::complete_storage,
a9453218 3674 optional => 1,
335af808 3675 }),
f519ab0b
TL
3676 'format' => {
3677 type => 'string',
3678 description => "Target Format.",
3679 enum => [ 'raw', 'qcow2', 'vmdk' ],
3680 optional => 1,
3681 },
70d45e33
DM
3682 delete => {
3683 type => 'boolean',
f519ab0b
TL
3684 description => "Delete the original disk after successful copy. By default the"
3685 ." original disk is kept as unused disk.",
70d45e33
DM
3686 optional => 1,
3687 default => 0,
3688 },
586bfa78
AD
3689 digest => {
3690 type => 'string',
f519ab0b
TL
3691 description => 'Prevent changes if current configuration file has different SHA1"
3692 ." digest. This can be used to prevent concurrent modifications.',
586bfa78
AD
3693 maxLength => 40,
3694 optional => 1,
3695 },
0aab5a16
SI
3696 bwlimit => {
3697 description => "Override I/O bandwidth limit (in KiB/s).",
3698 optional => 1,
3699 type => 'integer',
3700 minimum => '0',
41756a3b 3701 default => 'move limit from datacenter or storage config',
0aab5a16 3702 },
a9453218
AL
3703 'target-disk' => {
3704 type => 'string',
f519ab0b
TL
3705 description => "The config key the disk will be moved to on the target VM"
3706 ." (for example, ide0 or scsi1). Default is the source disk key.",
a9453218
AL
3707 enum => [PVE::QemuServer::Drive::valid_drive_names_with_unused()],
3708 optional => 1,
3709 },
3710 'target-digest' => {
3711 type => 'string',
f519ab0b
TL
3712 description => 'Prevent changes if the current config file of the target VM has a"
3713 ." different SHA1 digest. This can be used to detect concurrent modifications.',
a9453218
AL
3714 maxLength => 40,
3715 optional => 1,
3716 },
586bfa78
AD
3717 },
3718 },
e2cd75fa
DM
3719 returns => {
3720 type => 'string',
3721 description => "the task ID.",
3722 },
586bfa78
AD
3723 code => sub {
3724 my ($param) = @_;
3725
3726 my $rpcenv = PVE::RPCEnvironment::get();
586bfa78
AD
3727 my $authuser = $rpcenv->get_user();
3728
3729 my $node = extract_param($param, 'node');
586bfa78 3730 my $vmid = extract_param($param, 'vmid');
a9453218 3731 my $target_vmid = extract_param($param, 'target-vmid');
586bfa78 3732 my $digest = extract_param($param, 'digest');
a9453218 3733 my $target_digest = extract_param($param, 'target-digest');
586bfa78 3734 my $disk = extract_param($param, 'disk');
a9453218 3735 my $target_disk = extract_param($param, 'target-disk') // $disk;
586bfa78 3736 my $storeid = extract_param($param, 'storage');
586bfa78
AD
3737 my $format = extract_param($param, 'format');
3738
586bfa78
AD
3739 my $storecfg = PVE::Storage::config();
3740
bdf6ba1e 3741 my $load_and_check_move = sub {
ffda963f 3742 my $conf = PVE::QemuConfig->load_config($vmid);
dcce9b46
FG
3743 PVE::QemuConfig->check_lock($conf);
3744
44102492 3745 PVE::Tools::assert_if_modified($digest, $conf->{digest});
586bfa78
AD
3746
3747 die "disk '$disk' does not exist\n" if !$conf->{$disk};
3748
3749 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
3750
a85ff91b 3751 die "disk '$disk' has no associated volume\n" if !$drive->{file};
931432bd 3752 die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive, 1);
586bfa78 3753
a85ff91b 3754 my $old_volid = $drive->{file};
e2cd75fa 3755 my $oldfmt;
70d45e33 3756 my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
586bfa78
AD
3757 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
3758 $oldfmt = $1;
3759 }
3760
bdf6ba1e
FE
3761 die "you can't move to the same storage with same format\n"
3762 if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format);
586bfa78 3763
9dbf9b54 3764 # this only checks snapshots because $disk is passed!
70c0ad66
AL
3765 my $snapshotted = PVE::QemuServer::Drive::is_volume_in_use(
3766 $storecfg,
3767 $conf,
3768 $disk,
3769 $old_volid
3770 );
9dbf9b54
FG
3771 die "you can't move a disk with snapshots and delete the source\n"
3772 if $snapshotted && $param->{delete};
3773
bdf6ba1e
FE
3774 return ($conf, $drive, $oldstoreid, $snapshotted);
3775 };
3776
3777 my $move_updatefn = sub {
3778 my ($conf, $drive, $oldstoreid, $snapshotted) = $load_and_check_move->();
3779 my $old_volid = $drive->{file};
3780
70c0ad66
AL
3781 PVE::Cluster::log_msg(
3782 'info',
3783 $authuser,
3784 "move disk VM $vmid: move --disk $disk --storage $storeid"
3785 );
586bfa78
AD
3786
3787 my $running = PVE::QemuServer::check_running($vmid);
e2cd75fa
DM
3788
3789 PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]);
3790
bdf6ba1e 3791 my $newvollist = [];
586bfa78 3792
bdf6ba1e
FE
3793 eval {
3794 local $SIG{INT} =
3795 local $SIG{TERM} =
3796 local $SIG{QUIT} =
3797 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
0aab5a16 3798
bdf6ba1e
FE
3799 warn "moving disk with snapshots, snapshots will not be moved!\n"
3800 if $snapshotted;
e2cd75fa 3801
bdf6ba1e
FE
3802 my $bwlimit = extract_param($param, 'bwlimit');
3803 my $movelimit = PVE::Storage::get_bandwidth_limit(
3804 'move',
3805 [$oldstoreid, $storeid],
3806 $bwlimit
3807 );
7043d946 3808
1196086f
FE
3809 my $source_info = {
3810 vmid => $vmid,
3811 running => $running,
3812 drivename => $disk,
3813 drive => $drive,
3814 snapname => undef,
3815 };
3816
3817 my $dest_info = {
3818 vmid => $vmid,
25166060 3819 drivename => $disk,
1196086f
FE
3820 storage => $storeid,
3821 format => $format,
3822 };
3823
7344af7b
FE
3824 $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($conf)
3825 if $disk eq 'efidisk0';
3826
bdf6ba1e
FE
3827 my $newdrive = PVE::QemuServer::clone_disk(
3828 $storecfg,
1196086f
FE
3829 $source_info,
3830 $dest_info,
bdf6ba1e
FE
3831 1,
3832 $newvollist,
3833 undef,
3834 undef,
3835 undef,
3836 $movelimit,
bdf6ba1e
FE
3837 );
3838 $conf->{$disk} = PVE::QemuServer::print_drive($newdrive);
fbd7dcce 3839
bdf6ba1e 3840 PVE::QemuConfig->add_unused_volume($conf, $old_volid) if !$param->{delete};
73272365 3841
bdf6ba1e
FE
3842 # convert moved disk to base if part of template
3843 PVE::QemuServer::template_create($vmid, $conf, $disk)
3844 if PVE::QemuConfig->is_template($conf);
ca662131 3845
bdf6ba1e 3846 PVE::QemuConfig->write_config($vmid, $conf);
70d45e33 3847
bdf6ba1e
FE
3848 my $do_trim = PVE::QemuServer::get_qga_key($conf, 'fstrim_cloned_disks');
3849 if ($running && $do_trim && PVE::QemuServer::qga_check_running($vmid)) {
3850 eval { mon_cmd($vmid, "guest-fstrim") };
70d45e33 3851 }
bdf6ba1e
FE
3852
3853 eval {
3854 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
3855 PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ])
3856 if !$running;
3857 };
3858 warn $@ if $@;
586bfa78 3859 };
bdf6ba1e
FE
3860 if (my $err = $@) {
3861 foreach my $volid (@$newvollist) {
3862 eval { PVE::Storage::vdisk_free($storecfg, $volid) };
3863 warn $@ if $@;
3864 }
3865 die "storage migration failed: $err";
3866 }
586bfa78 3867
bdf6ba1e
FE
3868 if ($param->{delete}) {
3869 eval {
3870 PVE::Storage::deactivate_volumes($storecfg, [$old_volid]);
3871 PVE::Storage::vdisk_free($storecfg, $old_volid);
3872 };
3873 warn $@ if $@;
3874 }
586bfa78 3875 };
e2cd75fa 3876
a9453218
AL
3877 my $load_and_check_reassign_configs = sub {
3878 my $vmlist = PVE::Cluster::get_vmlist()->{ids};
3879
3880 die "could not find VM ${vmid}\n" if !exists($vmlist->{$vmid});
dbc817ba 3881 die "could not find target VM ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
a9453218 3882
dbc817ba
FG
3883 my $source_node = $vmlist->{$vmid}->{node};
3884 my $target_node = $vmlist->{$target_vmid}->{node};
3885
3886 die "Both VMs need to be on the same node ($source_node != $target_node)\n"
3887 if $source_node ne $target_node;
a9453218
AL
3888
3889 my $source_conf = PVE::QemuConfig->load_config($vmid);
3890 PVE::QemuConfig->check_lock($source_conf);
3891 my $target_conf = PVE::QemuConfig->load_config($target_vmid);
3892 PVE::QemuConfig->check_lock($target_conf);
3893
3894 die "Can't move disks from or to template VMs\n"
3895 if ($source_conf->{template} || $target_conf->{template});
3896
3897 if ($digest) {
3898 eval { PVE::Tools::assert_if_modified($digest, $source_conf->{digest}) };
3899 die "VM ${vmid}: $@" if $@;
3900 }
3901
3902 if ($target_digest) {
3903 eval { PVE::Tools::assert_if_modified($target_digest, $target_conf->{digest}) };
3904 die "VM ${target_vmid}: $@" if $@;
3905 }
3906
3907 die "Disk '${disk}' for VM '$vmid' does not exist\n" if !defined($source_conf->{$disk});
3908
3909 die "Target disk key '${target_disk}' is already in use for VM '$target_vmid'\n"
dbc817ba 3910 if $target_conf->{$target_disk};
a9453218
AL
3911
3912 my $drive = PVE::QemuServer::parse_drive(
3913 $disk,
3914 $source_conf->{$disk},
3915 );
dbc817ba
FG
3916 die "failed to parse source disk - $@\n" if !$drive;
3917
3918 my $source_volid = $drive->{file};
a9453218
AL
3919
3920 die "disk '${disk}' has no associated volume\n" if !$source_volid;
3921 die "CD drive contents can't be moved to another VM\n"
3922 if PVE::QemuServer::drive_is_cdrom($drive, 1);
dbc817ba
FG
3923
3924 my $storeid = PVE::Storage::parse_volume_id($source_volid, 1);
3925 die "Volume '$source_volid' not managed by PVE\n" if !defined($storeid);
3926
a9453218
AL
3927 die "Can't move disk used by a snapshot to another VM\n"
3928 if PVE::QemuServer::Drive::is_volume_in_use($storecfg, $source_conf, $disk, $source_volid);
3929 die "Storage does not support moving of this disk to another VM\n"
3930 if (!PVE::Storage::volume_has_feature($storecfg, 'rename', $source_volid));
dbc817ba 3931 die "Cannot move disk to another VM while the source VM is running - detach first\n"
a9453218
AL
3932 if PVE::QemuServer::check_running($vmid) && $disk !~ m/^unused\d+$/;
3933
dbc817ba 3934 # now re-parse using target disk slot format
f4e4c779
FG
3935 if ($target_disk =~ /^unused\d+$/) {
3936 $drive = PVE::QemuServer::parse_drive(
3937 $target_disk,
3938 $source_volid,
3939 );
3940 } else {
3941 $drive = PVE::QemuServer::parse_drive(
3942 $target_disk,
3943 $source_conf->{$disk},
3944 );
3945 }
dbc817ba 3946 die "failed to parse source disk for target disk format - $@\n" if !$drive;
a9453218
AL
3947
3948 my $repl_conf = PVE::ReplicationConfig->new();
dbc817ba
FG
3949 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
3950 my $format = (PVE::Storage::parse_volname($storecfg, $source_volid))[6];
3951 die "Cannot move disk to a replicated VM. Storage does not support replication!\n"
3952 if !PVE::Storage::storage_can_replicate($storecfg, $storeid, $format);
a9453218
AL
3953 }
3954
dbc817ba 3955 return ($source_conf, $target_conf, $drive);
a9453218
AL
3956 };
3957
3958 my $logfunc = sub {
3959 my ($msg) = @_;
3960 print STDERR "$msg\n";
3961 };
3962
3963 my $disk_reassignfn = sub {
3964 return PVE::QemuConfig->lock_config($vmid, sub {
3965 return PVE::QemuConfig->lock_config($target_vmid, sub {
dbc817ba 3966 my ($source_conf, $target_conf, $drive) = &$load_and_check_reassign_configs();
a9453218 3967
dbc817ba 3968 my $source_volid = $drive->{file};
a9453218
AL
3969
3970 print "moving disk '$disk' from VM '$vmid' to '$target_vmid'\n";
3971 my ($storeid, $source_volname) = PVE::Storage::parse_volume_id($source_volid);
3972
3973 my $fmt = (PVE::Storage::parse_volname($storecfg, $source_volid))[6];
3974
3975 my $new_volid = PVE::Storage::rename_volume(
3976 $storecfg,
3977 $source_volid,
3978 $target_vmid,
3979 );
3980
dbc817ba 3981 $drive->{file} = $new_volid;
a9453218 3982
96670745
TL
3983 my $boot_order = PVE::QemuServer::device_bootorder($source_conf);
3984 if (defined(delete $boot_order->{$disk})) {
3985 print "removing disk '$disk' from boot order config\n";
3986 my $boot_devs = [ sort { $boot_order->{$a} <=> $boot_order->{$b} } keys %$boot_order ];
3987 $source_conf->{boot} = PVE::QemuServer::print_bootorder($boot_devs);
3988 }
3989
a9453218
AL
3990 delete $source_conf->{$disk};
3991 print "removing disk '${disk}' from VM '${vmid}' config\n";
3992 PVE::QemuConfig->write_config($vmid, $source_conf);
3993
dbc817ba 3994 my $drive_string = PVE::QemuServer::print_drive($drive);
bf67da2b
AL
3995
3996 if ($target_disk =~ /^unused\d+$/) {
3997 $target_conf->{$target_disk} = $drive_string;
3998 PVE::QemuConfig->write_config($target_vmid, $target_conf);
3999 } else {
4000 &$update_vm_api(
4001 {
4002 node => $node,
4003 vmid => $target_vmid,
4004 digest => $target_digest,
4005 $target_disk => $drive_string,
4006 },
4007 1,
4008 );
4009 }
a9453218
AL
4010
4011 # remove possible replication snapshots
4012 if (PVE::Storage::volume_has_feature(
4013 $storecfg,
4014 'replicate',
4015 $source_volid),
4016 ) {
4017 eval {
4018 PVE::Replication::prepare(
4019 $storecfg,
4020 [$new_volid],
4021 undef,
4022 1,
4023 undef,
4024 $logfunc,
4025 )
4026 };
4027 if (my $err = $@) {
4028 print "Failed to remove replication snapshots on moved disk " .
4029 "'$target_disk'. Manual cleanup could be necessary.\n";
4030 }
4031 }
4032 });
4033 });
4034 };
4035
dbc817ba
FG
4036 if ($target_vmid && $storeid) {
4037 my $msg = "either set 'storage' or 'target-vmid', but not both";
4038 raise_param_exc({ 'target-vmid' => $msg, 'storage' => $msg });
4039 } elsif ($target_vmid) {
a9453218
AL
4040 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
4041 if $authuser ne 'root@pam';
4042
dbc817ba 4043 raise_param_exc({ 'target-vmid' => "must be different than source VMID to reassign disk" })
a9453218
AL
4044 if $vmid eq $target_vmid;
4045
44102492 4046 my (undef, undef, $drive) = &$load_and_check_reassign_configs();
a6273aa8
FG
4047 my $storage = PVE::Storage::parse_volume_id($drive->{file});
4048 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
44102492 4049
a9453218
AL
4050 return $rpcenv->fork_worker(
4051 'qmmove',
4052 "${vmid}-${disk}>${target_vmid}-${target_disk}",
4053 $authuser,
4054 $disk_reassignfn
4055 );
4056 } elsif ($storeid) {
44102492
FG
4057 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4058
a9453218
AL
4059 die "cannot move disk '$disk', only configured disks can be moved to another storage\n"
4060 if $disk =~ m/^unused\d+$/;
bdf6ba1e
FE
4061
4062 $load_and_check_move->(); # early checks before forking/locking
4063
4064 my $realcmd = sub {
4065 PVE::QemuConfig->lock_config($vmid, $move_updatefn);
4066 };
4067
4068 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
a9453218 4069 } else {
dbc817ba
FG
4070 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
4071 raise_param_exc({ 'target-vmid' => $msg, 'storage' => $msg });
a9453218 4072 }
586bfa78
AD
4073 }});
4074
71fc647f
TM
4075my $check_vm_disks_local = sub {
4076 my ($storecfg, $vmconf, $vmid) = @_;
4077
4078 my $local_disks = {};
4079
4080 # add some more information to the disks e.g. cdrom
4081 PVE::QemuServer::foreach_volid($vmconf, sub {
4082 my ($volid, $attr) = @_;
4083
4084 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
4085 if ($storeid) {
4086 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
4087 return if $scfg->{shared};
4088 }
4089 # The shared attr here is just a special case where the vdisk
4090 # is marked as shared manually
4091 return if $attr->{shared};
4092 return if $attr->{cdrom} and $volid eq "none";
4093
4094 if (exists $local_disks->{$volid}) {
4095 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
4096 } else {
4097 $local_disks->{$volid} = $attr;
4098 # ensure volid is present in case it's needed
4099 $local_disks->{$volid}->{volid} = $volid;
4100 }
4101 });
4102
4103 return $local_disks;
4104};
4105
4106__PACKAGE__->register_method({
4107 name => 'migrate_vm_precondition',
4108 path => '{vmid}/migrate',
4109 method => 'GET',
4110 protected => 1,
4111 proxyto => 'node',
4112 description => "Get preconditions for migration.",
4113 permissions => {
4114 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4115 },
4116 parameters => {
4117 additionalProperties => 0,
4118 properties => {
4119 node => get_standard_option('pve-node'),
4120 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
4121 target => get_standard_option('pve-node', {
4122 description => "Target node.",
4123 completion => \&PVE::Cluster::complete_migration_target,
4124 optional => 1,
4125 }),
4126 },
4127 },
4128 returns => {
4129 type => "object",
4130 properties => {
4131 running => { type => 'boolean' },
4132 allowed_nodes => {
4133 type => 'array',
4134 optional => 1,
f25852c2
TM
4135 description => "List nodes allowed for offline migration, only passed if VM is offline"
4136 },
4137 not_allowed_nodes => {
4138 type => 'object',
4139 optional => 1,
4140 description => "List not allowed nodes with additional informations, only passed if VM is offline"
71fc647f
TM
4141 },
4142 local_disks => {
4143 type => 'array',
4144 description => "List local disks including CD-Rom, unsused and not referenced disks"
4145 },
4146 local_resources => {
4147 type => 'array',
4148 description => "List local resources e.g. pci, usb"
4149 }
4150 },
4151 },
4152 code => sub {
4153 my ($param) = @_;
4154
4155 my $rpcenv = PVE::RPCEnvironment::get();
4156
4157 my $authuser = $rpcenv->get_user();
4158
4159 PVE::Cluster::check_cfs_quorum();
4160
4161 my $res = {};
4162
4163 my $vmid = extract_param($param, 'vmid');
4164 my $target = extract_param($param, 'target');
4165 my $localnode = PVE::INotify::nodename();
4166
4167
4168 # test if VM exists
4169 my $vmconf = PVE::QemuConfig->load_config($vmid);
4170 my $storecfg = PVE::Storage::config();
4171
4172
4173 # try to detect errors early
4174 PVE::QemuConfig->check_lock($vmconf);
4175
4176 $res->{running} = PVE::QemuServer::check_running($vmid) ? 1:0;
4177
4178 # if vm is not running, return target nodes where local storage is available
4179 # for offline migration
4180 if (!$res->{running}) {
f25852c2
TM
4181 $res->{allowed_nodes} = [];
4182 my $checked_nodes = PVE::QemuServer::check_local_storage_availability($vmconf, $storecfg);
32075a2c 4183 delete $checked_nodes->{$localnode};
f25852c2 4184
f25852c2 4185 foreach my $node (keys %$checked_nodes) {
32075a2c 4186 if (!defined $checked_nodes->{$node}->{unavailable_storages}) {
f25852c2 4187 push @{$res->{allowed_nodes}}, $node;
f25852c2 4188 }
71fc647f 4189
f25852c2
TM
4190 }
4191 $res->{not_allowed_nodes} = $checked_nodes;
71fc647f
TM
4192 }
4193
4194
4195 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
4196 $res->{local_disks} = [ values %$local_disks ];;
4197
4198 my $local_resources = PVE::QemuServer::check_local_resources($vmconf, 1);
4199
4200 $res->{local_resources} = $local_resources;
4201
4202 return $res;
4203
4204
4205 }});
4206
3ea94c60 4207__PACKAGE__->register_method({
afdb31d5 4208 name => 'migrate_vm',
3ea94c60
DM
4209 path => '{vmid}/migrate',
4210 method => 'POST',
4211 protected => 1,
4212 proxyto => 'node',
4213 description => "Migrate virtual machine. Creates a new migration task.",
a0d1b1a2
DM
4214 permissions => {
4215 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4216 },
3ea94c60 4217 parameters => {
3326ae19 4218 additionalProperties => 0,
3ea94c60
DM
4219 properties => {
4220 node => get_standard_option('pve-node'),
335af808 4221 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
c2ed338e 4222 target => get_standard_option('pve-node', {
335af808
DM
4223 description => "Target node.",
4224 completion => \&PVE::Cluster::complete_migration_target,
4225 }),
3ea94c60
DM
4226 online => {
4227 type => 'boolean',
13739386 4228 description => "Use online/live migration if VM is running. Ignored if VM is stopped.",
3ea94c60
DM
4229 optional => 1,
4230 },
4231 force => {
4232 type => 'boolean',
4233 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
4234 optional => 1,
4235 },
2de2d6f7
TL
4236 migration_type => {
4237 type => 'string',
4238 enum => ['secure', 'insecure'],
c07a9e3d 4239 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
4240 optional => 1,
4241 },
4242 migration_network => {
c07a9e3d 4243 type => 'string', format => 'CIDR',
2de2d6f7
TL
4244 description => "CIDR of the (sub) network that is used for migration.",
4245 optional => 1,
4246 },
56af7146
AD
4247 "with-local-disks" => {
4248 type => 'boolean',
4249 description => "Enable live storage migration for local disk",
b74cad8a 4250 optional => 1,
56af7146 4251 },
bf8fc5a3 4252 targetstorage => get_standard_option('pve-targetstorage', {
255e9c54 4253 completion => \&PVE::QemuServer::complete_migration_storage,
56af7146 4254 }),
0aab5a16
SI
4255 bwlimit => {
4256 description => "Override I/O bandwidth limit (in KiB/s).",
4257 optional => 1,
4258 type => 'integer',
4259 minimum => '0',
41756a3b 4260 default => 'migrate limit from datacenter or storage config',
0aab5a16 4261 },
3ea94c60
DM
4262 },
4263 },
afdb31d5 4264 returns => {
3ea94c60
DM
4265 type => 'string',
4266 description => "the task ID.",
4267 },
4268 code => sub {
4269 my ($param) = @_;
4270
4271 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 4272 my $authuser = $rpcenv->get_user();
3ea94c60
DM
4273
4274 my $target = extract_param($param, 'target');
4275
4276 my $localnode = PVE::INotify::nodename();
4277 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
4278
4279 PVE::Cluster::check_cfs_quorum();
4280
4281 PVE::Cluster::check_node_exists($target);
4282
4283 my $targetip = PVE::Cluster::remote_node_ip($target);
4284
4285 my $vmid = extract_param($param, 'vmid');
4286
afdb31d5 4287 raise_param_exc({ force => "Only root may use this option." })
a0d1b1a2 4288 if $param->{force} && $authuser ne 'root@pam';
3ea94c60 4289
2de2d6f7
TL
4290 raise_param_exc({ migration_type => "Only root may use this option." })
4291 if $param->{migration_type} && $authuser ne 'root@pam';
4292
4293 # allow root only until better network permissions are available
4294 raise_param_exc({ migration_network => "Only root may use this option." })
4295 if $param->{migration_network} && $authuser ne 'root@pam';
4296
3ea94c60 4297 # test if VM exists
ffda963f 4298 my $conf = PVE::QemuConfig->load_config($vmid);
3ea94c60
DM
4299
4300 # try to detect errors early
a5ed42d3 4301
ffda963f 4302 PVE::QemuConfig->check_lock($conf);
a5ed42d3 4303
3ea94c60 4304 if (PVE::QemuServer::check_running($vmid)) {
fda72913 4305 die "can't migrate running VM without --online\n" if !$param->{online};
aa491a6e
FE
4306
4307 my $repl_conf = PVE::ReplicationConfig->new();
4308 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
4309 my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
68980d66
FE
4310 if (!$param->{force} && $is_replicated && !$is_replicated_to_target) {
4311 die "Cannot live-migrate replicated VM to node '$target' - not a replication " .
4312 "target. Use 'force' to override.\n";
aa491a6e 4313 }
13739386 4314 } else {
c3ddb94d 4315 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online};
13739386 4316 $param->{online} = 0;
3ea94c60
DM
4317 }
4318
47152e2e 4319 my $storecfg = PVE::Storage::config();
bf8fc5a3
FG
4320 if (my $targetstorage = $param->{targetstorage}) {
4321 my $storagemap = eval { PVE::JSONSchema::parse_idmap($targetstorage, 'pve-storage-id') };
e214cda8 4322 raise_param_exc({ targetstorage => "failed to parse storage map: $@" })
bf8fc5a3
FG
4323 if $@;
4324
aea447bb
FG
4325 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
4326 if !defined($storagemap->{identity});
4327
bd61033e 4328 foreach my $target_sid (values %{$storagemap->{entries}}) {
9fb295d0 4329 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
bf8fc5a3
FG
4330 }
4331
9fb295d0 4332 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
bf8fc5a3
FG
4333 if $storagemap->{default};
4334
4335 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target)
4336 if $storagemap->{identity};
4337
4338 $param->{storagemap} = $storagemap;
d80ad67f
AD
4339 } else {
4340 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target);
4341 }
47152e2e 4342
2003f0f8 4343 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
3ea94c60 4344
88fc87b4
DM
4345 my $hacmd = sub {
4346 my $upid = shift;
3ea94c60 4347
02765844 4348 print "Requesting HA migration for VM $vmid to node $target\n";
88fc87b4 4349
a4262553 4350 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
88fc87b4 4351 PVE::Tools::run_command($cmd);
88fc87b4
DM
4352 return;
4353 };
4354
4355 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
4356
4357 } else {
4358
f53c6ad8 4359 my $realcmd = sub {
f53c6ad8
DM
4360 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
4361 };
88fc87b4 4362
f53c6ad8
DM
4363 my $worker = sub {
4364 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
88fc87b4
DM
4365 };
4366
f53c6ad8 4367 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
88fc87b4 4368 }
3ea94c60 4369
3ea94c60 4370 }});
1e3baf05 4371
91c94f0a 4372__PACKAGE__->register_method({
afdb31d5
DM
4373 name => 'monitor',
4374 path => '{vmid}/monitor',
91c94f0a
DM
4375 method => 'POST',
4376 protected => 1,
4377 proxyto => 'node',
4378 description => "Execute Qemu monitor commands.",
a0d1b1a2 4379 permissions => {
a8f2f427 4380 description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
c07a9e3d 4381 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
a0d1b1a2 4382 },
91c94f0a 4383 parameters => {
3326ae19 4384 additionalProperties => 0,
91c94f0a
DM
4385 properties => {
4386 node => get_standard_option('pve-node'),
4387 vmid => get_standard_option('pve-vmid'),
4388 command => {
4389 type => 'string',
4390 description => "The monitor command.",
4391 }
4392 },
4393 },
4394 returns => { type => 'string'},
4395 code => sub {
4396 my ($param) = @_;
4397
a8f2f427
FG
4398 my $rpcenv = PVE::RPCEnvironment::get();
4399 my $authuser = $rpcenv->get_user();
4400
4401 my $is_ro = sub {
4402 my $command = shift;
4403 return $command =~ m/^\s*info(\s+|$)/
4404 || $command =~ m/^\s*help\s*$/;
4405 };
4406
4407 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4408 if !&$is_ro($param->{command});
4409
91c94f0a
DM
4410 my $vmid = $param->{vmid};
4411
ffda963f 4412 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
91c94f0a
DM
4413
4414 my $res = '';
4415 eval {
0a13e08e 4416 $res = PVE::QemuServer::Monitor::hmp_cmd($vmid, $param->{command});
91c94f0a
DM
4417 };
4418 $res = "ERROR: $@" if $@;
4419
4420 return $res;
4421 }});
4422
0d02881c
AD
4423__PACKAGE__->register_method({
4424 name => 'resize_vm',
614e3941 4425 path => '{vmid}/resize',
0d02881c
AD
4426 method => 'PUT',
4427 protected => 1,
4428 proxyto => 'node',
2f48a4f5 4429 description => "Extend volume size.",
0d02881c 4430 permissions => {
3b2773f6 4431 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
0d02881c
AD
4432 },
4433 parameters => {
3326ae19
TL
4434 additionalProperties => 0,
4435 properties => {
2f48a4f5 4436 node => get_standard_option('pve-node'),
335af808 4437 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2f48a4f5
DM
4438 skiplock => get_standard_option('skiplock'),
4439 disk => {
4440 type => 'string',
4441 description => "The disk you want to resize.",
e0fd2b2f 4442 enum => [PVE::QemuServer::Drive::valid_drive_names()],
2f48a4f5
DM
4443 },
4444 size => {
4445 type => 'string',
f91b2e45 4446 pattern => '\+?\d+(\.\d+)?[KMGT]?',
e248477e 4447 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
4448 },
4449 digest => {
4450 type => 'string',
4451 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4452 maxLength => 40,
4453 optional => 1,
4454 },
4455 },
0d02881c
AD
4456 },
4457 returns => { type => 'null'},
4458 code => sub {
4459 my ($param) = @_;
4460
4461 my $rpcenv = PVE::RPCEnvironment::get();
4462
4463 my $authuser = $rpcenv->get_user();
4464
4465 my $node = extract_param($param, 'node');
4466
4467 my $vmid = extract_param($param, 'vmid');
4468
4469 my $digest = extract_param($param, 'digest');
4470
2f48a4f5 4471 my $disk = extract_param($param, 'disk');
75466c4f 4472
2f48a4f5 4473 my $sizestr = extract_param($param, 'size');
0d02881c 4474
f91b2e45 4475 my $skiplock = extract_param($param, 'skiplock');
0d02881c
AD
4476 raise_param_exc({ skiplock => "Only root may use this option." })
4477 if $skiplock && $authuser ne 'root@pam';
4478
0d02881c
AD
4479 my $storecfg = PVE::Storage::config();
4480
0d02881c
AD
4481 my $updatefn = sub {
4482
ffda963f 4483 my $conf = PVE::QemuConfig->load_config($vmid);
0d02881c
AD
4484
4485 die "checksum missmatch (file change by other user?)\n"
4486 if $digest && $digest ne $conf->{digest};
ffda963f 4487 PVE::QemuConfig->check_lock($conf) if !$skiplock;
0d02881c 4488
f91b2e45
DM
4489 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4490
4491 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
4492
d662790a
WL
4493 my (undef, undef, undef, undef, undef, undef, $format) =
4494 PVE::Storage::parse_volname($storecfg, $drive->{file});
4495
c2ed338e 4496 die "can't resize volume: $disk if snapshot exists\n"
d662790a
WL
4497 if %{$conf->{snapshots}} && $format eq 'qcow2';
4498
f91b2e45
DM
4499 my $volid = $drive->{file};
4500
4501 die "disk '$disk' has no associated volume\n" if !$volid;
4502
4503 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
4504
4505 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
4506
4507 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4508
b572a606 4509 PVE::Storage::activate_volumes($storecfg, [$volid]);
f91b2e45
DM
4510 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
4511
ed94b2ad 4512 die "Could not determine current size of volume '$volid'\n" if !defined($size);
f8b829aa 4513
f91b2e45
DM
4514 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
4515 my ($ext, $newsize, $unit) = ($1, $2, $4);
4516 if ($unit) {
4517 if ($unit eq 'K') {
4518 $newsize = $newsize * 1024;
4519 } elsif ($unit eq 'M') {
4520 $newsize = $newsize * 1024 * 1024;
4521 } elsif ($unit eq 'G') {
4522 $newsize = $newsize * 1024 * 1024 * 1024;
4523 } elsif ($unit eq 'T') {
4524 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
4525 }
4526 }
4527 $newsize += $size if $ext;
4528 $newsize = int($newsize);
4529
9a478b17 4530 die "shrinking disks is not supported\n" if $newsize < $size;
f91b2e45
DM
4531
4532 return if $size == $newsize;
4533
2f48a4f5 4534 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
0d02881c 4535
f91b2e45 4536 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
75466c4f 4537
e29e5be6 4538 $drive->{size} = $newsize;
71c58bb7 4539 $conf->{$disk} = PVE::QemuServer::print_drive($drive);
f91b2e45 4540
ffda963f 4541 PVE::QemuConfig->write_config($vmid, $conf);
f91b2e45 4542 };
0d02881c 4543
ffda963f 4544 PVE::QemuConfig->lock_config($vmid, $updatefn);
d1c1af4b 4545 return;
0d02881c
AD
4546 }});
4547
9dbd1ee4 4548__PACKAGE__->register_method({
7e7d7b61 4549 name => 'snapshot_list',
9dbd1ee4 4550 path => '{vmid}/snapshot',
7e7d7b61
DM
4551 method => 'GET',
4552 description => "List all snapshots.",
4553 permissions => {
4554 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
4555 },
4556 proxyto => 'node',
4557 protected => 1, # qemu pid files are only readable by root
4558 parameters => {
3326ae19 4559 additionalProperties => 0,
7e7d7b61 4560 properties => {
e261de40 4561 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
7e7d7b61
DM
4562 node => get_standard_option('pve-node'),
4563 },
4564 },
4565 returns => {
4566 type => 'array',
4567 items => {
4568 type => "object",
ce9b0a38
DM
4569 properties => {
4570 name => {
4571 description => "Snapshot identifier. Value 'current' identifies the current VM.",
4572 type => 'string',
4573 },
4574 vmstate => {
4575 description => "Snapshot includes RAM.",
4576 type => 'boolean',
4577 optional => 1,
4578 },
4579 description => {
4580 description => "Snapshot description.",
4581 type => 'string',
4582 },
4583 snaptime => {
4584 description => "Snapshot creation time",
4585 type => 'integer',
4586 renderer => 'timestamp',
4587 optional => 1,
4588 },
4589 parent => {
4590 description => "Parent snapshot identifier.",
4591 type => 'string',
4592 optional => 1,
4593 },
4594 },
7e7d7b61
DM
4595 },
4596 links => [ { rel => 'child', href => "{name}" } ],
4597 },
4598 code => sub {
4599 my ($param) = @_;
4600
6aa4651b
DM
4601 my $vmid = $param->{vmid};
4602
ffda963f 4603 my $conf = PVE::QemuConfig->load_config($vmid);
7e7d7b61
DM
4604 my $snaphash = $conf->{snapshots} || {};
4605
4606 my $res = [];
4607
4608 foreach my $name (keys %$snaphash) {
0ea6bc69 4609 my $d = $snaphash->{$name};
75466c4f
DM
4610 my $item = {
4611 name => $name,
4612 snaptime => $d->{snaptime} || 0,
6aa4651b 4613 vmstate => $d->{vmstate} ? 1 : 0,
982c7f12
DM
4614 description => $d->{description} || '',
4615 };
0ea6bc69 4616 $item->{parent} = $d->{parent} if $d->{parent};
3ee28e38 4617 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
0ea6bc69
DM
4618 push @$res, $item;
4619 }
4620
6aa4651b 4621 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
ce9b0a38
DM
4622 my $current = {
4623 name => 'current',
4624 digest => $conf->{digest},
4625 running => $running,
4626 description => "You are here!",
4627 };
d1914468
DM
4628 $current->{parent} = $conf->{parent} if $conf->{parent};
4629
4630 push @$res, $current;
7e7d7b61
DM
4631
4632 return $res;
4633 }});
4634
4635__PACKAGE__->register_method({
4636 name => 'snapshot',
4637 path => '{vmid}/snapshot',
4638 method => 'POST',
9dbd1ee4
AD
4639 protected => 1,
4640 proxyto => 'node',
4641 description => "Snapshot a VM.",
4642 permissions => {
f1baf1df 4643 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
9dbd1ee4
AD
4644 },
4645 parameters => {
4646 additionalProperties => 0,
4647 properties => {
4648 node => get_standard_option('pve-node'),
335af808 4649 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 4650 snapname => get_standard_option('pve-snapshot-name'),
9dbd1ee4
AD
4651 vmstate => {
4652 optional => 1,
4653 type => 'boolean',
4654 description => "Save the vmstate",
4655 },
782f4f75
DM
4656 description => {
4657 optional => 1,
4658 type => 'string',
4659 description => "A textual description or comment.",
4660 },
9dbd1ee4
AD
4661 },
4662 },
7e7d7b61
DM
4663 returns => {
4664 type => 'string',
4665 description => "the task ID.",
4666 },
9dbd1ee4
AD
4667 code => sub {
4668 my ($param) = @_;
4669
4670 my $rpcenv = PVE::RPCEnvironment::get();
4671
4672 my $authuser = $rpcenv->get_user();
4673
4674 my $node = extract_param($param, 'node');
4675
4676 my $vmid = extract_param($param, 'vmid');
4677
9dbd1ee4
AD
4678 my $snapname = extract_param($param, 'snapname');
4679
d1914468
DM
4680 die "unable to use snapshot name 'current' (reserved name)\n"
4681 if $snapname eq 'current';
4682
a85c6be1
FG
4683 die "unable to use snapshot name 'pending' (reserved name)\n"
4684 if lc($snapname) eq 'pending';
4685
7e7d7b61 4686 my $realcmd = sub {
22c377f0 4687 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
c2ed338e 4688 PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate},
af9110dd 4689 $param->{description});
7e7d7b61
DM
4690 };
4691
4692 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
4693 }});
4694
154ccdcd
DM
4695__PACKAGE__->register_method({
4696 name => 'snapshot_cmd_idx',
4697 path => '{vmid}/snapshot/{snapname}',
4698 description => '',
4699 method => 'GET',
4700 permissions => {
4701 user => 'all',
4702 },
4703 parameters => {
3326ae19 4704 additionalProperties => 0,
154ccdcd
DM
4705 properties => {
4706 vmid => get_standard_option('pve-vmid'),
4707 node => get_standard_option('pve-node'),
8abd398b 4708 snapname => get_standard_option('pve-snapshot-name'),
154ccdcd
DM
4709 },
4710 },
4711 returns => {
4712 type => 'array',
4713 items => {
4714 type => "object",
4715 properties => {},
4716 },
4717 links => [ { rel => 'child', href => "{cmd}" } ],
4718 },
4719 code => sub {
4720 my ($param) = @_;
4721
4722 my $res = [];
4723
4724 push @$res, { cmd => 'rollback' };
d788cea6 4725 push @$res, { cmd => 'config' };
154ccdcd
DM
4726
4727 return $res;
4728 }});
4729
d788cea6
DM
4730__PACKAGE__->register_method({
4731 name => 'update_snapshot_config',
4732 path => '{vmid}/snapshot/{snapname}/config',
4733 method => 'PUT',
4734 protected => 1,
4735 proxyto => 'node',
4736 description => "Update snapshot metadata.",
4737 permissions => {
4738 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
4739 },
4740 parameters => {
4741 additionalProperties => 0,
4742 properties => {
4743 node => get_standard_option('pve-node'),
4744 vmid => get_standard_option('pve-vmid'),
4745 snapname => get_standard_option('pve-snapshot-name'),
4746 description => {
4747 optional => 1,
4748 type => 'string',
4749 description => "A textual description or comment.",
4750 },
4751 },
4752 },
4753 returns => { type => 'null' },
4754 code => sub {
4755 my ($param) = @_;
4756
4757 my $rpcenv = PVE::RPCEnvironment::get();
4758
4759 my $authuser = $rpcenv->get_user();
4760
4761 my $vmid = extract_param($param, 'vmid');
4762
4763 my $snapname = extract_param($param, 'snapname');
4764
d1c1af4b 4765 return if !defined($param->{description});
d788cea6
DM
4766
4767 my $updatefn = sub {
4768
ffda963f 4769 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6 4770
ffda963f 4771 PVE::QemuConfig->check_lock($conf);
d788cea6
DM
4772
4773 my $snap = $conf->{snapshots}->{$snapname};
4774
75466c4f
DM
4775 die "snapshot '$snapname' does not exist\n" if !defined($snap);
4776
d788cea6
DM
4777 $snap->{description} = $param->{description} if defined($param->{description});
4778
ffda963f 4779 PVE::QemuConfig->write_config($vmid, $conf);
d788cea6
DM
4780 };
4781
ffda963f 4782 PVE::QemuConfig->lock_config($vmid, $updatefn);
d788cea6 4783
d1c1af4b 4784 return;
d788cea6
DM
4785 }});
4786
4787__PACKAGE__->register_method({
4788 name => 'get_snapshot_config',
4789 path => '{vmid}/snapshot/{snapname}/config',
4790 method => 'GET',
4791 proxyto => 'node',
4792 description => "Get snapshot configuration",
4793 permissions => {
65204e92 4794 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any => 1],
d788cea6
DM
4795 },
4796 parameters => {
4797 additionalProperties => 0,
4798 properties => {
4799 node => get_standard_option('pve-node'),
4800 vmid => get_standard_option('pve-vmid'),
4801 snapname => get_standard_option('pve-snapshot-name'),
4802 },
4803 },
4804 returns => { type => "object" },
4805 code => sub {
4806 my ($param) = @_;
4807
4808 my $rpcenv = PVE::RPCEnvironment::get();
4809
4810 my $authuser = $rpcenv->get_user();
4811
4812 my $vmid = extract_param($param, 'vmid');
4813
4814 my $snapname = extract_param($param, 'snapname');
4815
ffda963f 4816 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6
DM
4817
4818 my $snap = $conf->{snapshots}->{$snapname};
4819
75466c4f
DM
4820 die "snapshot '$snapname' does not exist\n" if !defined($snap);
4821
d788cea6
DM
4822 return $snap;
4823 }});
4824
7e7d7b61
DM
4825__PACKAGE__->register_method({
4826 name => 'rollback',
154ccdcd 4827 path => '{vmid}/snapshot/{snapname}/rollback',
7e7d7b61
DM
4828 method => 'POST',
4829 protected => 1,
4830 proxyto => 'node',
4831 description => "Rollback VM state to specified snapshot.",
4832 permissions => {
c268337d 4833 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
7e7d7b61
DM
4834 },
4835 parameters => {
4836 additionalProperties => 0,
4837 properties => {
4838 node => get_standard_option('pve-node'),
335af808 4839 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 4840 snapname => get_standard_option('pve-snapshot-name'),
7e7d7b61
DM
4841 },
4842 },
4843 returns => {
4844 type => 'string',
4845 description => "the task ID.",
4846 },
4847 code => sub {
4848 my ($param) = @_;
4849
4850 my $rpcenv = PVE::RPCEnvironment::get();
4851
4852 my $authuser = $rpcenv->get_user();
4853
4854 my $node = extract_param($param, 'node');
4855
4856 my $vmid = extract_param($param, 'vmid');
4857
4858 my $snapname = extract_param($param, 'snapname');
4859
7e7d7b61 4860 my $realcmd = sub {
22c377f0 4861 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
b2c9558d 4862 PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
7e7d7b61
DM
4863 };
4864
c068c1c3
WL
4865 my $worker = sub {
4866 # hold migration lock, this makes sure that nobody create replication snapshots
4867 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
4868 };
4869
4870 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
7e7d7b61
DM
4871 }});
4872
4873__PACKAGE__->register_method({
4874 name => 'delsnapshot',
4875 path => '{vmid}/snapshot/{snapname}',
4876 method => 'DELETE',
4877 protected => 1,
4878 proxyto => 'node',
4879 description => "Delete a VM snapshot.",
4880 permissions => {
f1baf1df 4881 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
7e7d7b61
DM
4882 },
4883 parameters => {
4884 additionalProperties => 0,
4885 properties => {
4886 node => get_standard_option('pve-node'),
335af808 4887 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 4888 snapname => get_standard_option('pve-snapshot-name'),
3ee28e38
DM
4889 force => {
4890 optional => 1,
4891 type => 'boolean',
4892 description => "For removal from config file, even if removing disk snapshots fails.",
4893 },
7e7d7b61
DM
4894 },
4895 },
4896 returns => {
4897 type => 'string',
4898 description => "the task ID.",
4899 },
4900 code => sub {
4901 my ($param) = @_;
4902
4903 my $rpcenv = PVE::RPCEnvironment::get();
4904
4905 my $authuser = $rpcenv->get_user();
4906
4907 my $node = extract_param($param, 'node');
4908
4909 my $vmid = extract_param($param, 'vmid');
4910
4911 my $snapname = extract_param($param, 'snapname');
4912
1770b70f 4913 my $lock_obtained;
fdbbed2f 4914 my $do_delete = sub {
1770b70f 4915 $lock_obtained = 1;
22c377f0 4916 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
b2c9558d 4917 PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force});
7e7d7b61 4918 };
9dbd1ee4 4919
fdbbed2f
FE
4920 my $realcmd = sub {
4921 if ($param->{force}) {
4922 $do_delete->();
4923 } else {
1770b70f
FG
4924 eval { PVE::GuestHelpers::guest_migration_lock($vmid, 10, $do_delete); };
4925 if (my $err = $@) {
4926 die $err if $lock_obtained;
4927 die "Failed to obtain guest migration lock - replication running?\n";
4928 }
fdbbed2f
FE
4929 }
4930 };
4931
7b2257a8 4932 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
9dbd1ee4
AD
4933 }});
4934
04a69bb4
AD
4935__PACKAGE__->register_method({
4936 name => 'template',
4937 path => '{vmid}/template',
4938 method => 'POST',
4939 protected => 1,
4940 proxyto => 'node',
4941 description => "Create a Template.",
b02691d8 4942 permissions => {
7af0a6c8
DM
4943 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
4944 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
b02691d8 4945 },
04a69bb4
AD
4946 parameters => {
4947 additionalProperties => 0,
4948 properties => {
4949 node => get_standard_option('pve-node'),
335af808 4950 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
04a69bb4
AD
4951 disk => {
4952 optional => 1,
4953 type => 'string',
4954 description => "If you want to convert only 1 disk to base image.",
e0fd2b2f 4955 enum => [PVE::QemuServer::Drive::valid_drive_names()],
04a69bb4
AD
4956 },
4957
4958 },
4959 },
b297918c
FG
4960 returns => {
4961 type => 'string',
4962 description => "the task ID.",
4963 },
04a69bb4
AD
4964 code => sub {
4965 my ($param) = @_;
4966
4967 my $rpcenv = PVE::RPCEnvironment::get();
4968
4969 my $authuser = $rpcenv->get_user();
4970
4971 my $node = extract_param($param, 'node');
4972
4973 my $vmid = extract_param($param, 'vmid');
4974
4975 my $disk = extract_param($param, 'disk');
4976
d2ceac56 4977 my $load_and_check = sub {
ffda963f 4978 my $conf = PVE::QemuConfig->load_config($vmid);
04a69bb4 4979
ffda963f 4980 PVE::QemuConfig->check_lock($conf);
04a69bb4 4981
75466c4f 4982 die "unable to create template, because VM contains snapshots\n"
b91c2aae 4983 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
0402a80b 4984
75466c4f 4985 die "you can't convert a template to a template\n"
ffda963f 4986 if PVE::QemuConfig->is_template($conf) && !$disk;
0402a80b 4987
75466c4f 4988 die "you can't convert a VM to template if VM is running\n"
218cab9a 4989 if PVE::QemuServer::check_running($vmid);
35c5fdef 4990
d2ceac56
FG
4991 return $conf;
4992 };
04a69bb4 4993
d2ceac56 4994 $load_and_check->();
75e7e997 4995
d2ceac56
FG
4996 my $realcmd = sub {
4997 PVE::QemuConfig->lock_config($vmid, sub {
4998 my $conf = $load_and_check->();
4999
5000 $conf->{template} = 1;
5001 PVE::QemuConfig->write_config($vmid, $conf);
5002
5003 PVE::QemuServer::template_create($vmid, $conf, $disk);
5004 });
04a69bb4
AD
5005 };
5006
d2ceac56 5007 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
04a69bb4
AD
5008 }});
5009
73709749
ML
5010__PACKAGE__->register_method({
5011 name => 'cloudinit_generated_config_dump',
5012 path => '{vmid}/cloudinit/dump',
5013 method => 'GET',
5014 proxyto => 'node',
5015 description => "Get automatically generated cloudinit config.",
5016 permissions => {
5017 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5018 },
5019 parameters => {
5020 additionalProperties => 0,
5021 properties => {
5022 node => get_standard_option('pve-node'),
5023 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
5024 type => {
5025 description => 'Config type.',
5026 type => 'string',
5027 enum => ['user', 'network', 'meta'],
5028 },
5029 },
5030 },
5031 returns => {
5032 type => 'string',
5033 },
5034 code => sub {
5035 my ($param) = @_;
5036
5037 my $conf = PVE::QemuConfig->load_config($param->{vmid});
5038
5039 return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, $param->{vmid}, $param->{type});
5040 }});
5041
1e3baf05 50421;