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