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