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