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