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