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