]> git.proxmox.com Git - qemu-server.git/blame - PVE/API2/Qemu.pm
d/control: bump versioned dependencies for libpve-guest-common-perl
[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',
346130b2 3020 description => "Stop virtual machine. The qemu process will exit immediately. This" .
d6c747ff 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' => {
3046 description => "Abort any active and visible 'qmshutdown' tasks before stopping",
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',
d6c747ff
EK
3176 description => "Shutdown virtual machine. This is similar to pressing the power button on a physical machine." .
3177 "This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown.",
a0d1b1a2
DM
3178 permissions => {
3179 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3180 },
5fdbe4f0 3181 parameters => {
3326ae19 3182 additionalProperties => 0,
5fdbe4f0
DM
3183 properties => {
3184 node => get_standard_option('pve-node'),
ab5904f7
TL
3185 vmid => get_standard_option('pve-vmid',
3186 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 3187 skiplock => get_standard_option('skiplock'),
c6bb9502
DM
3188 timeout => {
3189 description => "Wait maximal timeout seconds.",
3190 type => 'integer',
3191 minimum => 0,
3192 optional => 1,
9269013a
DM
3193 },
3194 forceStop => {
3195 description => "Make sure the VM stops.",
3196 type => 'boolean',
3197 optional => 1,
3198 default => 0,
254575e9
DM
3199 },
3200 keepActive => {
94a17e1d 3201 description => "Do not deactivate storage volumes.",
254575e9
DM
3202 type => 'boolean',
3203 optional => 1,
3204 default => 0,
c6bb9502 3205 }
5fdbe4f0
DM
3206 },
3207 },
afdb31d5 3208 returns => {
5fdbe4f0
DM
3209 type => 'string',
3210 },
3211 code => sub {
3212 my ($param) = @_;
3213
3214 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 3215 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
3216
3217 my $node = extract_param($param, 'node');
5fdbe4f0
DM
3218 my $vmid = extract_param($param, 'vmid');
3219
3220 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 3221 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 3222 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 3223
254575e9 3224 my $keepActive = extract_param($param, 'keepActive');
afdb31d5 3225 raise_param_exc({ keepActive => "Only root may use this option." })
a0d1b1a2 3226 if $keepActive && $authuser ne 'root@pam';
254575e9 3227
02d07cf5
DM
3228 my $storecfg = PVE::Storage::config();
3229
89897367
DC
3230 my $shutdown = 1;
3231
263c803d
TL
3232 # sending a graceful shutdown command to paused VMs runs into timeouts, and even worse, when
3233 # the VM gets resumed later, it still gets the request delivered and powers off
6f0627d4 3234 if (PVE::QemuServer::vm_is_paused($vmid, 1)) {
89897367
DC
3235 if ($param->{forceStop}) {
3236 warn "VM is paused - stop instead of shutdown\n";
3237 $shutdown = 0;
3238 } else {
3239 die "VM is paused - cannot shutdown\n";
3240 }
3241 }
3242
a4262553 3243 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
5fdbe4f0 3244
1805fac3 3245 my $timeout = $param->{timeout} // 60;
ae849692
DM
3246 my $hacmd = sub {
3247 my $upid = shift;
5fdbe4f0 3248
02765844 3249 print "Requesting HA stop for VM $vmid\n";
ae849692 3250
1805fac3 3251 my $cmd = ['ha-manager', 'crm-command', 'stop', "vm:$vmid", "$timeout"];
ae849692 3252 PVE::Tools::run_command($cmd);
ae849692
DM
3253 return;
3254 };
3255
3256 return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
3257
3258 } else {
3259
3260 my $realcmd = sub {
3261 my $upid = shift;
3262
3263 syslog('info', "shutdown VM $vmid: $upid\n");
5fdbe4f0 3264
ae849692
DM
3265 PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout},
3266 $shutdown, $param->{forceStop}, $keepActive);
ae849692
DM
3267 return;
3268 };
3269
3270 return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd);
3271 }
5fdbe4f0
DM
3272 }});
3273
165411f0
DC
3274__PACKAGE__->register_method({
3275 name => 'vm_reboot',
3276 path => '{vmid}/status/reboot',
3277 method => 'POST',
3278 protected => 1,
3279 proxyto => 'node',
3280 description => "Reboot the VM by shutting it down, and starting it again. Applies pending changes.",
3281 permissions => {
3282 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3283 },
3284 parameters => {
3285 additionalProperties => 0,
3286 properties => {
3287 node => get_standard_option('pve-node'),
3288 vmid => get_standard_option('pve-vmid',
3289 { completion => \&PVE::QemuServer::complete_vmid_running }),
3290 timeout => {
3291 description => "Wait maximal timeout seconds for the shutdown.",
3292 type => 'integer',
3293 minimum => 0,
3294 optional => 1,
3295 },
3296 },
3297 },
3298 returns => {
3299 type => 'string',
3300 },
3301 code => sub {
3302 my ($param) = @_;
3303
3304 my $rpcenv = PVE::RPCEnvironment::get();
3305 my $authuser = $rpcenv->get_user();
3306
3307 my $node = extract_param($param, 'node');
3308 my $vmid = extract_param($param, 'vmid');
3309
6f0627d4 3310 die "VM is paused - cannot shutdown\n" if PVE::QemuServer::vm_is_paused($vmid, 1);
165411f0
DC
3311
3312 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
3313
3314 my $realcmd = sub {
3315 my $upid = shift;
3316
3317 syslog('info', "requesting reboot of VM $vmid: $upid\n");
3318 PVE::QemuServer::vm_reboot($vmid, $param->{timeout});
3319 return;
3320 };
3321
3322 return $rpcenv->fork_worker('qmreboot', $vmid, $authuser, $realcmd);
3323 }});
3324
5fdbe4f0 3325__PACKAGE__->register_method({
afdb31d5 3326 name => 'vm_suspend',
5fdbe4f0
DM
3327 path => '{vmid}/status/suspend',
3328 method => 'POST',
3329 protected => 1,
3330 proxyto => 'node',
3331 description => "Suspend virtual machine.",
a0d1b1a2 3332 permissions => {
75c24bba
DC
3333 description => "You need 'VM.PowerMgmt' on /vms/{vmid}, and if you have set 'todisk',".
3334 " you need also 'VM.Config.Disk' on /vms/{vmid} and 'Datastore.AllocateSpace'".
3335 " on the storage for the vmstate.",
a0d1b1a2
DM
3336 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3337 },
5fdbe4f0 3338 parameters => {
3326ae19 3339 additionalProperties => 0,
5fdbe4f0
DM
3340 properties => {
3341 node => get_standard_option('pve-node'),
ab5904f7
TL
3342 vmid => get_standard_option('pve-vmid',
3343 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 3344 skiplock => get_standard_option('skiplock'),
22371fe0
DC
3345 todisk => {
3346 type => 'boolean',
3347 default => 0,
3348 optional => 1,
3349 description => 'If set, suspends the VM to disk. Will be resumed on next VM start.',
3350 },
48b4cdc2
DC
3351 statestorage => get_standard_option('pve-storage-id', {
3352 description => "The storage for the VM state",
3353 requires => 'todisk',
3354 optional => 1,
3355 completion => \&PVE::Storage::complete_storage_enabled,
3356 }),
5fdbe4f0
DM
3357 },
3358 },
afdb31d5 3359 returns => {
5fdbe4f0
DM
3360 type => 'string',
3361 },
3362 code => sub {
3363 my ($param) = @_;
3364
3365 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 3366 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
3367
3368 my $node = extract_param($param, 'node');
5fdbe4f0
DM
3369 my $vmid = extract_param($param, 'vmid');
3370
22371fe0
DC
3371 my $todisk = extract_param($param, 'todisk') // 0;
3372
48b4cdc2
DC
3373 my $statestorage = extract_param($param, 'statestorage');
3374
5fdbe4f0 3375 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 3376 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 3377 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 3378
ff1a2432
DM
3379 die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
3380
22371fe0
DC
3381 die "Cannot suspend HA managed VM to disk\n"
3382 if $todisk && PVE::HA::Config::vm_is_ha_managed($vmid);
3383
75c24bba
DC
3384 # early check for storage permission, for better user feedback
3385 if ($todisk) {
3386 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']);
227a298f
DC
3387 my $conf = PVE::QemuConfig->load_config($vmid);
3388
876b24f2 3389 # cannot save the state of a non-virtualized PCIe device, so resume cannot really work
227a298f
DC
3390 for my $key (keys %$conf) {
3391 next if $key !~ /^hostpci\d+/;
161c2dde
TL
3392 die "cannot suspend VM to disk due to passed-through PCI device(s), which lack the"
3393 ." possibility to save/restore their internal state\n";
227a298f 3394 }
75c24bba
DC
3395
3396 if (!$statestorage) {
3397 # get statestorage from config if none is given
75c24bba
DC
3398 my $storecfg = PVE::Storage::config();
3399 $statestorage = PVE::QemuServer::find_vmstate_storage($conf, $storecfg);
3400 }
3401
3402 $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
3403 }
3404
5fdbe4f0
DM
3405 my $realcmd = sub {
3406 my $upid = shift;
3407
3408 syslog('info', "suspend VM $vmid: $upid\n");
3409
48b4cdc2 3410 PVE::QemuServer::vm_suspend($vmid, $skiplock, $todisk, $statestorage);
5fdbe4f0
DM
3411
3412 return;
3413 };
3414
a4262553 3415 my $taskname = $todisk ? 'qmsuspend' : 'qmpause';
f17fb184 3416 return $rpcenv->fork_worker($taskname, $vmid, $authuser, $realcmd);
5fdbe4f0
DM
3417 }});
3418
3419__PACKAGE__->register_method({
afdb31d5 3420 name => 'vm_resume',
5fdbe4f0
DM
3421 path => '{vmid}/status/resume',
3422 method => 'POST',
3423 protected => 1,
3424 proxyto => 'node',
3425 description => "Resume virtual machine.",
a0d1b1a2
DM
3426 permissions => {
3427 check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
3428 },
5fdbe4f0 3429 parameters => {
3326ae19 3430 additionalProperties => 0,
5fdbe4f0
DM
3431 properties => {
3432 node => get_standard_option('pve-node'),
ab5904f7
TL
3433 vmid => get_standard_option('pve-vmid',
3434 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0 3435 skiplock => get_standard_option('skiplock'),
289e0b85
AD
3436 nocheck => { type => 'boolean', optional => 1 },
3437
5fdbe4f0
DM
3438 },
3439 },
afdb31d5 3440 returns => {
5fdbe4f0
DM
3441 type => 'string',
3442 },
3443 code => sub {
3444 my ($param) = @_;
3445
3446 my $rpcenv = PVE::RPCEnvironment::get();
3447
a0d1b1a2 3448 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
3449
3450 my $node = extract_param($param, 'node');
3451
3452 my $vmid = extract_param($param, 'vmid');
3453
3454 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 3455 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 3456 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0 3457
a20dc58a
FG
3458 # nocheck is used as part of migration when config file might be still
3459 # be on source node
289e0b85 3460 my $nocheck = extract_param($param, 'nocheck');
4fb85adc
FG
3461 raise_param_exc({ nocheck => "Only root may use this option." })
3462 if $nocheck && $authuser ne 'root@pam';
289e0b85 3463
cd9a035b
TL
3464 my $to_disk_suspended;
3465 eval {
3466 PVE::QemuConfig->lock_config($vmid, sub {
3467 my $conf = PVE::QemuConfig->load_config($vmid);
3468 $to_disk_suspended = PVE::QemuConfig->has_lock($conf, 'suspended');
3469 });
3470 };
3471
3472 die "VM $vmid not running\n"
3473 if !$to_disk_suspended && !PVE::QemuServer::check_running($vmid, $nocheck);
ff1a2432 3474
5fdbe4f0
DM
3475 my $realcmd = sub {
3476 my $upid = shift;
3477
3478 syslog('info', "resume VM $vmid: $upid\n");
3479
cd9a035b
TL
3480 if (!$to_disk_suspended) {
3481 PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck);
3482 } else {
3483 my $storecfg = PVE::Storage::config();
0c498cca 3484 PVE::QemuServer::vm_start($storecfg, $vmid, { skiplock => $skiplock });
cd9a035b 3485 }
1e3baf05 3486
5fdbe4f0
DM
3487 return;
3488 };
3489
a0d1b1a2 3490 return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd);
5fdbe4f0
DM
3491 }});
3492
3493__PACKAGE__->register_method({
afdb31d5 3494 name => 'vm_sendkey',
5fdbe4f0
DM
3495 path => '{vmid}/sendkey',
3496 method => 'PUT',
3497 protected => 1,
3498 proxyto => 'node',
3499 description => "Send key event to virtual machine.",
a0d1b1a2
DM
3500 permissions => {
3501 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
3502 },
5fdbe4f0 3503 parameters => {
3326ae19 3504 additionalProperties => 0,
5fdbe4f0
DM
3505 properties => {
3506 node => get_standard_option('pve-node'),
ab5904f7
TL
3507 vmid => get_standard_option('pve-vmid',
3508 { completion => \&PVE::QemuServer::complete_vmid_running }),
5fdbe4f0
DM
3509 skiplock => get_standard_option('skiplock'),
3510 key => {
3511 description => "The key (qemu monitor encoding).",
3512 type => 'string'
3513 }
3514 },
3515 },
3516 returns => { type => 'null'},
3517 code => sub {
3518 my ($param) = @_;
3519
3520 my $rpcenv = PVE::RPCEnvironment::get();
3521
a0d1b1a2 3522 my $authuser = $rpcenv->get_user();
5fdbe4f0
DM
3523
3524 my $node = extract_param($param, 'node');
3525
3526 my $vmid = extract_param($param, 'vmid');
3527
3528 my $skiplock = extract_param($param, 'skiplock');
afdb31d5 3529 raise_param_exc({ skiplock => "Only root may use this option." })
a0d1b1a2 3530 if $skiplock && $authuser ne 'root@pam';
5fdbe4f0
DM
3531
3532 PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key});
3533
3534 return;
1e3baf05
DM
3535 }});
3536
1ac0d2ee
AD
3537__PACKAGE__->register_method({
3538 name => 'vm_feature',
3539 path => '{vmid}/feature',
3540 method => 'GET',
3541 proxyto => 'node',
75466c4f 3542 protected => 1,
1ac0d2ee
AD
3543 description => "Check if feature for virtual machine is available.",
3544 permissions => {
3545 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
3546 },
3547 parameters => {
3326ae19 3548 additionalProperties => 0,
1ac0d2ee
AD
3549 properties => {
3550 node => get_standard_option('pve-node'),
3551 vmid => get_standard_option('pve-vmid'),
3552 feature => {
3553 description => "Feature to check.",
3554 type => 'string',
7758ce86 3555 enum => [ 'snapshot', 'clone', 'copy' ],
1ac0d2ee
AD
3556 },
3557 snapname => get_standard_option('pve-snapshot-name', {
3558 optional => 1,
3559 }),
3560 },
1ac0d2ee
AD
3561 },
3562 returns => {
719893a9
DM
3563 type => "object",
3564 properties => {
3565 hasFeature => { type => 'boolean' },
7043d946 3566 nodes => {
719893a9
DM
3567 type => 'array',
3568 items => { type => 'string' },
3569 }
3570 },
1ac0d2ee
AD
3571 },
3572 code => sub {
3573 my ($param) = @_;
3574
3575 my $node = extract_param($param, 'node');
3576
3577 my $vmid = extract_param($param, 'vmid');
3578
3579 my $snapname = extract_param($param, 'snapname');
3580
3581 my $feature = extract_param($param, 'feature');
3582
3583 my $running = PVE::QemuServer::check_running($vmid);
3584
ffda963f 3585 my $conf = PVE::QemuConfig->load_config($vmid);
1ac0d2ee
AD
3586
3587 if($snapname){
3588 my $snap = $conf->{snapshots}->{$snapname};
3589 die "snapshot '$snapname' does not exist\n" if !defined($snap);
3590 $conf = $snap;
3591 }
3592 my $storecfg = PVE::Storage::config();
3593
719893a9 3594 my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg);
b2c9558d 3595 my $hasFeature = PVE::QemuConfig->has_feature($feature, $conf, $storecfg, $snapname, $running);
7043d946 3596
719893a9
DM
3597 return {
3598 hasFeature => $hasFeature,
3599 nodes => [ keys %$nodelist ],
7043d946 3600 };
1ac0d2ee
AD
3601 }});
3602
6116f729 3603__PACKAGE__->register_method({
9418baad
DM
3604 name => 'clone_vm',
3605 path => '{vmid}/clone',
6116f729
DM
3606 method => 'POST',
3607 protected => 1,
3608 proxyto => 'node',
37329185 3609 description => "Create a copy of virtual machine/template.",
6116f729 3610 permissions => {
9418baad 3611 description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
6116f729 3612 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
9cfd06d9 3613 "'Datastore.AllocateSpace' on any used storage and 'SDN.Use' on any used bridge/vnet",
75466c4f
DM
3614 check =>
3615 [ 'and',
9418baad 3616 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
75466c4f 3617 [ 'or',
6116f729
DM
3618 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
3619 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
3620 ],
3621 ]
3622 },
3623 parameters => {
3326ae19 3624 additionalProperties => 0,
6116f729 3625 properties => {
6116f729 3626 node => get_standard_option('pve-node'),
335af808 3627 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
1ae43f8c
DM
3628 newid => get_standard_option('pve-vmid', {
3629 completion => \&PVE::Cluster::complete_next_vmid,
3630 description => 'VMID for the clone.' }),
a60ab1a6
DM
3631 name => {
3632 optional => 1,
3633 type => 'string', format => 'dns-name',
3634 description => "Set a name for the new VM.",
3635 },
3636 description => {
3637 optional => 1,
3638 type => 'string',
3639 description => "Description for the new VM.",
3640 },
75466c4f 3641 pool => {
6116f729
DM
3642 optional => 1,
3643 type => 'string', format => 'pve-poolid',
3644 description => "Add the new VM to the specified pool.",
3645 },
9076d880 3646 snapname => get_standard_option('pve-snapshot-name', {
9076d880
DM
3647 optional => 1,
3648 }),
81f043eb 3649 storage => get_standard_option('pve-storage-id', {
9418baad 3650 description => "Target storage for full clone.",
81f043eb
AD
3651 optional => 1,
3652 }),
55173c6b 3653 'format' => {
fd13b1d0 3654 description => "Target format for file storage. Only valid for full clone.",
42a19c87
AD
3655 type => 'string',
3656 optional => 1,
55173c6b 3657 enum => [ 'raw', 'qcow2', 'vmdk'],
42a19c87 3658 },
6116f729
DM
3659 full => {
3660 optional => 1,
55173c6b 3661 type => 'boolean',
fd13b1d0 3662 description => "Create a full copy of all disks. This is always done when " .
9418baad 3663 "you clone a normal VM. For VM templates, we try to create a linked clone by default.",
6116f729 3664 },
75466c4f 3665 target => get_standard_option('pve-node', {
55173c6b
DM
3666 description => "Target node. Only allowed if the original VM is on shared storage.",
3667 optional => 1,
3668 }),
0aab5a16
SI
3669 bwlimit => {
3670 description => "Override I/O bandwidth limit (in KiB/s).",
3671 optional => 1,
3672 type => 'integer',
3673 minimum => '0',
41756a3b 3674 default => 'clone limit from datacenter or storage config',
0aab5a16 3675 },
55173c6b 3676 },
6116f729
DM
3677 },
3678 returns => {
3679 type => 'string',
3680 },
3681 code => sub {
3682 my ($param) = @_;
3683
3684 my $rpcenv = PVE::RPCEnvironment::get();
a85ff91b 3685 my $authuser = $rpcenv->get_user();
6116f729
DM
3686
3687 my $node = extract_param($param, 'node');
6116f729 3688 my $vmid = extract_param($param, 'vmid');
6116f729 3689 my $newid = extract_param($param, 'newid');
6116f729 3690 my $pool = extract_param($param, 'pool');
6116f729 3691
55173c6b 3692 my $snapname = extract_param($param, 'snapname');
81f043eb 3693 my $storage = extract_param($param, 'storage');
42a19c87 3694 my $format = extract_param($param, 'format');
55173c6b
DM
3695 my $target = extract_param($param, 'target');
3696
3697 my $localnode = PVE::INotify::nodename();
3698
e099bad4 3699 if ($target && ($target eq $localnode || $target eq 'localhost')) {
a85ff91b 3700 undef $target;
a85ff91b 3701 }
55173c6b 3702
4df8fe45 3703 my $running = PVE::QemuServer::check_running($vmid) || 0;
d069275f 3704
4df8fe45
FG
3705 my $load_and_check = sub {
3706 $rpcenv->check_pool_exist($pool) if defined($pool);
3707 PVE::Cluster::check_node_exists($target) if $target;
6116f729 3708
4df8fe45 3709 my $storecfg = PVE::Storage::config();
4a5a2590 3710
4df8fe45
FG
3711 if ($storage) {
3712 # check if storage is enabled on local node
3713 PVE::Storage::storage_check_enabled($storecfg, $storage);
3714 if ($target) {
3715 # check if storage is available on target node
3716 PVE::Storage::storage_check_enabled($storecfg, $storage, $target);
3717 # clone only works if target storage is shared
3718 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
3719 die "can't clone to non-shared storage '$storage'\n"
3720 if !$scfg->{shared};
3721 }
3722 }
6116f729 3723
4df8fe45 3724 PVE::Cluster::check_cfs_quorum();
4e4f83fe 3725
ffda963f 3726 my $conf = PVE::QemuConfig->load_config($vmid);
ffda963f 3727 PVE::QemuConfig->check_lock($conf);
6116f729 3728
4e4f83fe 3729 my $verify_running = PVE::QemuServer::check_running($vmid) || 0;
4e4f83fe 3730 die "unexpected state change\n" if $verify_running != $running;
6116f729 3731
75466c4f
DM
3732 die "snapshot '$snapname' does not exist\n"
3733 if $snapname && !defined( $conf->{snapshots}->{$snapname});
6116f729 3734
dbecb46f 3735 my $full = $param->{full} // !PVE::QemuConfig->is_template($conf);
fd13b1d0
DM
3736
3737 die "parameter 'storage' not allowed for linked clones\n"
3738 if defined($storage) && !$full;
3739
3740 die "parameter 'format' not allowed for linked clones\n"
3741 if defined($format) && !$full;
3742
75466c4f 3743 my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
9076d880 3744
9418baad 3745 my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
e3971865 3746 PVE::QemuServer::check_mapping_access($rpcenv, $authuser, $oldconf);
6116f729 3747
d6deb7f6 3748 PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $oldconf);
9cfd06d9 3749
a85ff91b
TL
3750 die "can't clone VM to node '$target' (VM uses local storage)\n"
3751 if $target && !$sharedvm;
75466c4f 3752
ffda963f 3753 my $conffile = PVE::QemuConfig->config_file($newid);
6116f729
DM
3754 die "unable to create VM $newid: config file already exists\n"
3755 if -f $conffile;
3756
9418baad 3757 my $newconf = { lock => 'clone' };
829967a9 3758 my $drives = {};
34456bf0 3759 my $fullclone = {};
829967a9
DM
3760 my $vollist = [];
3761
3762 foreach my $opt (keys %$oldconf) {
3763 my $value = $oldconf->{$opt};
3764
3765 # do not copy snapshot related info
3766 next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' ||
3767 $opt eq 'vmstate' || $opt eq 'snapstate';
3768
a78ea5df
WL
3769 # no need to copy unused images, because VMID(owner) changes anyways
3770 next if $opt =~ m/^unused\d+$/;
3771
e4a70a41
FE
3772 die "cannot clone TPM state while VM is running\n"
3773 if $full && $running && !$snapname && $opt eq 'tpmstate0';
3774
829967a9
DM
3775 # always change MAC! address
3776 if ($opt =~ m/^net(\d+)$/) {
3777 my $net = PVE::QemuServer::parse_net($value);
b5b99790
WB
3778 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
3779 $net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
829967a9 3780 $newconf->{$opt} = PVE::QemuServer::print_net($net);
74479ee9 3781 } elsif (PVE::QemuServer::is_valid_drivename($opt)) {
1f1412d1
DM
3782 my $drive = PVE::QemuServer::parse_drive($opt, $value);
3783 die "unable to parse drive options for '$opt'\n" if !$drive;
7fe8b44c 3784 if (PVE::QemuServer::drive_is_cdrom($drive, 1)) {
829967a9
DM
3785 $newconf->{$opt} = $value; # simply copy configuration
3786 } else {
7fe8b44c 3787 if ($full || PVE::QemuServer::drive_is_cloudinit($drive)) {
6318daca 3788 die "Full clone feature is not supported for drive '$opt'\n"
dba198b0 3789 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running);
34456bf0 3790 $fullclone->{$opt} = 1;
64ff6fe4
SP
3791 } else {
3792 # not full means clone instead of copy
6318daca 3793 die "Linked clone feature is not supported for drive '$opt'\n"
64ff6fe4 3794 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $drive->{file}, $snapname, $running);
dba198b0 3795 }
829967a9 3796 $drives->{$opt} = $drive;
f8c4b2c5 3797 next if PVE::QemuServer::drive_is_cloudinit($drive);
829967a9
DM
3798 push @$vollist, $drive->{file};
3799 }
3800 } else {
3801 # copy everything else
3802 $newconf->{$opt} = $value;
3803 }
3804 }
3805
dbecb46f
FE
3806 return ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone);
3807 };
3808
3809 my $clonefn = sub {
3810 my ($conffile, $newconf, $oldconf, $vollist, $drives, $fullclone) = $load_and_check->();
4df8fe45 3811 my $storecfg = PVE::Storage::config();
dbecb46f
FE
3812
3813 # auto generate a new uuid
cd11416f 3814 my $smbios1 = PVE::QemuServer::parse_smbios1($newconf->{smbios1} || '');
6ee499ff 3815 $smbios1->{uuid} = PVE::QemuServer::generate_uuid();
cd11416f 3816 $newconf->{smbios1} = PVE::QemuServer::print_smbios1($smbios1);
a85ff91b 3817 # auto generate a new vmgenid only if the option was set for template
6ee499ff
DC
3818 if ($newconf->{vmgenid}) {
3819 $newconf->{vmgenid} = PVE::QemuServer::generate_uuid();
3820 }
3821
829967a9
DM
3822 delete $newconf->{template};
3823
3824 if ($param->{name}) {
3825 $newconf->{name} = $param->{name};
3826 } else {
a85ff91b 3827 $newconf->{name} = "Copy-of-VM-" . ($oldconf->{name} // $vmid);
829967a9 3828 }
2dd53043 3829
829967a9
DM
3830 if ($param->{description}) {
3831 $newconf->{description} = $param->{description};
3832 }
3833
6116f729 3834 # create empty/temp config - this fails if VM already exists on other node
a85ff91b 3835 # FIXME use PVE::QemuConfig->create_and_lock_config and adapt code
9418baad 3836 PVE::Tools::file_set_contents($conffile, "# qmclone temporary file\nlock: clone\n");
6116f729 3837
dbecb46f 3838 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
68e46b84 3839
dbecb46f
FE
3840 my $newvollist = [];
3841 my $jobs = {};
3842
3843 eval {
3844 local $SIG{INT} =
3845 local $SIG{TERM} =
3846 local $SIG{QUIT} =
3847 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
3848
3849 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
3850
3851 my $bwlimit = extract_param($param, 'bwlimit');
3852
3853 my $total_jobs = scalar(keys %{$drives});
3854 my $i = 1;
3855
3856 foreach my $opt (sort keys %$drives) {
3857 my $drive = $drives->{$opt};
3858 my $skipcomplete = ($total_jobs != $i); # finish after last drive
3859 my $completion = $skipcomplete ? 'skip' : 'complete';
3860
3861 my $src_sid = PVE::Storage::parse_volume_id($drive->{file});
3862 my $storage_list = [ $src_sid ];
3863 push @$storage_list, $storage if defined($storage);
3864 my $clonelimit = PVE::Storage::get_bandwidth_limit('clone', $storage_list, $bwlimit);
3865
1196086f
FE
3866 my $source_info = {
3867 vmid => $vmid,
3868 running => $running,
3869 drivename => $opt,
3870 drive => $drive,
3871 snapname => $snapname,
3872 };
3873
3874 my $dest_info = {
3875 vmid => $newid,
25166060 3876 drivename => $opt,
1196086f
FE
3877 storage => $storage,
3878 format => $format,
3879 };
3880
7344af7b
FE
3881 $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($oldconf)
3882 if $opt eq 'efidisk0';
3883
dbecb46f
FE
3884 my $newdrive = PVE::QemuServer::clone_disk(
3885 $storecfg,
1196086f
FE
3886 $source_info,
3887 $dest_info,
dbecb46f
FE
3888 $fullclone->{$opt},
3889 $newvollist,
3890 $jobs,
3891 $completion,
3892 $oldconf->{agent},
3893 $clonelimit,
dbecb46f
FE
3894 );
3895
3896 $newconf->{$opt} = PVE::QemuServer::print_drive($newdrive);
68e46b84 3897
ffda963f 3898 PVE::QemuConfig->write_config($newid, $newconf);
dbecb46f
FE
3899 $i++;
3900 }
55173c6b 3901
dbecb46f 3902 delete $newconf->{lock};
baca276d 3903
dbecb46f
FE
3904 # do not write pending changes
3905 if (my @changes = keys %{$newconf->{pending}}) {
3906 my $pending = join(',', @changes);
3907 warn "found pending changes for '$pending', discarding for clone\n";
3908 delete $newconf->{pending};
3909 }
d703d4c0 3910
dbecb46f 3911 PVE::QemuConfig->write_config($newid, $newconf);
b83e0181 3912
ee8d2dea
AD
3913 PVE::QemuServer::create_ifaces_ipams_ips($newconf, $newid);
3914
dbecb46f 3915 if ($target) {
04736ecb
TL
3916 if (!$running) {
3917 # always deactivate volumes – avoids that LVM LVs are active on several nodes
3918 eval { PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) };
3919 # but only warn when that fails (e.g., parallel clones keeping them active)
3920 log_warn($@) if $@;
3921 }
9d6126e8 3922
dbecb46f 3923 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
c05c90a1 3924
dbecb46f
FE
3925 my $newconffile = PVE::QemuConfig->config_file($newid, $target);
3926 die "Failed to move config to node '$target' - rename failed: $!\n"
3927 if !rename($conffile, $newconffile);
3928 }
c05c90a1 3929
dbecb46f
FE
3930 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
3931 };
3932 if (my $err = $@) {
3933 eval { PVE::QemuServer::qemu_blockjobs_cancel($vmid, $jobs) };
3934 sleep 1; # some storage like rbd need to wait before release volume - really?
990b65ab 3935
dbecb46f
FE
3936 foreach my $volid (@$newvollist) {
3937 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
3938 warn $@ if $@;
6116f729
DM
3939 }
3940
dbecb46f 3941 PVE::Firewall::remove_vmfw_conf($newid);
6116f729 3942
dbecb46f
FE
3943 unlink $conffile; # avoid races -> last thing before die
3944
3945 die "clone failed: $err";
3946 }
457010cc 3947
dbecb46f 3948 return;
6116f729
DM
3949 };
3950
45fd77bb
FG
3951 # Aquire exclusive lock lock for $newid
3952 my $lock_target_vm = sub {
ffda963f 3953 return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn);
45fd77bb 3954 };
6116f729 3955
dbecb46f
FE
3956 my $lock_source_vm = sub {
3957 # exclusive lock if VM is running - else shared lock is enough;
3958 if ($running) {
3959 return PVE::QemuConfig->lock_config_full($vmid, 1, $lock_target_vm);
3960 } else {
3961 return PVE::QemuConfig->lock_config_shared($vmid, 1, $lock_target_vm);
3962 }
3963 };
3964
3965 $load_and_check->(); # early checks before forking/locking
3966
3967 return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $lock_source_vm);
6116f729
DM
3968 }});
3969
586bfa78 3970__PACKAGE__->register_method({
43bc02a9
DM
3971 name => 'move_vm_disk',
3972 path => '{vmid}/move_disk',
e2cd75fa 3973 method => 'POST',
586bfa78
AD
3974 protected => 1,
3975 proxyto => 'node',
a9453218 3976 description => "Move volume to different storage or to a different VM.",
586bfa78 3977 permissions => {
a9453218
AL
3978 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
3979 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
3980 "a disk to another VM, you need the permissions on the target VM as well.",
44102492 3981 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
586bfa78
AD
3982 },
3983 parameters => {
3326ae19 3984 additionalProperties => 0,
c07a9e3d 3985 properties => {
586bfa78 3986 node => get_standard_option('pve-node'),
335af808 3987 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
a9453218
AL
3988 'target-vmid' => get_standard_option('pve-vmid', {
3989 completion => \&PVE::QemuServer::complete_vmid,
3990 optional => 1,
3991 }),
586bfa78
AD
3992 disk => {
3993 type => 'string',
3994 description => "The disk you want to move.",
a9453218 3995 enum => [PVE::QemuServer::Drive::valid_drive_names_with_unused()],
586bfa78 3996 },
335af808
DM
3997 storage => get_standard_option('pve-storage-id', {
3998 description => "Target storage.",
3999 completion => \&PVE::QemuServer::complete_storage,
a9453218 4000 optional => 1,
335af808 4001 }),
f519ab0b
TL
4002 'format' => {
4003 type => 'string',
4004 description => "Target Format.",
4005 enum => [ 'raw', 'qcow2', 'vmdk' ],
4006 optional => 1,
4007 },
70d45e33
DM
4008 delete => {
4009 type => 'boolean',
f519ab0b
TL
4010 description => "Delete the original disk after successful copy. By default the"
4011 ." original disk is kept as unused disk.",
70d45e33
DM
4012 optional => 1,
4013 default => 0,
4014 },
586bfa78
AD
4015 digest => {
4016 type => 'string',
f519ab0b
TL
4017 description => 'Prevent changes if current configuration file has different SHA1"
4018 ." digest. This can be used to prevent concurrent modifications.',
586bfa78
AD
4019 maxLength => 40,
4020 optional => 1,
4021 },
0aab5a16
SI
4022 bwlimit => {
4023 description => "Override I/O bandwidth limit (in KiB/s).",
4024 optional => 1,
4025 type => 'integer',
4026 minimum => '0',
41756a3b 4027 default => 'move limit from datacenter or storage config',
0aab5a16 4028 },
a9453218
AL
4029 'target-disk' => {
4030 type => 'string',
f519ab0b
TL
4031 description => "The config key the disk will be moved to on the target VM"
4032 ." (for example, ide0 or scsi1). Default is the source disk key.",
a9453218
AL
4033 enum => [PVE::QemuServer::Drive::valid_drive_names_with_unused()],
4034 optional => 1,
4035 },
4036 'target-digest' => {
4037 type => 'string',
f519ab0b
TL
4038 description => 'Prevent changes if the current config file of the target VM has a"
4039 ." different SHA1 digest. This can be used to detect concurrent modifications.',
a9453218
AL
4040 maxLength => 40,
4041 optional => 1,
4042 },
586bfa78
AD
4043 },
4044 },
e2cd75fa
DM
4045 returns => {
4046 type => 'string',
4047 description => "the task ID.",
4048 },
586bfa78
AD
4049 code => sub {
4050 my ($param) = @_;
4051
4052 my $rpcenv = PVE::RPCEnvironment::get();
586bfa78
AD
4053 my $authuser = $rpcenv->get_user();
4054
4055 my $node = extract_param($param, 'node');
586bfa78 4056 my $vmid = extract_param($param, 'vmid');
a9453218 4057 my $target_vmid = extract_param($param, 'target-vmid');
586bfa78 4058 my $digest = extract_param($param, 'digest');
a9453218 4059 my $target_digest = extract_param($param, 'target-digest');
586bfa78 4060 my $disk = extract_param($param, 'disk');
a9453218 4061 my $target_disk = extract_param($param, 'target-disk') // $disk;
586bfa78 4062 my $storeid = extract_param($param, 'storage');
586bfa78
AD
4063 my $format = extract_param($param, 'format');
4064
586bfa78
AD
4065 my $storecfg = PVE::Storage::config();
4066
bdf6ba1e 4067 my $load_and_check_move = sub {
ffda963f 4068 my $conf = PVE::QemuConfig->load_config($vmid);
dcce9b46
FG
4069 PVE::QemuConfig->check_lock($conf);
4070
44102492 4071 PVE::Tools::assert_if_modified($digest, $conf->{digest});
586bfa78
AD
4072
4073 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4074
4075 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
4076
a85ff91b 4077 die "disk '$disk' has no associated volume\n" if !$drive->{file};
931432bd 4078 die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive, 1);
586bfa78 4079
a85ff91b 4080 my $old_volid = $drive->{file};
e2cd75fa 4081 my $oldfmt;
70d45e33 4082 my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
586bfa78
AD
4083 if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){
4084 $oldfmt = $1;
4085 }
4086
bdf6ba1e
FE
4087 die "you can't move to the same storage with same format\n"
4088 if $oldstoreid eq $storeid && (!$format || !$oldfmt || $oldfmt eq $format);
586bfa78 4089
9dbf9b54 4090 # this only checks snapshots because $disk is passed!
70c0ad66
AL
4091 my $snapshotted = PVE::QemuServer::Drive::is_volume_in_use(
4092 $storecfg,
4093 $conf,
4094 $disk,
4095 $old_volid
4096 );
9dbf9b54
FG
4097 die "you can't move a disk with snapshots and delete the source\n"
4098 if $snapshotted && $param->{delete};
4099
bdf6ba1e
FE
4100 return ($conf, $drive, $oldstoreid, $snapshotted);
4101 };
4102
4103 my $move_updatefn = sub {
4104 my ($conf, $drive, $oldstoreid, $snapshotted) = $load_and_check_move->();
4105 my $old_volid = $drive->{file};
4106
70c0ad66
AL
4107 PVE::Cluster::log_msg(
4108 'info',
4109 $authuser,
4110 "move disk VM $vmid: move --disk $disk --storage $storeid"
4111 );
586bfa78
AD
4112
4113 my $running = PVE::QemuServer::check_running($vmid);
e2cd75fa
DM
4114
4115 PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]);
4116
bdf6ba1e 4117 my $newvollist = [];
586bfa78 4118
bdf6ba1e
FE
4119 eval {
4120 local $SIG{INT} =
4121 local $SIG{TERM} =
4122 local $SIG{QUIT} =
4123 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
0aab5a16 4124
bdf6ba1e
FE
4125 warn "moving disk with snapshots, snapshots will not be moved!\n"
4126 if $snapshotted;
e2cd75fa 4127
bdf6ba1e
FE
4128 my $bwlimit = extract_param($param, 'bwlimit');
4129 my $movelimit = PVE::Storage::get_bandwidth_limit(
4130 'move',
4131 [$oldstoreid, $storeid],
4132 $bwlimit
4133 );
7043d946 4134
1196086f
FE
4135 my $source_info = {
4136 vmid => $vmid,
4137 running => $running,
4138 drivename => $disk,
4139 drive => $drive,
4140 snapname => undef,
4141 };
4142
4143 my $dest_info = {
4144 vmid => $vmid,
25166060 4145 drivename => $disk,
1196086f
FE
4146 storage => $storeid,
4147 format => $format,
4148 };
4149
7344af7b
FE
4150 $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($conf)
4151 if $disk eq 'efidisk0';
4152
bdf6ba1e
FE
4153 my $newdrive = PVE::QemuServer::clone_disk(
4154 $storecfg,
1196086f
FE
4155 $source_info,
4156 $dest_info,
bdf6ba1e
FE
4157 1,
4158 $newvollist,
4159 undef,
4160 undef,
4161 undef,
4162 $movelimit,
bdf6ba1e
FE
4163 );
4164 $conf->{$disk} = PVE::QemuServer::print_drive($newdrive);
fbd7dcce 4165
bdf6ba1e 4166 PVE::QemuConfig->add_unused_volume($conf, $old_volid) if !$param->{delete};
73272365 4167
bdf6ba1e
FE
4168 # convert moved disk to base if part of template
4169 PVE::QemuServer::template_create($vmid, $conf, $disk)
4170 if PVE::QemuConfig->is_template($conf);
ca662131 4171
bdf6ba1e 4172 PVE::QemuConfig->write_config($vmid, $conf);
70d45e33 4173
bdf6ba1e
FE
4174 my $do_trim = PVE::QemuServer::get_qga_key($conf, 'fstrim_cloned_disks');
4175 if ($running && $do_trim && PVE::QemuServer::qga_check_running($vmid)) {
4176 eval { mon_cmd($vmid, "guest-fstrim") };
70d45e33 4177 }
bdf6ba1e
FE
4178
4179 eval {
4180 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
4181 PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ])
4182 if !$running;
4183 };
4184 warn $@ if $@;
586bfa78 4185 };
bdf6ba1e
FE
4186 if (my $err = $@) {
4187 foreach my $volid (@$newvollist) {
4188 eval { PVE::Storage::vdisk_free($storecfg, $volid) };
4189 warn $@ if $@;
4190 }
4191 die "storage migration failed: $err";
4192 }
586bfa78 4193
bdf6ba1e
FE
4194 if ($param->{delete}) {
4195 eval {
4196 PVE::Storage::deactivate_volumes($storecfg, [$old_volid]);
4197 PVE::Storage::vdisk_free($storecfg, $old_volid);
4198 };
4199 warn $@ if $@;
4200 }
586bfa78 4201 };
e2cd75fa 4202
a9453218
AL
4203 my $load_and_check_reassign_configs = sub {
4204 my $vmlist = PVE::Cluster::get_vmlist()->{ids};
4205
4206 die "could not find VM ${vmid}\n" if !exists($vmlist->{$vmid});
dbc817ba 4207 die "could not find target VM ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
a9453218 4208
dbc817ba
FG
4209 my $source_node = $vmlist->{$vmid}->{node};
4210 my $target_node = $vmlist->{$target_vmid}->{node};
4211
4212 die "Both VMs need to be on the same node ($source_node != $target_node)\n"
4213 if $source_node ne $target_node;
a9453218
AL
4214
4215 my $source_conf = PVE::QemuConfig->load_config($vmid);
4216 PVE::QemuConfig->check_lock($source_conf);
4217 my $target_conf = PVE::QemuConfig->load_config($target_vmid);
4218 PVE::QemuConfig->check_lock($target_conf);
4219
4220 die "Can't move disks from or to template VMs\n"
4221 if ($source_conf->{template} || $target_conf->{template});
4222
4223 if ($digest) {
4224 eval { PVE::Tools::assert_if_modified($digest, $source_conf->{digest}) };
4225 die "VM ${vmid}: $@" if $@;
4226 }
4227
4228 if ($target_digest) {
4229 eval { PVE::Tools::assert_if_modified($target_digest, $target_conf->{digest}) };
4230 die "VM ${target_vmid}: $@" if $@;
4231 }
4232
4233 die "Disk '${disk}' for VM '$vmid' does not exist\n" if !defined($source_conf->{$disk});
4234
4235 die "Target disk key '${target_disk}' is already in use for VM '$target_vmid'\n"
dbc817ba 4236 if $target_conf->{$target_disk};
a9453218
AL
4237
4238 my $drive = PVE::QemuServer::parse_drive(
4239 $disk,
4240 $source_conf->{$disk},
4241 );
dbc817ba
FG
4242 die "failed to parse source disk - $@\n" if !$drive;
4243
4244 my $source_volid = $drive->{file};
a9453218
AL
4245
4246 die "disk '${disk}' has no associated volume\n" if !$source_volid;
4247 die "CD drive contents can't be moved to another VM\n"
4248 if PVE::QemuServer::drive_is_cdrom($drive, 1);
dbc817ba
FG
4249
4250 my $storeid = PVE::Storage::parse_volume_id($source_volid, 1);
4251 die "Volume '$source_volid' not managed by PVE\n" if !defined($storeid);
4252
a9453218
AL
4253 die "Can't move disk used by a snapshot to another VM\n"
4254 if PVE::QemuServer::Drive::is_volume_in_use($storecfg, $source_conf, $disk, $source_volid);
4255 die "Storage does not support moving of this disk to another VM\n"
4256 if (!PVE::Storage::volume_has_feature($storecfg, 'rename', $source_volid));
dbc817ba 4257 die "Cannot move disk to another VM while the source VM is running - detach first\n"
a9453218
AL
4258 if PVE::QemuServer::check_running($vmid) && $disk !~ m/^unused\d+$/;
4259
dbc817ba 4260 # now re-parse using target disk slot format
f4e4c779
FG
4261 if ($target_disk =~ /^unused\d+$/) {
4262 $drive = PVE::QemuServer::parse_drive(
4263 $target_disk,
4264 $source_volid,
4265 );
4266 } else {
4267 $drive = PVE::QemuServer::parse_drive(
4268 $target_disk,
4269 $source_conf->{$disk},
4270 );
4271 }
dbc817ba 4272 die "failed to parse source disk for target disk format - $@\n" if !$drive;
a9453218
AL
4273
4274 my $repl_conf = PVE::ReplicationConfig->new();
dbc817ba
FG
4275 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
4276 my $format = (PVE::Storage::parse_volname($storecfg, $source_volid))[6];
4277 die "Cannot move disk to a replicated VM. Storage does not support replication!\n"
4278 if !PVE::Storage::storage_can_replicate($storecfg, $storeid, $format);
a9453218
AL
4279 }
4280
dbc817ba 4281 return ($source_conf, $target_conf, $drive);
a9453218
AL
4282 };
4283
4284 my $logfunc = sub {
4285 my ($msg) = @_;
4286 print STDERR "$msg\n";
4287 };
4288
4289 my $disk_reassignfn = sub {
4290 return PVE::QemuConfig->lock_config($vmid, sub {
4291 return PVE::QemuConfig->lock_config($target_vmid, sub {
dbc817ba 4292 my ($source_conf, $target_conf, $drive) = &$load_and_check_reassign_configs();
a9453218 4293
dbc817ba 4294 my $source_volid = $drive->{file};
a9453218
AL
4295
4296 print "moving disk '$disk' from VM '$vmid' to '$target_vmid'\n";
4297 my ($storeid, $source_volname) = PVE::Storage::parse_volume_id($source_volid);
4298
4299 my $fmt = (PVE::Storage::parse_volname($storecfg, $source_volid))[6];
4300
4301 my $new_volid = PVE::Storage::rename_volume(
4302 $storecfg,
4303 $source_volid,
4304 $target_vmid,
4305 );
4306
dbc817ba 4307 $drive->{file} = $new_volid;
a9453218 4308
96670745
TL
4309 my $boot_order = PVE::QemuServer::device_bootorder($source_conf);
4310 if (defined(delete $boot_order->{$disk})) {
4311 print "removing disk '$disk' from boot order config\n";
4312 my $boot_devs = [ sort { $boot_order->{$a} <=> $boot_order->{$b} } keys %$boot_order ];
4313 $source_conf->{boot} = PVE::QemuServer::print_bootorder($boot_devs);
4314 }
4315
a9453218
AL
4316 delete $source_conf->{$disk};
4317 print "removing disk '${disk}' from VM '${vmid}' config\n";
4318 PVE::QemuConfig->write_config($vmid, $source_conf);
4319
dbc817ba 4320 my $drive_string = PVE::QemuServer::print_drive($drive);
bf67da2b
AL
4321
4322 if ($target_disk =~ /^unused\d+$/) {
4323 $target_conf->{$target_disk} = $drive_string;
4324 PVE::QemuConfig->write_config($target_vmid, $target_conf);
4325 } else {
4326 &$update_vm_api(
4327 {
4328 node => $node,
4329 vmid => $target_vmid,
4330 digest => $target_digest,
4331 $target_disk => $drive_string,
4332 },
4333 1,
4334 );
4335 }
a9453218
AL
4336
4337 # remove possible replication snapshots
4338 if (PVE::Storage::volume_has_feature(
4339 $storecfg,
4340 'replicate',
4341 $source_volid),
4342 ) {
4343 eval {
4344 PVE::Replication::prepare(
4345 $storecfg,
4346 [$new_volid],
4347 undef,
4348 1,
4349 undef,
4350 $logfunc,
4351 )
4352 };
4353 if (my $err = $@) {
4354 print "Failed to remove replication snapshots on moved disk " .
4355 "'$target_disk'. Manual cleanup could be necessary.\n";
4356 }
4357 }
4358 });
4359 });
4360 };
4361
dbc817ba
FG
4362 if ($target_vmid && $storeid) {
4363 my $msg = "either set 'storage' or 'target-vmid', but not both";
4364 raise_param_exc({ 'target-vmid' => $msg, 'storage' => $msg });
4365 } elsif ($target_vmid) {
a9453218
AL
4366 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
4367 if $authuser ne 'root@pam';
4368
dbc817ba 4369 raise_param_exc({ 'target-vmid' => "must be different than source VMID to reassign disk" })
a9453218
AL
4370 if $vmid eq $target_vmid;
4371
44102492 4372 my (undef, undef, $drive) = &$load_and_check_reassign_configs();
a6273aa8
FG
4373 my $storage = PVE::Storage::parse_volume_id($drive->{file});
4374 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
44102492 4375
a9453218
AL
4376 return $rpcenv->fork_worker(
4377 'qmmove',
4378 "${vmid}-${disk}>${target_vmid}-${target_disk}",
4379 $authuser,
4380 $disk_reassignfn
4381 );
4382 } elsif ($storeid) {
44102492
FG
4383 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
4384
bdf6ba1e
FE
4385 $load_and_check_move->(); # early checks before forking/locking
4386
4387 my $realcmd = sub {
4388 PVE::QemuConfig->lock_config($vmid, $move_updatefn);
4389 };
4390
4391 return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd);
a9453218 4392 } else {
dbc817ba
FG
4393 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
4394 raise_param_exc({ 'target-vmid' => $msg, 'storage' => $msg });
a9453218 4395 }
586bfa78
AD
4396 }});
4397
71fc647f
TM
4398my $check_vm_disks_local = sub {
4399 my ($storecfg, $vmconf, $vmid) = @_;
4400
4401 my $local_disks = {};
4402
4403 # add some more information to the disks e.g. cdrom
0b7a0b78 4404 PVE::QemuServer::foreach_volid($vmconf, sub {
71fc647f
TM
4405 my ($volid, $attr) = @_;
4406
4407 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
4408 if ($storeid) {
4409 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
4410 return if $scfg->{shared};
4411 }
4412 # The shared attr here is just a special case where the vdisk
4413 # is marked as shared manually
4414 return if $attr->{shared};
4415 return if $attr->{cdrom} and $volid eq "none";
4416
4417 if (exists $local_disks->{$volid}) {
4418 @{$local_disks->{$volid}}{keys %$attr} = values %$attr
4419 } else {
4420 $local_disks->{$volid} = $attr;
4421 # ensure volid is present in case it's needed
4422 $local_disks->{$volid}->{volid} = $volid;
4423 }
4424 });
4425
4426 return $local_disks;
4427};
4428
4429__PACKAGE__->register_method({
4430 name => 'migrate_vm_precondition',
4431 path => '{vmid}/migrate',
4432 method => 'GET',
4433 protected => 1,
4434 proxyto => 'node',
4435 description => "Get preconditions for migration.",
4436 permissions => {
4437 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4438 },
4439 parameters => {
4440 additionalProperties => 0,
4441 properties => {
4442 node => get_standard_option('pve-node'),
4443 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
4444 target => get_standard_option('pve-node', {
4445 description => "Target node.",
4446 completion => \&PVE::Cluster::complete_migration_target,
4447 optional => 1,
4448 }),
4449 },
4450 },
4451 returns => {
4452 type => "object",
4453 properties => {
4454 running => { type => 'boolean' },
4455 allowed_nodes => {
4456 type => 'array',
4457 optional => 1,
f25852c2
TM
4458 description => "List nodes allowed for offline migration, only passed if VM is offline"
4459 },
4460 not_allowed_nodes => {
4461 type => 'object',
4462 optional => 1,
4463 description => "List not allowed nodes with additional informations, only passed if VM is offline"
71fc647f
TM
4464 },
4465 local_disks => {
4466 type => 'array',
4467 description => "List local disks including CD-Rom, unsused and not referenced disks"
4468 },
4469 local_resources => {
4470 type => 'array',
4471 description => "List local resources e.g. pci, usb"
bc1a5860
DC
4472 },
4473 'mapped-resources' => {
4474 type => 'array',
4475 description => "List of mapped resources e.g. pci, usb"
4476 },
71fc647f
TM
4477 },
4478 },
4479 code => sub {
4480 my ($param) = @_;
4481
4482 my $rpcenv = PVE::RPCEnvironment::get();
4483
4484 my $authuser = $rpcenv->get_user();
4485
4486 PVE::Cluster::check_cfs_quorum();
4487
4488 my $res = {};
4489
4490 my $vmid = extract_param($param, 'vmid');
4491 my $target = extract_param($param, 'target');
4492 my $localnode = PVE::INotify::nodename();
4493
4494
4495 # test if VM exists
4496 my $vmconf = PVE::QemuConfig->load_config($vmid);
4497 my $storecfg = PVE::Storage::config();
4498
4499
4500 # try to detect errors early
4501 PVE::QemuConfig->check_lock($vmconf);
4502
4503 $res->{running} = PVE::QemuServer::check_running($vmid) ? 1:0;
4504
bc1a5860
DC
4505 my ($local_resources, $mapped_resources, $missing_mappings_by_node) =
4506 PVE::QemuServer::check_local_resources($vmconf, 1);
4507 delete $missing_mappings_by_node->{$localnode};
4508
bb30eedf
MF
4509 my $vga = PVE::QemuServer::parse_vga($vmconf->{vga});
4510 if ($res->{running} && $vga->{'clipboard'} && $vga->{'clipboard'} eq 'vnc') {
4511 push $local_resources->@*, "clipboard=vnc";
4512 }
4513
bc1a5860 4514 # if vm is not running, return target nodes where local storage/mapped devices are available
71fc647f
TM
4515 # for offline migration
4516 if (!$res->{running}) {
f25852c2
TM
4517 $res->{allowed_nodes} = [];
4518 my $checked_nodes = PVE::QemuServer::check_local_storage_availability($vmconf, $storecfg);
32075a2c 4519 delete $checked_nodes->{$localnode};
f25852c2 4520
f25852c2 4521 foreach my $node (keys %$checked_nodes) {
bc1a5860
DC
4522 my $missing_mappings = $missing_mappings_by_node->{$node};
4523 if (scalar($missing_mappings->@*)) {
4524 $checked_nodes->{$node}->{'unavailable-resources'} = $missing_mappings;
4525 next;
4526 }
4527
4528 if (!defined($checked_nodes->{$node}->{unavailable_storages})) {
f25852c2 4529 push @{$res->{allowed_nodes}}, $node;
f25852c2 4530 }
71fc647f 4531
f25852c2
TM
4532 }
4533 $res->{not_allowed_nodes} = $checked_nodes;
71fc647f
TM
4534 }
4535
71fc647f
TM
4536 my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
4537 $res->{local_disks} = [ values %$local_disks ];;
4538
71fc647f 4539 $res->{local_resources} = $local_resources;
bc1a5860 4540 $res->{'mapped-resources'} = $mapped_resources;
71fc647f
TM
4541
4542 return $res;
4543
4544
4545 }});
4546
3ea94c60 4547__PACKAGE__->register_method({
afdb31d5 4548 name => 'migrate_vm',
3ea94c60
DM
4549 path => '{vmid}/migrate',
4550 method => 'POST',
4551 protected => 1,
4552 proxyto => 'node',
4553 description => "Migrate virtual machine. Creates a new migration task.",
a0d1b1a2
DM
4554 permissions => {
4555 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4556 },
3ea94c60 4557 parameters => {
3326ae19 4558 additionalProperties => 0,
3ea94c60
DM
4559 properties => {
4560 node => get_standard_option('pve-node'),
335af808 4561 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
c2ed338e 4562 target => get_standard_option('pve-node', {
335af808
DM
4563 description => "Target node.",
4564 completion => \&PVE::Cluster::complete_migration_target,
4565 }),
3ea94c60
DM
4566 online => {
4567 type => 'boolean',
13739386 4568 description => "Use online/live migration if VM is running. Ignored if VM is stopped.",
3ea94c60
DM
4569 optional => 1,
4570 },
4571 force => {
4572 type => 'boolean',
4573 description => "Allow to migrate VMs which use local devices. Only root may use this option.",
4574 optional => 1,
4575 },
2de2d6f7
TL
4576 migration_type => {
4577 type => 'string',
4578 enum => ['secure', 'insecure'],
c07a9e3d 4579 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
4580 optional => 1,
4581 },
4582 migration_network => {
c07a9e3d 4583 type => 'string', format => 'CIDR',
2de2d6f7
TL
4584 description => "CIDR of the (sub) network that is used for migration.",
4585 optional => 1,
4586 },
56af7146
AD
4587 "with-local-disks" => {
4588 type => 'boolean',
4589 description => "Enable live storage migration for local disk",
b74cad8a 4590 optional => 1,
56af7146 4591 },
bf8fc5a3 4592 targetstorage => get_standard_option('pve-targetstorage', {
255e9c54 4593 completion => \&PVE::QemuServer::complete_migration_storage,
56af7146 4594 }),
0aab5a16
SI
4595 bwlimit => {
4596 description => "Override I/O bandwidth limit (in KiB/s).",
4597 optional => 1,
4598 type => 'integer',
4599 minimum => '0',
41756a3b 4600 default => 'migrate limit from datacenter or storage config',
0aab5a16 4601 },
3ea94c60
DM
4602 },
4603 },
afdb31d5 4604 returns => {
3ea94c60
DM
4605 type => 'string',
4606 description => "the task ID.",
4607 },
4608 code => sub {
4609 my ($param) = @_;
4610
4611 my $rpcenv = PVE::RPCEnvironment::get();
a0d1b1a2 4612 my $authuser = $rpcenv->get_user();
3ea94c60
DM
4613
4614 my $target = extract_param($param, 'target');
4615
4616 my $localnode = PVE::INotify::nodename();
4617 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
4618
4619 PVE::Cluster::check_cfs_quorum();
4620
4621 PVE::Cluster::check_node_exists($target);
4622
4623 my $targetip = PVE::Cluster::remote_node_ip($target);
4624
4625 my $vmid = extract_param($param, 'vmid');
4626
afdb31d5 4627 raise_param_exc({ force => "Only root may use this option." })
a0d1b1a2 4628 if $param->{force} && $authuser ne 'root@pam';
3ea94c60 4629
2de2d6f7
TL
4630 raise_param_exc({ migration_type => "Only root may use this option." })
4631 if $param->{migration_type} && $authuser ne 'root@pam';
4632
4633 # allow root only until better network permissions are available
4634 raise_param_exc({ migration_network => "Only root may use this option." })
4635 if $param->{migration_network} && $authuser ne 'root@pam';
4636
3ea94c60 4637 # test if VM exists
ffda963f 4638 my $conf = PVE::QemuConfig->load_config($vmid);
3ea94c60
DM
4639
4640 # try to detect errors early
a5ed42d3 4641
ffda963f 4642 PVE::QemuConfig->check_lock($conf);
a5ed42d3 4643
3ea94c60 4644 if (PVE::QemuServer::check_running($vmid)) {
fda72913 4645 die "can't migrate running VM without --online\n" if !$param->{online};
aa491a6e
FE
4646
4647 my $repl_conf = PVE::ReplicationConfig->new();
4648 my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
4649 my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
68980d66
FE
4650 if (!$param->{force} && $is_replicated && !$is_replicated_to_target) {
4651 die "Cannot live-migrate replicated VM to node '$target' - not a replication " .
4652 "target. Use 'force' to override.\n";
aa491a6e 4653 }
13739386 4654 } else {
c3ddb94d 4655 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online};
13739386 4656 $param->{online} = 0;
3ea94c60
DM
4657 }
4658
47152e2e 4659 my $storecfg = PVE::Storage::config();
bf8fc5a3
FG
4660 if (my $targetstorage = $param->{targetstorage}) {
4661 my $storagemap = eval { PVE::JSONSchema::parse_idmap($targetstorage, 'pve-storage-id') };
e214cda8 4662 raise_param_exc({ targetstorage => "failed to parse storage map: $@" })
bf8fc5a3
FG
4663 if $@;
4664
aea447bb
FG
4665 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
4666 if !defined($storagemap->{identity});
4667
bd61033e 4668 foreach my $target_sid (values %{$storagemap->{entries}}) {
9fb295d0 4669 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
bf8fc5a3
FG
4670 }
4671
9fb295d0 4672 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
bf8fc5a3
FG
4673 if $storagemap->{default};
4674
4675 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target)
4676 if $storagemap->{identity};
4677
4678 $param->{storagemap} = $storagemap;
d80ad67f
AD
4679 } else {
4680 PVE::QemuServer::check_storage_availability($storecfg, $conf, $target);
4681 }
47152e2e 4682
2003f0f8 4683 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
3ea94c60 4684
88fc87b4
DM
4685 my $hacmd = sub {
4686 my $upid = shift;
3ea94c60 4687
02765844 4688 print "Requesting HA migration for VM $vmid to node $target\n";
88fc87b4 4689
a4262553 4690 my $cmd = ['ha-manager', 'migrate', "vm:$vmid", $target];
88fc87b4 4691 PVE::Tools::run_command($cmd);
88fc87b4
DM
4692 return;
4693 };
4694
4695 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
4696
4697 } else {
4698
f53c6ad8 4699 my $realcmd = sub {
f53c6ad8
DM
4700 PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param);
4701 };
88fc87b4 4702
f53c6ad8
DM
4703 my $worker = sub {
4704 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
88fc87b4
DM
4705 };
4706
f53c6ad8 4707 return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $worker);
88fc87b4 4708 }
3ea94c60 4709
3ea94c60 4710 }});
1e3baf05 4711
06fedff6
FG
4712__PACKAGE__->register_method({
4713 name => 'remote_migrate_vm',
4714 path => '{vmid}/remote_migrate',
4715 method => 'POST',
4716 protected => 1,
4717 proxyto => 'node',
4718 description => "Migrate virtual machine to a remote cluster. Creates a new migration task. EXPERIMENTAL feature!",
4719 permissions => {
4720 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
4721 },
4722 parameters => {
4723 additionalProperties => 0,
4724 properties => {
4725 node => get_standard_option('pve-node'),
4726 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
4727 'target-vmid' => get_standard_option('pve-vmid', { optional => 1 }),
4728 'target-endpoint' => get_standard_option('proxmox-remote', {
4729 description => "Remote target endpoint",
4730 }),
4731 online => {
4732 type => 'boolean',
4733 description => "Use online/live migration if VM is running. Ignored if VM is stopped.",
4734 optional => 1,
4735 },
4736 delete => {
4737 type => 'boolean',
4738 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.",
4739 optional => 1,
4740 default => 0,
4741 },
4742 'target-storage' => get_standard_option('pve-targetstorage', {
4743 completion => \&PVE::QemuServer::complete_migration_storage,
4744 optional => 0,
4745 }),
4746 'target-bridge' => {
4747 type => 'string',
4748 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.",
4749 format => 'bridge-pair-list',
4750 },
4751 bwlimit => {
4752 description => "Override I/O bandwidth limit (in KiB/s).",
4753 optional => 1,
4754 type => 'integer',
4755 minimum => '0',
4756 default => 'migrate limit from datacenter or storage config',
4757 },
4758 },
4759 },
4760 returns => {
4761 type => 'string',
4762 description => "the task ID.",
4763 },
4764 code => sub {
4765 my ($param) = @_;
4766
4767 my $rpcenv = PVE::RPCEnvironment::get();
4768 my $authuser = $rpcenv->get_user();
4769
4770 my $source_vmid = extract_param($param, 'vmid');
4771 my $target_endpoint = extract_param($param, 'target-endpoint');
4772 my $target_vmid = extract_param($param, 'target-vmid') // $source_vmid;
4773
4774 my $delete = extract_param($param, 'delete') // 0;
4775
4776 PVE::Cluster::check_cfs_quorum();
4777
4778 # test if VM exists
4779 my $conf = PVE::QemuConfig->load_config($source_vmid);
4780
4781 PVE::QemuConfig->check_lock($conf);
4782
4783 raise_param_exc({ vmid => "cannot migrate HA-managed VM to remote cluster" })
4784 if PVE::HA::Config::vm_is_ha_managed($source_vmid);
4785
4786 my $remote = PVE::JSONSchema::parse_property_string('proxmox-remote', $target_endpoint);
4787
4788 # TODO: move this as helper somewhere appropriate?
4789 my $conn_args = {
4790 protocol => 'https',
4791 host => $remote->{host},
4792 port => $remote->{port} // 8006,
4793 apitoken => $remote->{apitoken},
4794 };
4795
4796 my $fp;
4797 if ($fp = $remote->{fingerprint}) {
4798 $conn_args->{cached_fingerprints} = { uc($fp) => 1 };
4799 }
4800
4801 print "Establishing API connection with remote at '$remote->{host}'\n";
4802
4803 my $api_client = PVE::APIClient::LWP->new(%$conn_args);
4804
4805 if (!defined($fp)) {
4806 my $cert_info = $api_client->get("/nodes/localhost/certificates/info");
4807 foreach my $cert (@$cert_info) {
4808 my $filename = $cert->{filename};
4809 next if $filename ne 'pveproxy-ssl.pem' && $filename ne 'pve-ssl.pem';
4810 $fp = $cert->{fingerprint} if !$fp || $filename eq 'pveproxy-ssl.pem';
4811 }
4812 $conn_args->{cached_fingerprints} = { uc($fp) => 1 }
4813 if defined($fp);
4814 }
4815
4816 my $repl_conf = PVE::ReplicationConfig->new();
4817 my $is_replicated = $repl_conf->check_for_existing_jobs($source_vmid, 1);
4818 die "cannot remote-migrate replicated VM\n" if $is_replicated;
4819
4820 if (PVE::QemuServer::check_running($source_vmid)) {
4821 die "can't migrate running VM without --online\n" if !$param->{online};
4822
4823 } else {
4824 warn "VM isn't running. Doing offline migration instead.\n" if $param->{online};
4825 $param->{online} = 0;
4826 }
4827
06fedff6
FG
4828 my $storecfg = PVE::Storage::config();
4829 my $target_storage = extract_param($param, 'target-storage');
4830 my $storagemap = eval { PVE::JSONSchema::parse_idmap($target_storage, 'pve-storage-id') };
4831 raise_param_exc({ 'target-storage' => "failed to parse storage map: $@" })
4832 if $@;
4833
4834 my $target_bridge = extract_param($param, 'target-bridge');
4835 my $bridgemap = eval { PVE::JSONSchema::parse_idmap($target_bridge, 'pve-bridge-id') };
4836 raise_param_exc({ 'target-bridge' => "failed to parse bridge map: $@" })
4837 if $@;
4838
06fedff6
FG
4839 die "remote migration requires explicit storage mapping!\n"
4840 if $storagemap->{identity};
4841
4842 $param->{storagemap} = $storagemap;
4843 $param->{bridgemap} = $bridgemap;
4844 $param->{remote} = {
4845 conn => $conn_args, # re-use fingerprint for tunnel
4846 client => $api_client,
4847 vmid => $target_vmid,
4848 };
4849 $param->{migration_type} = 'websocket';
4850 $param->{'with-local-disks'} = 1;
4851 $param->{delete} = $delete if $delete;
4852
4853 my $cluster_status = $api_client->get("/cluster/status");
4854 my $target_node;
4855 foreach my $entry (@$cluster_status) {
4856 next if $entry->{type} ne 'node';
4857 if ($entry->{local}) {
4858 $target_node = $entry->{name};
4859 last;
4860 }
4861 }
4862
4863 die "couldn't determine endpoint's node name\n"
4864 if !defined($target_node);
4865
4866 my $realcmd = sub {
4867 PVE::QemuMigrate->migrate($target_node, $remote->{host}, $source_vmid, $param);
4868 };
4869
4870 my $worker = sub {
4871 return PVE::GuestHelpers::guest_migration_lock($source_vmid, 10, $realcmd);
4872 };
4873
4874 return $rpcenv->fork_worker('qmigrate', $source_vmid, $authuser, $worker);
4875 }});
4876
91c94f0a 4877__PACKAGE__->register_method({
afdb31d5
DM
4878 name => 'monitor',
4879 path => '{vmid}/monitor',
91c94f0a
DM
4880 method => 'POST',
4881 protected => 1,
4882 proxyto => 'node',
7bd9abd2 4883 description => "Execute QEMU monitor commands.",
a0d1b1a2 4884 permissions => {
a8f2f427 4885 description => "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
c07a9e3d 4886 check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
a0d1b1a2 4887 },
91c94f0a 4888 parameters => {
3326ae19 4889 additionalProperties => 0,
91c94f0a
DM
4890 properties => {
4891 node => get_standard_option('pve-node'),
4892 vmid => get_standard_option('pve-vmid'),
4893 command => {
4894 type => 'string',
4895 description => "The monitor command.",
4896 }
4897 },
4898 },
4899 returns => { type => 'string'},
4900 code => sub {
4901 my ($param) = @_;
4902
a8f2f427
FG
4903 my $rpcenv = PVE::RPCEnvironment::get();
4904 my $authuser = $rpcenv->get_user();
4905
4906 my $is_ro = sub {
4907 my $command = shift;
4908 return $command =~ m/^\s*info(\s+|$)/
4909 || $command =~ m/^\s*help\s*$/;
4910 };
4911
4912 $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
4913 if !&$is_ro($param->{command});
4914
91c94f0a
DM
4915 my $vmid = $param->{vmid};
4916
ffda963f 4917 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
91c94f0a
DM
4918
4919 my $res = '';
4920 eval {
0a13e08e 4921 $res = PVE::QemuServer::Monitor::hmp_cmd($vmid, $param->{command});
91c94f0a
DM
4922 };
4923 $res = "ERROR: $@" if $@;
4924
4925 return $res;
4926 }});
4927
0d02881c
AD
4928__PACKAGE__->register_method({
4929 name => 'resize_vm',
614e3941 4930 path => '{vmid}/resize',
0d02881c
AD
4931 method => 'PUT',
4932 protected => 1,
4933 proxyto => 'node',
2f48a4f5 4934 description => "Extend volume size.",
0d02881c 4935 permissions => {
3b2773f6 4936 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
0d02881c
AD
4937 },
4938 parameters => {
3326ae19
TL
4939 additionalProperties => 0,
4940 properties => {
2f48a4f5 4941 node => get_standard_option('pve-node'),
335af808 4942 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
2f48a4f5
DM
4943 skiplock => get_standard_option('skiplock'),
4944 disk => {
4945 type => 'string',
4946 description => "The disk you want to resize.",
e0fd2b2f 4947 enum => [PVE::QemuServer::Drive::valid_drive_names()],
2f48a4f5
DM
4948 },
4949 size => {
4950 type => 'string',
f91b2e45 4951 pattern => '\+?\d+(\.\d+)?[KMGT]?',
e248477e 4952 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
4953 },
4954 digest => {
4955 type => 'string',
4956 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
4957 maxLength => 40,
4958 optional => 1,
4959 },
4960 },
0d02881c 4961 },
606d9d76
FE
4962 returns => {
4963 type => 'string',
4964 description => "the task ID.",
4965 },
0d02881c
AD
4966 code => sub {
4967 my ($param) = @_;
4968
4969 my $rpcenv = PVE::RPCEnvironment::get();
4970
4971 my $authuser = $rpcenv->get_user();
4972
4973 my $node = extract_param($param, 'node');
4974
4975 my $vmid = extract_param($param, 'vmid');
4976
4977 my $digest = extract_param($param, 'digest');
4978
2f48a4f5 4979 my $disk = extract_param($param, 'disk');
75466c4f 4980
2f48a4f5 4981 my $sizestr = extract_param($param, 'size');
0d02881c 4982
f91b2e45 4983 my $skiplock = extract_param($param, 'skiplock');
0d02881c
AD
4984 raise_param_exc({ skiplock => "Only root may use this option." })
4985 if $skiplock && $authuser ne 'root@pam';
4986
0d02881c
AD
4987 my $storecfg = PVE::Storage::config();
4988
0d02881c
AD
4989 my $updatefn = sub {
4990
ffda963f 4991 my $conf = PVE::QemuConfig->load_config($vmid);
0d02881c
AD
4992
4993 die "checksum missmatch (file change by other user?)\n"
4994 if $digest && $digest ne $conf->{digest};
ffda963f 4995 PVE::QemuConfig->check_lock($conf) if !$skiplock;
0d02881c 4996
f91b2e45
DM
4997 die "disk '$disk' does not exist\n" if !$conf->{$disk};
4998
4999 my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk});
5000
d662790a
WL
5001 my (undef, undef, undef, undef, undef, undef, $format) =
5002 PVE::Storage::parse_volname($storecfg, $drive->{file});
5003
f91b2e45
DM
5004 my $volid = $drive->{file};
5005
5006 die "disk '$disk' has no associated volume\n" if !$volid;
5007
5008 die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
5009
5010 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
5011
5012 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
5013
b572a606 5014 PVE::Storage::activate_volumes($storecfg, [$volid]);
f91b2e45
DM
5015 my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5);
5016
ed94b2ad 5017 die "Could not determine current size of volume '$volid'\n" if !defined($size);
f8b829aa 5018
f91b2e45
DM
5019 die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/;
5020 my ($ext, $newsize, $unit) = ($1, $2, $4);
5021 if ($unit) {
5022 if ($unit eq 'K') {
5023 $newsize = $newsize * 1024;
5024 } elsif ($unit eq 'M') {
5025 $newsize = $newsize * 1024 * 1024;
5026 } elsif ($unit eq 'G') {
5027 $newsize = $newsize * 1024 * 1024 * 1024;
5028 } elsif ($unit eq 'T') {
5029 $newsize = $newsize * 1024 * 1024 * 1024 * 1024;
5030 }
5031 }
5032 $newsize += $size if $ext;
5033 $newsize = int($newsize);
5034
9a478b17 5035 die "shrinking disks is not supported\n" if $newsize < $size;
f91b2e45
DM
5036
5037 return if $size == $newsize;
5038
2f48a4f5 5039 PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr");
0d02881c 5040
f91b2e45 5041 PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
75466c4f 5042
e29e5be6 5043 $drive->{size} = $newsize;
71c58bb7 5044 $conf->{$disk} = PVE::QemuServer::print_drive($drive);
f91b2e45 5045
ffda963f 5046 PVE::QemuConfig->write_config($vmid, $conf);
f91b2e45 5047 };
0d02881c 5048
606d9d76
FE
5049 my $worker = sub {
5050 PVE::QemuConfig->lock_config($vmid, $updatefn);
5051 };
5052
5053 return $rpcenv->fork_worker('resize', $vmid, $authuser, $worker);
0d02881c
AD
5054 }});
5055
9dbd1ee4 5056__PACKAGE__->register_method({
7e7d7b61 5057 name => 'snapshot_list',
9dbd1ee4 5058 path => '{vmid}/snapshot',
7e7d7b61
DM
5059 method => 'GET',
5060 description => "List all snapshots.",
5061 permissions => {
5062 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5063 },
5064 proxyto => 'node',
5065 protected => 1, # qemu pid files are only readable by root
5066 parameters => {
3326ae19 5067 additionalProperties => 0,
7e7d7b61 5068 properties => {
e261de40 5069 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
7e7d7b61
DM
5070 node => get_standard_option('pve-node'),
5071 },
5072 },
5073 returns => {
5074 type => 'array',
5075 items => {
5076 type => "object",
ce9b0a38
DM
5077 properties => {
5078 name => {
5079 description => "Snapshot identifier. Value 'current' identifies the current VM.",
5080 type => 'string',
5081 },
5082 vmstate => {
5083 description => "Snapshot includes RAM.",
5084 type => 'boolean',
5085 optional => 1,
5086 },
5087 description => {
5088 description => "Snapshot description.",
5089 type => 'string',
5090 },
5091 snaptime => {
5092 description => "Snapshot creation time",
5093 type => 'integer',
5094 renderer => 'timestamp',
5095 optional => 1,
5096 },
5097 parent => {
5098 description => "Parent snapshot identifier.",
5099 type => 'string',
5100 optional => 1,
5101 },
5102 },
7e7d7b61
DM
5103 },
5104 links => [ { rel => 'child', href => "{name}" } ],
5105 },
5106 code => sub {
5107 my ($param) = @_;
5108
6aa4651b
DM
5109 my $vmid = $param->{vmid};
5110
ffda963f 5111 my $conf = PVE::QemuConfig->load_config($vmid);
7e7d7b61
DM
5112 my $snaphash = $conf->{snapshots} || {};
5113
5114 my $res = [];
5115
5116 foreach my $name (keys %$snaphash) {
0ea6bc69 5117 my $d = $snaphash->{$name};
75466c4f
DM
5118 my $item = {
5119 name => $name,
5120 snaptime => $d->{snaptime} || 0,
6aa4651b 5121 vmstate => $d->{vmstate} ? 1 : 0,
982c7f12
DM
5122 description => $d->{description} || '',
5123 };
0ea6bc69 5124 $item->{parent} = $d->{parent} if $d->{parent};
3ee28e38 5125 $item->{snapstate} = $d->{snapstate} if $d->{snapstate};
0ea6bc69
DM
5126 push @$res, $item;
5127 }
5128
6aa4651b 5129 my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
ce9b0a38
DM
5130 my $current = {
5131 name => 'current',
5132 digest => $conf->{digest},
5133 running => $running,
5134 description => "You are here!",
5135 };
d1914468
DM
5136 $current->{parent} = $conf->{parent} if $conf->{parent};
5137
5138 push @$res, $current;
7e7d7b61
DM
5139
5140 return $res;
5141 }});
5142
5143__PACKAGE__->register_method({
5144 name => 'snapshot',
5145 path => '{vmid}/snapshot',
5146 method => 'POST',
9dbd1ee4
AD
5147 protected => 1,
5148 proxyto => 'node',
5149 description => "Snapshot a VM.",
5150 permissions => {
f1baf1df 5151 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
9dbd1ee4
AD
5152 },
5153 parameters => {
5154 additionalProperties => 0,
5155 properties => {
5156 node => get_standard_option('pve-node'),
335af808 5157 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 5158 snapname => get_standard_option('pve-snapshot-name'),
9dbd1ee4
AD
5159 vmstate => {
5160 optional => 1,
5161 type => 'boolean',
5162 description => "Save the vmstate",
5163 },
782f4f75
DM
5164 description => {
5165 optional => 1,
5166 type => 'string',
5167 description => "A textual description or comment.",
5168 },
9dbd1ee4
AD
5169 },
5170 },
7e7d7b61
DM
5171 returns => {
5172 type => 'string',
5173 description => "the task ID.",
5174 },
9dbd1ee4
AD
5175 code => sub {
5176 my ($param) = @_;
5177
5178 my $rpcenv = PVE::RPCEnvironment::get();
5179
5180 my $authuser = $rpcenv->get_user();
5181
5182 my $node = extract_param($param, 'node');
5183
5184 my $vmid = extract_param($param, 'vmid');
5185
9dbd1ee4
AD
5186 my $snapname = extract_param($param, 'snapname');
5187
d1914468
DM
5188 die "unable to use snapshot name 'current' (reserved name)\n"
5189 if $snapname eq 'current';
5190
a85c6be1
FG
5191 die "unable to use snapshot name 'pending' (reserved name)\n"
5192 if lc($snapname) eq 'pending';
5193
7e7d7b61 5194 my $realcmd = sub {
22c377f0 5195 PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname");
c2ed338e 5196 PVE::QemuConfig->snapshot_create($vmid, $snapname, $param->{vmstate},
af9110dd 5197 $param->{description});
7e7d7b61
DM
5198 };
5199
5200 return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd);
5201 }});
5202
154ccdcd
DM
5203__PACKAGE__->register_method({
5204 name => 'snapshot_cmd_idx',
5205 path => '{vmid}/snapshot/{snapname}',
5206 description => '',
5207 method => 'GET',
5208 permissions => {
5209 user => 'all',
5210 },
5211 parameters => {
3326ae19 5212 additionalProperties => 0,
154ccdcd
DM
5213 properties => {
5214 vmid => get_standard_option('pve-vmid'),
5215 node => get_standard_option('pve-node'),
8abd398b 5216 snapname => get_standard_option('pve-snapshot-name'),
154ccdcd
DM
5217 },
5218 },
5219 returns => {
5220 type => 'array',
5221 items => {
5222 type => "object",
5223 properties => {},
5224 },
5225 links => [ { rel => 'child', href => "{cmd}" } ],
5226 },
5227 code => sub {
5228 my ($param) = @_;
5229
5230 my $res = [];
5231
5232 push @$res, { cmd => 'rollback' };
d788cea6 5233 push @$res, { cmd => 'config' };
154ccdcd
DM
5234
5235 return $res;
5236 }});
5237
d788cea6
DM
5238__PACKAGE__->register_method({
5239 name => 'update_snapshot_config',
5240 path => '{vmid}/snapshot/{snapname}/config',
5241 method => 'PUT',
5242 protected => 1,
5243 proxyto => 'node',
5244 description => "Update snapshot metadata.",
5245 permissions => {
5246 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
5247 },
5248 parameters => {
5249 additionalProperties => 0,
5250 properties => {
5251 node => get_standard_option('pve-node'),
5252 vmid => get_standard_option('pve-vmid'),
5253 snapname => get_standard_option('pve-snapshot-name'),
5254 description => {
5255 optional => 1,
5256 type => 'string',
5257 description => "A textual description or comment.",
5258 },
5259 },
5260 },
5261 returns => { type => 'null' },
5262 code => sub {
5263 my ($param) = @_;
5264
5265 my $rpcenv = PVE::RPCEnvironment::get();
5266
5267 my $authuser = $rpcenv->get_user();
5268
5269 my $vmid = extract_param($param, 'vmid');
5270
5271 my $snapname = extract_param($param, 'snapname');
5272
d1c1af4b 5273 return if !defined($param->{description});
d788cea6
DM
5274
5275 my $updatefn = sub {
5276
ffda963f 5277 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6 5278
ffda963f 5279 PVE::QemuConfig->check_lock($conf);
d788cea6
DM
5280
5281 my $snap = $conf->{snapshots}->{$snapname};
5282
75466c4f
DM
5283 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5284
d788cea6
DM
5285 $snap->{description} = $param->{description} if defined($param->{description});
5286
ffda963f 5287 PVE::QemuConfig->write_config($vmid, $conf);
d788cea6
DM
5288 };
5289
ffda963f 5290 PVE::QemuConfig->lock_config($vmid, $updatefn);
d788cea6 5291
d1c1af4b 5292 return;
d788cea6
DM
5293 }});
5294
5295__PACKAGE__->register_method({
5296 name => 'get_snapshot_config',
5297 path => '{vmid}/snapshot/{snapname}/config',
5298 method => 'GET',
5299 proxyto => 'node',
5300 description => "Get snapshot configuration",
5301 permissions => {
65204e92 5302 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback', 'VM.Audit' ], any => 1],
d788cea6
DM
5303 },
5304 parameters => {
5305 additionalProperties => 0,
5306 properties => {
5307 node => get_standard_option('pve-node'),
5308 vmid => get_standard_option('pve-vmid'),
5309 snapname => get_standard_option('pve-snapshot-name'),
5310 },
5311 },
5312 returns => { type => "object" },
5313 code => sub {
5314 my ($param) = @_;
5315
5316 my $rpcenv = PVE::RPCEnvironment::get();
5317
5318 my $authuser = $rpcenv->get_user();
5319
5320 my $vmid = extract_param($param, 'vmid');
5321
5322 my $snapname = extract_param($param, 'snapname');
5323
ffda963f 5324 my $conf = PVE::QemuConfig->load_config($vmid);
d788cea6
DM
5325
5326 my $snap = $conf->{snapshots}->{$snapname};
5327
75466c4f
DM
5328 die "snapshot '$snapname' does not exist\n" if !defined($snap);
5329
d788cea6
DM
5330 return $snap;
5331 }});
5332
7e7d7b61
DM
5333__PACKAGE__->register_method({
5334 name => 'rollback',
154ccdcd 5335 path => '{vmid}/snapshot/{snapname}/rollback',
7e7d7b61
DM
5336 method => 'POST',
5337 protected => 1,
5338 proxyto => 'node',
5339 description => "Rollback VM state to specified snapshot.",
5340 permissions => {
c268337d 5341 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
7e7d7b61
DM
5342 },
5343 parameters => {
5344 additionalProperties => 0,
5345 properties => {
5346 node => get_standard_option('pve-node'),
335af808 5347 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 5348 snapname => get_standard_option('pve-snapshot-name'),
76b29aaf
SH
5349 start => {
5350 type => 'boolean',
a46d039d
SH
5351 description => "Whether the VM should get started after rolling back successfully."
5352 . " (Note: VMs will be automatically started if the snapshot includes RAM.)",
76b29aaf
SH
5353 optional => 1,
5354 default => 0,
5355 },
7e7d7b61
DM
5356 },
5357 },
5358 returns => {
5359 type => 'string',
5360 description => "the task ID.",
5361 },
5362 code => sub {
5363 my ($param) = @_;
5364
5365 my $rpcenv = PVE::RPCEnvironment::get();
5366
5367 my $authuser = $rpcenv->get_user();
5368
5369 my $node = extract_param($param, 'node');
5370
5371 my $vmid = extract_param($param, 'vmid');
5372
5373 my $snapname = extract_param($param, 'snapname');
5374
7e7d7b61 5375 my $realcmd = sub {
22c377f0 5376 PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
b2c9558d 5377 PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
76b29aaf 5378
a46d039d 5379 if ($param->{start} && !PVE::QemuServer::Helpers::vm_running_locally($vmid)) {
76b29aaf
SH
5380 PVE::API2::Qemu->vm_start({ vmid => $vmid, node => $node });
5381 }
7e7d7b61
DM
5382 };
5383
c068c1c3
WL
5384 my $worker = sub {
5385 # hold migration lock, this makes sure that nobody create replication snapshots
5386 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
5387 };
5388
5389 return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $worker);
7e7d7b61
DM
5390 }});
5391
5392__PACKAGE__->register_method({
5393 name => 'delsnapshot',
5394 path => '{vmid}/snapshot/{snapname}',
5395 method => 'DELETE',
5396 protected => 1,
5397 proxyto => 'node',
5398 description => "Delete a VM snapshot.",
5399 permissions => {
f1baf1df 5400 check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
7e7d7b61
DM
5401 },
5402 parameters => {
5403 additionalProperties => 0,
5404 properties => {
5405 node => get_standard_option('pve-node'),
335af808 5406 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
8abd398b 5407 snapname => get_standard_option('pve-snapshot-name'),
3ee28e38
DM
5408 force => {
5409 optional => 1,
5410 type => 'boolean',
5411 description => "For removal from config file, even if removing disk snapshots fails.",
5412 },
7e7d7b61
DM
5413 },
5414 },
5415 returns => {
5416 type => 'string',
5417 description => "the task ID.",
5418 },
5419 code => sub {
5420 my ($param) = @_;
5421
5422 my $rpcenv = PVE::RPCEnvironment::get();
5423
5424 my $authuser = $rpcenv->get_user();
5425
5426 my $node = extract_param($param, 'node');
5427
5428 my $vmid = extract_param($param, 'vmid');
5429
5430 my $snapname = extract_param($param, 'snapname');
5431
1770b70f 5432 my $lock_obtained;
fdbbed2f 5433 my $do_delete = sub {
1770b70f 5434 $lock_obtained = 1;
22c377f0 5435 PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname");
b2c9558d 5436 PVE::QemuConfig->snapshot_delete($vmid, $snapname, $param->{force});
7e7d7b61 5437 };
9dbd1ee4 5438
fdbbed2f
FE
5439 my $realcmd = sub {
5440 if ($param->{force}) {
5441 $do_delete->();
5442 } else {
1770b70f
FG
5443 eval { PVE::GuestHelpers::guest_migration_lock($vmid, 10, $do_delete); };
5444 if (my $err = $@) {
5445 die $err if $lock_obtained;
5446 die "Failed to obtain guest migration lock - replication running?\n";
5447 }
fdbbed2f
FE
5448 }
5449 };
5450
7b2257a8 5451 return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd);
9dbd1ee4
AD
5452 }});
5453
04a69bb4
AD
5454__PACKAGE__->register_method({
5455 name => 'template',
5456 path => '{vmid}/template',
5457 method => 'POST',
5458 protected => 1,
5459 proxyto => 'node',
5460 description => "Create a Template.",
b02691d8 5461 permissions => {
7af0a6c8
DM
5462 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
5463 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
b02691d8 5464 },
04a69bb4
AD
5465 parameters => {
5466 additionalProperties => 0,
5467 properties => {
5468 node => get_standard_option('pve-node'),
335af808 5469 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid_stopped }),
04a69bb4
AD
5470 disk => {
5471 optional => 1,
5472 type => 'string',
5473 description => "If you want to convert only 1 disk to base image.",
e0fd2b2f 5474 enum => [PVE::QemuServer::Drive::valid_drive_names()],
04a69bb4
AD
5475 },
5476
5477 },
5478 },
b297918c
FG
5479 returns => {
5480 type => 'string',
5481 description => "the task ID.",
5482 },
04a69bb4
AD
5483 code => sub {
5484 my ($param) = @_;
5485
5486 my $rpcenv = PVE::RPCEnvironment::get();
5487
5488 my $authuser = $rpcenv->get_user();
5489
5490 my $node = extract_param($param, 'node');
5491
5492 my $vmid = extract_param($param, 'vmid');
5493
5494 my $disk = extract_param($param, 'disk');
5495
d2ceac56 5496 my $load_and_check = sub {
ffda963f 5497 my $conf = PVE::QemuConfig->load_config($vmid);
04a69bb4 5498
ffda963f 5499 PVE::QemuConfig->check_lock($conf);
04a69bb4 5500
75466c4f 5501 die "unable to create template, because VM contains snapshots\n"
b91c2aae 5502 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
0402a80b 5503
75466c4f 5504 die "you can't convert a template to a template\n"
ffda963f 5505 if PVE::QemuConfig->is_template($conf) && !$disk;
0402a80b 5506
75466c4f 5507 die "you can't convert a VM to template if VM is running\n"
218cab9a 5508 if PVE::QemuServer::check_running($vmid);
35c5fdef 5509
d2ceac56
FG
5510 return $conf;
5511 };
04a69bb4 5512
d2ceac56 5513 $load_and_check->();
75e7e997 5514
d2ceac56
FG
5515 my $realcmd = sub {
5516 PVE::QemuConfig->lock_config($vmid, sub {
5517 my $conf = $load_and_check->();
5518
5519 $conf->{template} = 1;
5520 PVE::QemuConfig->write_config($vmid, $conf);
5521
5522 PVE::QemuServer::template_create($vmid, $conf, $disk);
5523 });
04a69bb4
AD
5524 };
5525
d2ceac56 5526 return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd);
04a69bb4
AD
5527 }});
5528
73709749
ML
5529__PACKAGE__->register_method({
5530 name => 'cloudinit_generated_config_dump',
5531 path => '{vmid}/cloudinit/dump',
5532 method => 'GET',
5533 proxyto => 'node',
5534 description => "Get automatically generated cloudinit config.",
5535 permissions => {
5536 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
5537 },
5538 parameters => {
5539 additionalProperties => 0,
5540 properties => {
5541 node => get_standard_option('pve-node'),
5542 vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
5543 type => {
5544 description => 'Config type.',
5545 type => 'string',
5546 enum => ['user', 'network', 'meta'],
5547 },
5548 },
5549 },
5550 returns => {
5551 type => 'string',
5552 },
5553 code => sub {
5554 my ($param) = @_;
5555
5556 my $conf = PVE::QemuConfig->load_config($param->{vmid});
5557
5558 return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, $param->{vmid}, $param->{type});
5559 }});
5560
347dc136
FG
5561__PACKAGE__->register_method({
5562 name => 'mtunnel',
5563 path => '{vmid}/mtunnel',
5564 method => 'POST',
5565 protected => 1,
5566 description => 'Migration tunnel endpoint - only for internal use by VM migration.',
5567 permissions => {
5568 check =>
5569 [ 'and',
5570 ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
5571 ['perm', '/', [ 'Sys.Incoming' ]],
5572 ],
5573 description => "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming" .
5574 " on '/'. Further permission checks happen during the actual migration.",
5575 },
5576 parameters => {
5577 additionalProperties => 0,
5578 properties => {
5579 node => get_standard_option('pve-node'),
5580 vmid => get_standard_option('pve-vmid'),
5581 storages => {
5582 type => 'string',
5583 format => 'pve-storage-id-list',
5584 optional => 1,
5585 description => 'List of storages to check permission and availability. Will be checked again for all actually used storages during migration.',
5586 },
06fedff6
FG
5587 bridges => {
5588 type => 'string',
5589 format => 'pve-bridge-id-list',
5590 optional => 1,
5591 description => 'List of network bridges to check availability. Will be checked again for actually used bridges during migration.',
5592 },
347dc136
FG
5593 },
5594 },
5595 returns => {
5596 additionalProperties => 0,
5597 properties => {
5598 upid => { type => 'string' },
5599 ticket => { type => 'string' },
5600 socket => { type => 'string' },
5601 },
5602 },
5603 code => sub {
5604 my ($param) = @_;
5605
5606 my $rpcenv = PVE::RPCEnvironment::get();
5607 my $authuser = $rpcenv->get_user();
5608
5609 my $node = extract_param($param, 'node');
5610 my $vmid = extract_param($param, 'vmid');
5611
5612 my $storages = extract_param($param, 'storages');
06fedff6 5613 my $bridges = extract_param($param, 'bridges');
347dc136
FG
5614
5615 my $nodename = PVE::INotify::nodename();
5616
5617 raise_param_exc({ node => "node needs to be 'localhost' or local hostname '$nodename'" })
5618 if $node ne 'localhost' && $node ne $nodename;
5619
5620 $node = $nodename;
5621
5622 my $storecfg = PVE::Storage::config();
5623 foreach my $storeid (PVE::Tools::split_list($storages)) {
5624 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storeid, $node);
5625 }
5626
06fedff6
FG
5627 foreach my $bridge (PVE::Tools::split_list($bridges)) {
5628 PVE::Network::read_bridge_mtu($bridge);
5629 }
5630
347dc136
FG
5631 PVE::Cluster::check_cfs_quorum();
5632
5633 my $lock = 'create';
5634 eval { PVE::QemuConfig->create_and_lock_config($vmid, 0, $lock); };
5635
5636 raise_param_exc({ vmid => "unable to create empty VM config - $@"})
5637 if $@;
5638
5639 my $realcmd = sub {
5640 my $state = {
5641 storecfg => PVE::Storage::config(),
5642 lock => $lock,
5643 vmid => $vmid,
5644 };
5645
5646 my $run_locked = sub {
5647 my ($code, $params) = @_;
5648 return PVE::QemuConfig->lock_config($state->{vmid}, sub {
5649 my $conf = PVE::QemuConfig->load_config($state->{vmid});
5650
5651 $state->{conf} = $conf;
5652
5653 die "Encountered wrong lock - aborting mtunnel command handling.\n"
5654 if $state->{lock} && !PVE::QemuConfig->has_lock($conf, $state->{lock});
5655
5656 return $code->($params);
5657 });
5658 };
5659
5660 my $cmd_desc = {
5661 config => {
5662 conf => {
5663 type => 'string',
5664 description => 'Full VM config, adapted for target cluster/node',
5665 },
5666 'firewall-config' => {
5667 type => 'string',
5668 description => 'VM firewall config',
5669 optional => 1,
5670 },
5671 },
5672 disk => {
5673 format => PVE::JSONSchema::get_standard_option('pve-qm-image-format'),
5674 storage => {
5675 type => 'string',
5676 format => 'pve-storage-id',
5677 },
5678 drive => {
5679 type => 'object',
5680 description => 'parsed drive information without volid and format',
5681 },
5682 },
5683 start => {
5684 start_params => {
5685 type => 'object',
5686 description => 'params passed to vm_start_nolock',
5687 },
5688 migrate_opts => {
5689 type => 'object',
5690 description => 'migrate_opts passed to vm_start_nolock',
5691 },
5692 },
5693 ticket => {
5694 path => {
5695 type => 'string',
5696 description => 'socket path for which the ticket should be valid. must be known to current mtunnel instance.',
5697 },
5698 },
5699 quit => {
5700 cleanup => {
5701 type => 'boolean',
5702 description => 'remove VM config and disks, aborting migration',
5703 default => 0,
5704 },
5705 },
5706 'disk-import' => $PVE::StorageTunnel::cmd_schema->{'disk-import'},
5707 'query-disk-import' => $PVE::StorageTunnel::cmd_schema->{'query-disk-import'},
5708 bwlimit => $PVE::StorageTunnel::cmd_schema->{bwlimit},
5709 };
5710
5711 my $cmd_handlers = {
5712 'version' => sub {
5713 # compared against other end's version
5714 # bump/reset for breaking changes
5715 # bump/bump for opt-in changes
5716 return {
eef93bc5 5717 api => $PVE::QemuMigrate::WS_TUNNEL_VERSION,
347dc136
FG
5718 age => 0,
5719 };
5720 },
5721 'config' => sub {
5722 my ($params) = @_;
5723
5724 # parse and write out VM FW config if given
5725 if (my $fw_conf = $params->{'firewall-config'}) {
5726 my ($path, $fh) = PVE::Tools::tempfile_contents($fw_conf, 700);
5727
5728 my $empty_conf = {
5729 rules => [],
5730 options => {},
5731 aliases => {},
5732 ipset => {} ,
5733 ipset_comments => {},
5734 };
5735 my $cluster_fw_conf = PVE::Firewall::load_clusterfw_conf();
5736
5737 # TODO: add flag for strict parsing?
5738 # TODO: add import sub that does all this given raw content?
5739 my $vmfw_conf = PVE::Firewall::generic_fw_config_parser($path, $cluster_fw_conf, $empty_conf, 'vm');
5740 $vmfw_conf->{vmid} = $state->{vmid};
5741 PVE::Firewall::save_vmfw_conf($state->{vmid}, $vmfw_conf);
5742
5743 $state->{cleanup}->{fw} = 1;
5744 }
5745
5746 my $conf_fn = "incoming/qemu-server/$state->{vmid}.conf";
5747 my $new_conf = PVE::QemuServer::parse_vm_config($conf_fn, $params->{conf}, 1);
5748 delete $new_conf->{lock};
5749 delete $new_conf->{digest};
5750
5751 # TODO handle properly?
5752 delete $new_conf->{snapshots};
5753 delete $new_conf->{parent};
5754 delete $new_conf->{pending};
5755
5756 # not handled by update_vm_api
5757 my $vmgenid = delete $new_conf->{vmgenid};
5758 my $meta = delete $new_conf->{meta};
5759 my $cloudinit = delete $new_conf->{cloudinit}; # this is informational only
5760 $new_conf->{skip_cloud_init} = 1; # re-use image from source side
5761
5762 $new_conf->{vmid} = $state->{vmid};
5763 $new_conf->{node} = $node;
5764
5765 PVE::QemuConfig->remove_lock($state->{vmid}, 'create');
5766
5767 eval {
5768 $update_vm_api->($new_conf, 1);
5769 };
5770 if (my $err = $@) {
5771 # revert to locked previous config
5772 my $conf = PVE::QemuConfig->load_config($state->{vmid});
5773 $conf->{lock} = 'create';
5774 PVE::QemuConfig->write_config($state->{vmid}, $conf);
5775
5776 die $err;
5777 }
5778
5779 my $conf = PVE::QemuConfig->load_config($state->{vmid});
5780 $conf->{lock} = 'migrate';
5781 $conf->{vmgenid} = $vmgenid if defined($vmgenid);
5782 $conf->{meta} = $meta if defined($meta);
5783 $conf->{cloudinit} = $cloudinit if defined($cloudinit);
5784 PVE::QemuConfig->write_config($state->{vmid}, $conf);
5785
5786 $state->{lock} = 'migrate';
5787
5788 return;
5789 },
5790 'bwlimit' => sub {
5791 my ($params) = @_;
5792 return PVE::StorageTunnel::handle_bwlimit($params);
5793 },
5794 'disk' => sub {
5795 my ($params) = @_;
5796
5797 my $format = $params->{format};
5798 my $storeid = $params->{storage};
5799 my $drive = $params->{drive};
5800
5801 $check_storage_access_migrate->($rpcenv, $authuser, $state->{storecfg}, $storeid, $node);
5802
5803 my $storagemap = {
5804 default => $storeid,
5805 };
5806
5807 my $source_volumes = {
5808 'disk' => [
5809 undef,
5810 $storeid,
347dc136
FG
5811 $drive,
5812 0,
5813 $format,
5814 ],
5815 };
5816
5817 my $res = PVE::QemuServer::vm_migrate_alloc_nbd_disks($state->{storecfg}, $state->{vmid}, $source_volumes, $storagemap);
5818 if (defined($res->{disk})) {
5819 $state->{cleanup}->{volumes}->{$res->{disk}->{volid}} = 1;
5820 return $res->{disk};
5821 } else {
5822 die "failed to allocate NBD disk..\n";
5823 }
5824 },
5825 'disk-import' => sub {
5826 my ($params) = @_;
5827
5828 $check_storage_access_migrate->(
5829 $rpcenv,
5830 $authuser,
5831 $state->{storecfg},
5832 $params->{storage},
5833 $node
5834 );
5835
5836 $params->{unix} = "/run/qemu-server/$state->{vmid}.storage";
5837
5838 return PVE::StorageTunnel::handle_disk_import($state, $params);
5839 },
5840 'query-disk-import' => sub {
5841 my ($params) = @_;
5842
5843 return PVE::StorageTunnel::handle_query_disk_import($state, $params);
5844 },
5845 'start' => sub {
5846 my ($params) = @_;
5847
5848 my $info = PVE::QemuServer::vm_start_nolock(
5849 $state->{storecfg},
5850 $state->{vmid},
5851 $state->{conf},
5852 $params->{start_params},
5853 $params->{migrate_opts},
5854 );
5855
5856
5857 if ($info->{migrate}->{proto} ne 'unix') {
5858 PVE::QemuServer::vm_stop(undef, $state->{vmid}, 1, 1);
5859 die "migration over non-UNIX sockets not possible\n";
5860 }
5861
5862 my $socket = $info->{migrate}->{addr};
5863 chown $state->{socket_uid}, -1, $socket;
5864 $state->{sockets}->{$socket} = 1;
5865
5866 my $unix_sockets = $info->{migrate}->{unix_sockets};
5867 foreach my $socket (@$unix_sockets) {
5868 chown $state->{socket_uid}, -1, $socket;
5869 $state->{sockets}->{$socket} = 1;
5870 }
5871 return $info;
5872 },
5873 'fstrim' => sub {
5874 if (PVE::QemuServer::qga_check_running($state->{vmid})) {
5875 eval { mon_cmd($state->{vmid}, "guest-fstrim") };
5876 warn "fstrim failed: $@\n" if $@;
5877 }
5878 return;
5879 },
5880 'stop' => sub {
5881 PVE::QemuServer::vm_stop(undef, $state->{vmid}, 1, 1);
5882 return;
5883 },
5884 'nbdstop' => sub {
5885 PVE::QemuServer::nbd_stop($state->{vmid});
5886 return;
5887 },
5888 'resume' => sub {
5889 if (PVE::QemuServer::Helpers::vm_running_locally($state->{vmid})) {
5890 PVE::QemuServer::vm_resume($state->{vmid}, 1, 1);
5891 } else {
5892 die "VM $state->{vmid} not running\n";
5893 }
5894 return;
5895 },
5896 'unlock' => sub {
5897 PVE::QemuConfig->remove_lock($state->{vmid}, $state->{lock});
5898 delete $state->{lock};
5899 return;
5900 },
5901 'ticket' => sub {
5902 my ($params) = @_;
5903
5904 my $path = $params->{path};
5905
5906 die "Not allowed to generate ticket for unknown socket '$path'\n"
5907 if !defined($state->{sockets}->{$path});
5908
5909 return { ticket => PVE::AccessControl::assemble_tunnel_ticket($authuser, "/socket/$path") };
5910 },
5911 'quit' => sub {
5912 my ($params) = @_;
5913
5914 if ($params->{cleanup}) {
5915 if ($state->{cleanup}->{fw}) {
5916 PVE::Firewall::remove_vmfw_conf($state->{vmid});
5917 }
5918
5919 for my $volid (keys $state->{cleanup}->{volumes}->%*) {
5920 print "freeing volume '$volid' as part of cleanup\n";
5921 eval { PVE::Storage::vdisk_free($state->{storecfg}, $volid) };
5922 warn $@ if $@;
5923 }
5924
5925 PVE::QemuServer::destroy_vm($state->{storecfg}, $state->{vmid}, 1);
5926 }
5927
5928 print "switching to exit-mode, waiting for client to disconnect\n";
5929 $state->{exit} = 1;
5930 return;
5931 },
5932 };
5933
5934 $run_locked->(sub {
5935 my $socket_addr = "/run/qemu-server/$state->{vmid}.mtunnel";
5936 unlink $socket_addr;
5937
5938 $state->{socket} = IO::Socket::UNIX->new(
5939 Type => SOCK_STREAM(),
5940 Local => $socket_addr,
5941 Listen => 1,
5942 );
5943
5944 $state->{socket_uid} = getpwnam('www-data')
5945 or die "Failed to resolve user 'www-data' to numeric UID\n";
5946 chown $state->{socket_uid}, -1, $socket_addr;
5947 });
5948
5949 print "mtunnel started\n";
5950
5951 my $conn = eval { PVE::Tools::run_with_timeout(300, sub { $state->{socket}->accept() }) };
5952 if ($@) {
5953 warn "Failed to accept tunnel connection - $@\n";
5954
5955 warn "Removing tunnel socket..\n";
5956 unlink $state->{socket};
5957
5958 warn "Removing temporary VM config..\n";
5959 $run_locked->(sub {
5960 PVE::QemuServer::destroy_vm($state->{storecfg}, $state->{vmid}, 1);
5961 });
5962
5963 die "Exiting mtunnel\n";
5964 }
5965
5966 $state->{conn} = $conn;
5967
5968 my $reply_err = sub {
5969 my ($msg) = @_;
5970
5971 my $reply = JSON::encode_json({
5972 success => JSON::false,
5973 msg => $msg,
5974 });
5975 $conn->print("$reply\n");
5976 $conn->flush();
5977 };
5978
5979 my $reply_ok = sub {
5980 my ($res) = @_;
5981
5982 $res->{success} = JSON::true;
5983 my $reply = JSON::encode_json($res);
5984 $conn->print("$reply\n");
5985 $conn->flush();
5986 };
5987
5988 while (my $line = <$conn>) {
5989 chomp $line;
5990
5991 # untaint, we validate below if needed
5992 ($line) = $line =~ /^(.*)$/;
5993 my $parsed = eval { JSON::decode_json($line) };
5994 if ($@) {
5995 $reply_err->("failed to parse command - $@");
5996 next;
5997 }
5998
5999 my $cmd = delete $parsed->{cmd};
6000 if (!defined($cmd)) {
6001 $reply_err->("'cmd' missing");
6002 } elsif ($state->{exit}) {
6003 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd not possible");
6004 next;
6005 } elsif (my $handler = $cmd_handlers->{$cmd}) {
6006 print "received command '$cmd'\n";
6007 eval {
6008 if ($cmd_desc->{$cmd}) {
6009 PVE::JSONSchema::validate($parsed, $cmd_desc->{$cmd});
6010 } else {
6011 $parsed = {};
6012 }
6013 my $res = $run_locked->($handler, $parsed);
6014 $reply_ok->($res);
6015 };
6016 $reply_err->("failed to handle '$cmd' command - $@")
6017 if $@;
6018 } else {
6019 $reply_err->("unknown command '$cmd' given");
6020 }
6021 }
6022
6023 if ($state->{exit}) {
6024 print "mtunnel exited\n";
6025 } else {
6026 die "mtunnel exited unexpectedly\n";
6027 }
6028 };
6029
6030 my $socket_addr = "/run/qemu-server/$vmid.mtunnel";
6031 my $ticket = PVE::AccessControl::assemble_tunnel_ticket($authuser, "/socket/$socket_addr");
6032 my $upid = $rpcenv->fork_worker('qmtunnel', $vmid, $authuser, $realcmd);
6033
6034 return {
6035 ticket => $ticket,
6036 upid => $upid,
6037 socket => $socket_addr,
6038 };
6039 }});
6040
6041__PACKAGE__->register_method({
6042 name => 'mtunnelwebsocket',
6043 path => '{vmid}/mtunnelwebsocket',
6044 method => 'GET',
6045 permissions => {
6046 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.",
6047 user => 'all', # check inside
6048 },
6049 description => 'Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.',
6050 parameters => {
6051 additionalProperties => 0,
6052 properties => {
6053 node => get_standard_option('pve-node'),
6054 vmid => get_standard_option('pve-vmid'),
6055 socket => {
6056 type => "string",
6057 description => "unix socket to forward to",
6058 },
6059 ticket => {
6060 type => "string",
6061 description => "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command",
6062 },
6063 },
6064 },
6065 returns => {
6066 type => "object",
6067 properties => {
6068 port => { type => 'string', optional => 1 },
6069 socket => { type => 'string', optional => 1 },
6070 },
6071 },
6072 code => sub {
6073 my ($param) = @_;
6074
6075 my $rpcenv = PVE::RPCEnvironment::get();
6076 my $authuser = $rpcenv->get_user();
6077
6078 my $nodename = PVE::INotify::nodename();
6079 my $node = extract_param($param, 'node');
6080
6081 raise_param_exc({ node => "node needs to be 'localhost' or local hostname '$nodename'" })
6082 if $node ne 'localhost' && $node ne $nodename;
6083
6084 my $vmid = $param->{vmid};
6085 # check VM exists
6086 PVE::QemuConfig->load_config($vmid);
6087
6088 my $socket = $param->{socket};
6089 PVE::AccessControl::verify_tunnel_ticket($param->{ticket}, $authuser, "/socket/$socket");
6090
6091 return { socket => $socket };
6092 }});
6093
1e3baf05 60941;