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