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