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