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