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